diff --git a/Mail/AppDelegate.swift b/Mail/AppDelegate.swift index e6a4bb6a6..0f1ca774c 100644 --- a/Mail/AppDelegate.swift +++ b/Mail/AppDelegate.swift @@ -17,31 +17,19 @@ */ import CocoaLumberjackSwift -import InfomaniakBugTracker -import InfomaniakCore -import InfomaniakCoreUI import InfomaniakDI -import InfomaniakLogin import InfomaniakNotifications import MailCore -import Sentry -import SwiftUI import UIKit -@main class AppDelegate: UIResponder, UIApplicationDelegate { private let notificationCenterDelegate = NotificationCenterDelegate() - private var accountManager: AccountManager! static var orientationLock = UIInterfaceOrientationMask.all - 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 - + func application( + _ application: UIApplication, + willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + ) -> Bool { UNUserNotificationCenter.current().delegate = notificationCenterDelegate Task { // Ask permission app launch @@ -52,17 +40,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return true } - func application(_ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - // Override point for customization after application launch. - return true - } - func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { @InjectService var notificationService: InfomaniakNotifications - for account in accountManager.accounts { + for account in AccountManager.instance.accounts { guard account.token != nil else { continue } - let userApiFetcher = accountManager.getApiFetcher(for: account.userId, token: account.token) + let userApiFetcher = AccountManager.instance.getApiFetcher(for: account.userId, token: account.token) Task { await notificationService.updateRemoteNotificationsTokenIfNeeded(tokenData: deviceToken, userApiFetcher: userApiFetcher) @@ -74,65 +56,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { DDLogError("Failed registering for notifications: \(error)") } - // MARK: UISceneSession Lifecycle - - func application(_ application: UIApplication, - configurationForConnecting connectingSceneSession: UISceneSession, - options: UIScene.ConnectionOptions) -> UISceneConfiguration { - // Called when a new scene session is being created. - // Use this method to select a configuration to create the new scene with. - return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) - } - - func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { - // Called when the user discards a scene session. - // If any sessions were discarded while the application was not running, this will be called shortly after - // application:didFinishLaunchingWithOptions. - // Use this method to release any resources that were specific to the discarded scenes, as they will not return. - } - func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask { return AppDelegate.orientationLock } - - 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() - } - - 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) - } } diff --git a/Mail/Components/MailboxListView.swift b/Mail/Components/MailboxListView.swift index fbf8ff205..a941d89cd 100644 --- a/Mail/Components/MailboxListView.swift +++ b/Mail/Components/MailboxListView.swift @@ -22,8 +22,6 @@ import RealmSwift import SwiftUI struct MailboxListView: View { - @Environment(\.window) private var window - @AppStorage(UserDefaults.shared.key(.accentColor)) private var accentColor = DefaultPreferences.accentColor @ObservedResults( @@ -48,12 +46,7 @@ struct MailboxListView: View { Spacer() NavigationLink { - AddMailboxView { mailbox in - DispatchQueue.main.async { - guard let mailbox = mailbox else { return } - (window?.windowScene?.delegate as? SceneDelegate)?.switchMailbox(mailbox) - } - } + AddMailboxView() } label: { MailResourcesAsset.addCircle.swiftUIImage .resizable() diff --git a/Mail/Components/UnavailableMailboxListView.swift b/Mail/Components/UnavailableMailboxListView.swift index f96cc5cf1..0108a2ab1 100644 --- a/Mail/Components/UnavailableMailboxListView.swift +++ b/Mail/Components/UnavailableMailboxListView.swift @@ -22,8 +22,6 @@ import RealmSwift import SwiftUI struct UnavailableMailboxListView: View { - @Environment(\.window) private var window - @AppStorage(UserDefaults.shared.key(.accentColor)) private var accentColor = DefaultPreferences.accentColor @ObservedResults( diff --git a/Mail/Info.plist b/Mail/Info.plist index 60010620c..409fa15c7 100644 --- a/Mail/Info.plist +++ b/Mail/Info.plist @@ -55,25 +55,6 @@ To be able to lock the app NSLocalNetworkUsageDescription Atlantis would use Bonjour Service to discover Proxyman app from your local network. - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - UISceneConfigurations - - UIWindowSceneSessionRoleApplication - - - UISceneConfigurationName - Default Configuration - UISceneDelegateClassName - $(PRODUCT_MODULE_NAME).SceneDelegate - UISceneStoryboardFile - Main - - - - UIApplicationSupportsIndirectInputEvents UIBackgroundModes @@ -82,8 +63,6 @@ UILaunchStoryboardName LaunchScreen - UIMainStoryboardFile - Main UIRequiredDeviceCapabilities armv7 diff --git a/Mail/LockedAppView.swift b/Mail/LockedAppView.swift index 9e471c78d..582dcb2bc 100644 --- a/Mail/LockedAppView.swift +++ b/Mail/LockedAppView.swift @@ -24,7 +24,8 @@ import SwiftUI struct LockedAppView: View { @LazyInjectService var appLockHelper: AppLockHelper - @Environment(\.window) var window + + @EnvironmentObject var navigationState: NavigationState var body: some View { ZStack { @@ -61,7 +62,10 @@ struct LockedAppView: View { private func unlockApp() { Task { if (try? await appLockHelper.evaluatePolicy(reason: MailResourcesStrings.Localizable.lockAppTitle)) == true { - await (window?.windowScene?.delegate as? SceneDelegate)?.showMainView() + appLockHelper.setTime() + Task { + navigationState.transitionToRootViewDestination(.mainView) + } } } } diff --git a/Mail/MailApp.swift b/Mail/MailApp.swift new file mode 100644 index 000000000..2f8c5c2f1 --- /dev/null +++ b/Mail/MailApp.swift @@ -0,0 +1,162 @@ +/* + 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 InfomaniakBugTracker +import InfomaniakCore +import InfomaniakCoreUI +import InfomaniakDI +import InfomaniakLogin +import InfomaniakNotifications +import MailCore +import Sentry +import SwiftUI +import UIKit + +public struct EarlyDIHook { + public init() { + // setup DI ASAP + setupDI() + } + + func setupDI() { + let networkLoginService = Factory(type: InfomaniakNetworkLoginable.self) { _, _ in + InfomaniakNetworkLogin(clientId: MailApiFetcher.clientId) + } + let loginService = Factory(type: InfomaniakLoginable.self) { _, _ in + InfomaniakLogin(clientId: MailApiFetcher.clientId) + } + let keychainHelper = Factory(type: KeychainHelper.self) { _, _ in + KeychainHelper(accessGroup: AccountManager.accessGroup) + } + let 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() + } + + 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) + } +} + +@main +struct MailApp: App { + /// Making sure the DI is registered at a very early stage of the app launch. + private let dependencyInjectionHook = EarlyDIHook() + @LazyInjectService var appLockHelper: AppLockHelper + + @UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate + + @Environment(\.scenePhase) private var scenePhase + + @AppStorage(UserDefaults.shared.key(.accentColor)) private var accentColor = DefaultPreferences.accentColor + @AppStorage(UserDefaults.shared.key(.theme)) private var theme = DefaultPreferences.theme + + @StateObject private var navigationState = NavigationState() + + private let accountManager = AccountManager.instance + + init() { + Logging.initLogging() + DDLogInfo("Application starting in foreground ? \(UIApplication.shared.applicationState != .background)") + ApiFetcher.decoder.dateDecodingStrategy = .iso8601 + } + + var body: some Scene { + WindowGroup { + RootView() + .environmentObject(navigationState) + .onAppear { + updateUI(accent: accentColor, theme: theme) + } + .onChange(of: theme) { newTheme in + updateUI(accent: accentColor, theme: newTheme) + } + .onChange(of: accentColor) { newAccentColor in + updateUI(accent: newAccentColor, theme: theme) + } + .onChange(of: scenePhase) { newScenePhase in + switch newScenePhase { + case .active: + refreshCacheData() + navigationState.transitionToLockViewIfNeeded() + case .background: + if UserDefaults.shared.isAppLockEnabled && navigationState.rootViewState != .appLocked { + appLockHelper.setTime() + } + case .inactive: + Task { + await NotificationsHelper.updateUnreadCountBadge() + } + @unknown default: + break + } + } + .onChange(of: accountManager.currentAccount) { _ in + refreshCacheData() + } + } + .defaultAppStorage(.shared) + } + + func updateUI(accent: AccentColor, theme: Theme) { + let allWindows = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.flatMap { $0.windows } + for window in allWindows { + window.tintColor = accent.primary.color + window.overrideUserInterfaceStyle = theme.interfaceStyle + } + } + + func refreshCacheData() { + guard let currentAccount = accountManager.currentAccount else { + return + } + + Task { + do { + try await accountManager.updateUser(for: currentAccount) + accountManager.enableBugTrackerIfAvailable() + + try await accountManager.currentContactManager?.fetchContactsAndAddressBooks() + } catch { + DDLogError("Error while updating user account: \(error)") + } + } + } +} diff --git a/Mail/NotificationCenterDelegate.swift b/Mail/NotificationCenterDelegate.swift index 7bcc51ae0..5439099d5 100644 --- a/Mail/NotificationCenterDelegate.swift +++ b/Mail/NotificationCenterDelegate.swift @@ -39,10 +39,11 @@ class NotificationCenterDelegate: NSObject, UNUserNotificationCenterDelegate { if AccountManager.instance.currentAccount.userId != mailboxManager.mailbox.userId { if let switchedAccount = AccountManager.instance.accounts .first(where: { $0.userId == mailboxManager.mailbox.userId }) { - (scene?.delegate as? SceneDelegate)?.switchAccount(switchedAccount, mailbox: mailbox) + AccountManager.instance.switchAccount(newAccount: switchedAccount) + AccountManager.instance.switchMailbox(newMailbox: mailbox) } } else { - (scene?.delegate as? SceneDelegate)?.switchMailbox(mailbox) + AccountManager.instance.switchMailbox(newMailbox: mailbox) } } diff --git a/Mail/RootView.swift b/Mail/RootView.swift new file mode 100644 index 000000000..6fc7891a7 --- /dev/null +++ b/Mail/RootView.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 SwiftUI + +struct RootView: View { + @Environment(\.horizontalSizeClass) private var horizontalSizeClass + @Environment(\.verticalSizeClass) private var verticalSizeClass + + @EnvironmentObject private var navigationState: NavigationState + + var body: some View { + ZStack { + switch navigationState.rootViewState { + case .appLocked: + LockedAppView() + case .mainView(let currentMailboxManager): + SplitView(mailboxManager: currentMailboxManager) + case .onboarding: + OnboardingView() + case .noMailboxes: + NoMailboxView() + case .unavailableMailboxes: + UnavailableMailboxesView() + } + } + .environment(\.isCompactWindow, horizontalSizeClass == .compact || verticalSizeClass == .compact) + } +} diff --git a/Mail/SceneDelegate.swift b/Mail/SceneDelegate.swift deleted file mode 100644 index c0f155158..000000000 --- a/Mail/SceneDelegate.swift +++ /dev/null @@ -1,217 +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 CocoaLumberjackSwift -import InfomaniakCore -import InfomaniakCoreUI -import InfomaniakDI -import MailCore -import MailResources -import SwiftUI -import UIKit - -class SceneDelegate: UIResponder, UIWindowSceneDelegate, AccountManagerDelegate { - var window: UIWindow? - - private var accountManager: AccountManager! - @LazyInjectService var appLockHelper: AppLockHelper - - 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`. - // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. - // 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() - if let mailToURL = connectionOptions.urlContexts.first?.url { - handleUrlOpen(mailToURL) - } - } - - func sceneDidDisconnect(_ scene: UIScene) { - // Called as the scene is being released by the system. - // This occurs shortly after the scene enters the background, or when its session is discarded. - // Release any resources associated with this scene that can be re-created the next time the scene connects. - // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` - // instead). - } - - func sceneDidBecomeActive(_ scene: UIScene) { - // Called when the scene has moved from an inactive state to an active state. - // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. - } - - func sceneWillResignActive(_ scene: UIScene) { - // Called when the scene will move from an active state to an inactive state. - // This may occur due to temporary interruptions (ex. an incoming phone call). - Task { - await NotificationsHelper.updateUnreadCountBadge() - } - } - - 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. - refreshCacheData() - - if UserDefaults.shared.isAppLockEnabled && appLockHelper.isAppLocked { - showLockView() - } - } - - func sceneDidEnterBackground(_ scene: UIScene) { - // Called as the scene transitions from the foreground to the background. - // Use this method to save data, release shared resources, and store enough scene-specific state information - // to restore the scene back to its current state. - - // Cast rootViewController in a UIHostingViewController containing a LockedAppView and a UIWindow? environment variable - if UserDefaults.shared.isAppLockEnabled && window?.rootViewController?.isKind(of: UIHostingController - >>.self) != true { - appLockHelper.setTime() - } - } - - func setRootViewController(_ viewController: UIViewController, animated: Bool = true) { - guard let window = window else { return } - window.rootViewController = viewController - window.makeKeyAndVisible() - if animated { - UIView.transition(with: window, duration: 0.3, options: .transitionCrossDissolve, animations: nil, completion: nil) - } - } - - func setRootView(_ view: Content, animated: Bool = true) where Content: View { - // Inject window in view as environment variable - let view = view.environment(\.window, window) - // Set root view controller - let hostingController = UIHostingController(rootView: view) - setRootViewController(hostingController) - } - - func currentAccountNeedsAuthentication() { - DispatchQueue.main.async { [weak self] in - self?.showLoginView() - } - } - - private func setupLaunch() { - if accountManager.accounts.isEmpty { - showLoginView(animated: false) - } else { - showMainView(animated: false) - } - } - - func switchMailbox(_ mailbox: Mailbox) { - accountManager.switchMailbox(newMailbox: mailbox) - if let mailboxManager = accountManager.getMailboxManager(for: mailbox) { - showMainView(mailboxManager: mailboxManager) - } - } - - func switchAccount(_ account: Account, mailbox: Mailbox? = nil) { - accountManager.switchAccount(newAccount: account) - refreshCacheData() - - if let mailbox = mailbox { - switchMailbox(mailbox) - } else { - showMainView() - } - } - - func updateWindowUI() { - window?.tintColor = UserDefaults.shared.accentColor.primary.color - window?.overrideUserInterfaceStyle = UserDefaults.shared.theme.interfaceStyle - } - - // MARK: - Show views - - func showLoginView(animated: Bool = true) { - setRootView(OnboardingView(), animated: animated) - } - - func showMainView(mailboxManager: MailboxManager, animated: Bool = true) { - setRootView(SplitView(mailboxManager: mailboxManager), animated: animated) - } - - func showNoMailboxView(animated: Bool = true) { - setRootView(NoMailboxView(), animated: animated) - } - - func showMainView(animated: Bool = true) { - if let mailboxManager = accountManager.currentMailboxManager { - showMainView(mailboxManager: mailboxManager, animated: animated) - } else if !accountManager.mailboxes.isEmpty && accountManager.mailboxes.allSatisfy({ !$0.isAvailable }) { - setRootView(UnavailableMailboxesView(), animated: animated) - } else { - showNoMailboxView(animated: animated) - } - } - - func showLockView() { - setRootView(LockedAppView(), animated: false) - } - - func refreshCacheData() { - guard let currentAccount = AccountManager.instance.currentAccount else { - return - } - - Task { - do { - let mailboxIdBeforeSwitching = accountManager.currentMailboxId - try await accountManager.updateUser(for: currentAccount) - accountManager.enableBugTrackerIfAvailable() - - try await accountManager.currentContactManager?.fetchContactsAndAddressBooks() - - if mailboxIdBeforeSwitching != accountManager.currentMailboxId { - showMainView() - } - } catch { - DDLogError("Error while updating user account: \(error)") - } - } - } - - // MARK: - Open URLs - - func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { - _ = URLContexts.first { handleUrlOpen($0.url) } - } - - @discardableResult - func handleUrlOpen(_ url: URL) -> Bool { - guard let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: true) else { return false } - - if Constants.isMailTo(url) { - NotificationCenter.default.post( - name: .onOpenedMailTo, - object: IdentifiableURLComponents(urlComponents: urlComponents) - ) - } - - return true - } -} diff --git a/Mail/Utils/Environment+Extension.swift b/Mail/Utils/Environment+Extension.swift index cd1468be5..ab1dc4904 100644 --- a/Mail/Utils/Environment+Extension.swift +++ b/Mail/Utils/Environment+Extension.swift @@ -19,17 +19,6 @@ import Foundation import SwiftUI -public struct WindowKey: EnvironmentKey { - public static let defaultValue: UIWindow? = nil -} - -public extension EnvironmentValues { - var window: WindowKey.Value { - get { return self[WindowKey.self] } - set { self[WindowKey.self] = newValue } - } -} - public struct CompactWindowKey: EnvironmentKey { public static let defaultValue = true } diff --git a/Mail/Utils/NavigationState.swift b/Mail/Utils/NavigationState.swift new file mode 100644 index 000000000..d082ac9c4 --- /dev/null +++ b/Mail/Utils/NavigationState.swift @@ -0,0 +1,147 @@ +/* + 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 InfomaniakCoreUI +import InfomaniakDI +import MailCore +import SwiftUI + +enum RootViewState: Equatable, Hashable, Identifiable { + var id: Int { + return hashValue + } + + static func == (lhs: RootViewState, rhs: RootViewState) -> Bool { + switch (lhs, rhs) { + case (.appLocked, .appLocked): + return true + case (.onboarding, .onboarding): + return true + case (.noMailboxes, .noMailboxes): + return true + case (.unavailableMailboxes, .unavailableMailboxes): + return true + case (.mainView(let lhsMailboxManager), .mainView(let rhsMailboxManager)): + return lhsMailboxManager.mailbox.objectId == rhsMailboxManager.mailbox.objectId + default: + return false + } + } + + func hash(into hasher: inout Hasher) { + switch self { + case .appLocked: + hasher.combine("applocked") + case .mainView(let mailboxManager): + hasher.combine("mainView\(mailboxManager.mailbox.objectId)") + case .onboarding: + hasher.combine("onboarding") + case .noMailboxes: + hasher.combine("noMailboxes") + case .unavailableMailboxes: + hasher.combine("unavailableMailboxes") + } + } + + case appLocked + case mainView(MailboxManager) + case onboarding + case noMailboxes + case unavailableMailboxes +} + +enum RootViewDestination { + case appLocked + case mainView + case onboarding + case noMailboxes + case unavailableMailboxes +} + +@MainActor +/// Something that represents the state of navigation +class NavigationState: ObservableObject { + @LazyInjectService private var appLockHelper: AppLockHelper + + private let accountManager = AccountManager.instance + private var accountManagerObservation: AnyCancellable? + + @Published private(set) var rootViewState: RootViewState + @Published var messageReply: MessageReply? + @Published var editedMessageDraft: Draft? + + /// Represents the state of navigation + /// + /// The selected thread is the last in collection, by convention. + @Published var threadPath = [Thread]() + + init() { + if accountManager.currentAccount != nil, + let currentMailboxManager = accountManager.currentMailboxManager { + rootViewState = .mainView(currentMailboxManager) + } else if !accountManager.mailboxes.isEmpty && accountManager.mailboxes.allSatisfy({ !$0.isAvailable }) { + rootViewState = .unavailableMailboxes + } else { + rootViewState = .onboarding + } + + accountManagerObservation = accountManager.objectWillChange.receive(on: RunLoop.main).sink { [weak self] in + self?.switchToCurrentMailboxManagerIfPossible() + } + } + + func transitionToRootViewDestination(_ destination: RootViewDestination) { + withAnimation { + switch destination { + case .appLocked: + rootViewState = .appLocked + case .mainView: + switchToCurrentMailboxManagerIfPossible() + case .onboarding: + rootViewState = .onboarding + case .noMailboxes: + rootViewState = .noMailboxes + case .unavailableMailboxes: + rootViewState = .unavailableMailboxes + } + } + } + + func transitionToLockViewIfNeeded() { + if UserDefaults.shared.isAppLockEnabled + && appLockHelper.isAppLocked + && accountManager.currentAccount != nil { + transitionToRootViewDestination(.appLocked) + } + } + + private func switchToCurrentMailboxManagerIfPossible() { + if accountManager.currentAccount != nil, + let currentMailboxManager = accountManager.currentMailboxManager { + if rootViewState != .mainView(currentMailboxManager) { + rootViewState = .mainView(currentMailboxManager) + } + } else if !accountManager.mailboxes.isEmpty && accountManager.mailboxes.allSatisfy({ !$0.isAvailable }) { + rootViewState = .unavailableMailboxes + } else { + rootViewState = .onboarding + } + } +} diff --git a/Mail/Utils/NavigationStore.swift b/Mail/Utils/NavigationStore.swift deleted file mode 100644 index 8c2339ce9..000000000 --- a/Mail/Utils/NavigationStore.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 MailCore -import SwiftUI - -/// Something that represents the state of navigation -final class NavigationStore: ObservableObject { - @Published var messageReply: MessageReply? - - /// Represents the state of navigation - /// - /// The selected thread is the last in collection, by convention. - @Published var threadPath = [Thread]() -} diff --git a/Mail/Views/Alerts/LogoutConfirmationView.swift b/Mail/Views/Alerts/LogoutConfirmationView.swift index d05b62460..b41c765c2 100644 --- a/Mail/Views/Alerts/LogoutConfirmationView.swift +++ b/Mail/Views/Alerts/LogoutConfirmationView.swift @@ -25,8 +25,6 @@ import MailResources import SwiftUI struct LogoutConfirmationView: View { - @Environment(\.window) private var window - let account: Account var body: some View { @@ -48,9 +46,7 @@ struct LogoutConfirmationView: View { } AccountManager.instance.removeTokenAndAccount(token: account.token) if let nextAccount = AccountManager.instance.accounts.first { - (window?.windowScene?.delegate as? SceneDelegate)?.switchAccount(nextAccount) - } else { - (window?.windowScene?.delegate as? SceneDelegate)?.showLoginView() + AccountManager.instance.switchAccount(newAccount: nextAccount) } AccountManager.instance.saveAccounts() } diff --git a/Mail/Views/Bottom sheets/Actions/ActionsPanelViewModifier.swift b/Mail/Views/Bottom sheets/Actions/ActionsPanelViewModifier.swift index ea02a4217..c51e7f28b 100644 --- a/Mail/Views/Bottom sheets/Actions/ActionsPanelViewModifier.swift +++ b/Mail/Views/Bottom sheets/Actions/ActionsPanelViewModifier.swift @@ -28,7 +28,7 @@ extension View { struct ActionsPanelViewModifier: ViewModifier { @EnvironmentObject private var mailboxManager: MailboxManager - @EnvironmentObject private var navigationStore: NavigationStore + @EnvironmentObject private var navigationState: NavigationState @State private var moveAction: MoveAction? @State private var reportJunkActionsTarget: ActionsTarget? @@ -44,7 +44,7 @@ struct ActionsPanelViewModifier: ViewModifier { ActionsView(mailboxManager: mailboxManager, target: target, moveAction: $moveAction, - messageReply: $navigationStore.messageReply, + messageReply: $navigationState.messageReply, reportJunkActionsTarget: $reportJunkActionsTarget, reportedForDisplayProblemMessage: $reportedForDisplayProblemMessage) { completionHandler?() diff --git a/Mail/Views/Menu Drawer/FolderListView.swift b/Mail/Views/Menu Drawer/FolderListView.swift index 46ab4c30b..814c1136e 100644 --- a/Mail/Views/Menu Drawer/FolderListView.swift +++ b/Mail/Views/Menu Drawer/FolderListView.swift @@ -54,20 +54,20 @@ class FolderListViewModel: ObservableObject { private var foldersObservationToken: NotificationToken? - private let userFoldersSortDescriptors = [ - SortDescriptor(keyPath: \Folder.isFavorite, ascending: false), - SortDescriptor(keyPath: \Folder.unreadCount, ascending: false), - SortDescriptor(keyPath: \Folder.name) - ] - init(mailboxManager: MailboxManager) { + updateFolderListForMailboxManager(mailboxManager, animateInitialChanges: false) + } + + func updateFolderListForMailboxManager(_ mailboxManager: MailboxManager, animateInitialChanges: Bool) { // swiftlint:disable empty_count foldersObservationToken = mailboxManager.getRealm() .objects(Folder.self).where { $0.parents.count == 0 && $0.toolType == nil } .observe(on: DispatchQueue.main) { [weak self] results in switch results { case .initial(let folders): - self?.handleFoldersUpdate(folders) + withAnimation(animateInitialChanges ? .default : nil) { + self?.handleFoldersUpdate(folders) + } case .update(let folders, _, _, _): withAnimation { self?.handleFoldersUpdate(folders) @@ -87,15 +87,23 @@ class FolderListViewModel: ObservableObject { struct FolderListView: View { @StateObject private var viewModel: FolderListViewModel + private let mailboxManager: MailboxManager + init(mailboxManager: MailboxManager) { + self.mailboxManager = mailboxManager _viewModel = StateObject(wrappedValue: FolderListViewModel(mailboxManager: mailboxManager)) } var body: some View { - RoleFoldersListView(folders: viewModel.roleFolders) + Group { + RoleFoldersListView(folders: viewModel.roleFolders) - IKDivider(hasVerticalPadding: true, horizontalPadding: UIConstants.menuDrawerHorizontalPadding) + IKDivider(hasVerticalPadding: true, horizontalPadding: UIConstants.menuDrawerHorizontalPadding) - UserFoldersListView(folders: viewModel.userFolders) + UserFoldersListView(folders: viewModel.userFolders) + } + .onChange(of: mailboxManager) { newMailboxManager in + viewModel.updateFolderListForMailboxManager(newMailboxManager, animateInitialChanges: true) + } } } diff --git a/Mail/Views/Menu Drawer/MailboxManagement/MailboxCell.swift b/Mail/Views/Menu Drawer/MailboxManagement/MailboxCell.swift index 59c9c9dd7..fa5f7b526 100644 --- a/Mail/Views/Menu Drawer/MailboxManagement/MailboxCell.swift +++ b/Mail/Views/Menu Drawer/MailboxManagement/MailboxCell.swift @@ -42,7 +42,7 @@ extension View { struct MailboxCell: View { @Environment(\.mailboxCellStyle) private var style: Style - @Environment(\.window) private var window + @EnvironmentObject private var navigationDrawerState: NavigationDrawerState @State private var isShowingLockedView = false @State private var isShowingUpdatePasswordView = false @@ -77,7 +77,8 @@ struct MailboxCell: View { case .account: matomo.track(eventWithCategory: .account, name: "switchMailbox") } - (window?.windowScene?.delegate as? SceneDelegate)?.switchMailbox(mailbox) + AccountManager.instance.switchMailbox(newMailbox: mailbox) + navigationDrawerState.close() } .floatingPanel(isPresented: $isShowingLockedView) { LockedMailboxView(lockedMailbox: mailbox) diff --git a/Mail/Views/Menu Drawer/MailboxManagement/MailboxesManagementView.swift b/Mail/Views/Menu Drawer/MailboxManagement/MailboxesManagementView.swift index 572192785..bec3d96b3 100644 --- a/Mail/Views/Menu Drawer/MailboxManagement/MailboxesManagementView.swift +++ b/Mail/Views/Menu Drawer/MailboxManagement/MailboxesManagementView.swift @@ -82,6 +82,11 @@ struct MailboxesManagementView: View { } } } + .onChange(of: mailboxManager.mailbox.id) { _ in + withAnimation { + navigationDrawerState.showMailboxes = false + } + } } private func updateAccount() async throws { diff --git a/Mail/Views/Menu Drawer/MenuDrawerView.swift b/Mail/Views/Menu Drawer/MenuDrawerView.swift index d75c9dd55..4f19a9d0e 100644 --- a/Mail/Views/Menu Drawer/MenuDrawerView.swift +++ b/Mail/Views/Menu Drawer/MenuDrawerView.swift @@ -46,8 +46,6 @@ struct NavigationDrawer: View { private let maxWidth = 350.0 private let spacing = 60.0 - @Environment(\.window) private var window - @EnvironmentObject private var mailboxManager: MailboxManager @EnvironmentObject private var splitViewManager: SplitViewManager @EnvironmentObject private var navigationDrawerState: NavigationDrawerState @@ -58,7 +56,49 @@ struct NavigationDrawer: View { @LazyInjectService private var matomo: MatomoUtils - private var dragGesture: some Gesture { + var body: some View { + GeometryReader { rootViewSizeProxy in + ZStack { + Color.black + .opacity(navigationDrawerState.isOpen ? 0.5 : 0) + .ignoresSafeArea() + .onTapGesture { + matomo.track(eventWithCategory: .menuDrawer, name: "closeByTap") + navigationDrawerState.close() + } + + GeometryReader { geometryProxy in + HStack { + MenuDrawerView() + .frame(maxWidth: maxWidth) + .padding(.trailing, spacing) + .offset(x: navigationDrawerState.isOpen ? offsetWidth : -geometryProxy.size.width) + Spacer() + } + } + } + .accessibilityAction(.escape) { + matomo.track(eventWithCategory: .menuDrawer, name: "closeByAccessibility") + navigationDrawerState.close() + } + .gesture(dragGestureForRootViewSize(rootViewSizeProxy.size)) + .statusBarHidden(navigationDrawerState.isOpen) + .onChange(of: navigationDrawerState.isOpen) { isOpen in + if !isOpen { + offsetWidth = 0 + } + } + .onChange(of: isDragGestureActive) { newIsDragGestureActive in + if !newIsDragGestureActive && navigationDrawerState.isOpen { + withAnimation { + offsetWidth = 0 + } + } + } + } + } + + func dragGestureForRootViewSize(_ size: CGSize) -> some Gesture { DragGesture() .updating($isDragGestureActive) { _, active, _ in active = true @@ -69,8 +109,7 @@ struct NavigationDrawer: View { } } .onEnded { value in - let windowWidth = window?.frame.size.width ?? 0 - if navigationDrawerState.isOpen && value.translation.width < -(windowWidth / 2) { + if navigationDrawerState.isOpen && value.translation.width < -(size.width / 2) { matomo.track(eventWithCategory: .menuDrawer, name: "closeByGesture") navigationDrawerState.close() } else { @@ -81,46 +120,6 @@ struct NavigationDrawer: View { } } } - - var body: some View { - ZStack { - Color.black - .opacity(navigationDrawerState.isOpen ? 0.5 : 0) - .ignoresSafeArea() - .onTapGesture { - matomo.track(eventWithCategory: .menuDrawer, name: "closeByTap") - navigationDrawerState.close() - } - - GeometryReader { geometryProxy in - HStack { - MenuDrawerView() - .frame(maxWidth: maxWidth) - .padding(.trailing, spacing) - .offset(x: navigationDrawerState.isOpen ? offsetWidth : -geometryProxy.size.width) - Spacer() - } - } - } - .accessibilityAction(.escape) { - matomo.track(eventWithCategory: .menuDrawer, name: "closeByAccessibility") - navigationDrawerState.close() - } - .gesture(dragGesture) - .statusBarHidden(navigationDrawerState.isOpen) - .onChange(of: navigationDrawerState.isOpen) { isOpen in - if !isOpen { - offsetWidth = 0 - } - } - .onChange(of: isDragGestureActive) { newIsDragGestureActive in - if !newIsDragGestureActive && navigationDrawerState.isOpen { - withAnimation { - offsetWidth = 0 - } - } - } - } } struct MenuDrawerView: View { @@ -138,33 +137,30 @@ struct MenuDrawerView: View { ScrollView { VStack(spacing: 0) { - Group { - MailboxesManagementView() - - IKDivider(hasVerticalPadding: true, horizontalPadding: UIConstants.menuDrawerHorizontalPadding) + MailboxesManagementView() - FolderListView(mailboxManager: mailboxManager) + IKDivider(hasVerticalPadding: true, horizontalPadding: UIConstants.menuDrawerHorizontalPadding) - IKDivider(hasVerticalPadding: true, horizontalPadding: UIConstants.menuDrawerHorizontalPadding) - } - Group { - MenuDrawerItemsAdvancedListView( - mailboxCanRestoreEmails: mailboxManager.mailbox.permissions?.canRestoreEmails == true - ) + FolderListView(mailboxManager: mailboxManager) - IKDivider(hasVerticalPadding: true, horizontalPadding: UIConstants.menuDrawerHorizontalPadding) + IKDivider(hasVerticalPadding: true, horizontalPadding: UIConstants.menuDrawerHorizontalPadding) - MenuDrawerItemsHelpListView() - if mailboxManager.mailbox.isLimited, let quotas = mailboxManager.mailbox.quotas { - IKDivider(hasVerticalPadding: true, horizontalPadding: UIConstants.menuDrawerHorizontalPadding) + MenuDrawerItemsAdvancedListView( + mailboxCanRestoreEmails: mailboxManager.mailbox.permissions?.canRestoreEmails == true + ) - MailboxQuotaView(quotas: quotas) - } + IKDivider(hasVerticalPadding: true, horizontalPadding: UIConstants.menuDrawerHorizontalPadding) + MenuDrawerItemsHelpListView() + if mailboxManager.mailbox.isLimited, let quotas = mailboxManager.mailbox.quotas { IKDivider(hasVerticalPadding: true, horizontalPadding: UIConstants.menuDrawerHorizontalPadding) - AppVersionView() + MailboxQuotaView(quotas: quotas) } + + IKDivider(hasVerticalPadding: true, horizontalPadding: UIConstants.menuDrawerHorizontalPadding) + + AppVersionView() } .padding(.vertical, 16) } diff --git a/Mail/Views/New Message/Recipients/RecipientChip.swift b/Mail/Views/New Message/Recipients/RecipientChip.swift index a42d2c2f9..2d0b1f5bc 100644 --- a/Mail/Views/New Message/Recipients/RecipientChip.swift +++ b/Mail/Views/New Message/Recipients/RecipientChip.swift @@ -23,7 +23,6 @@ import Popovers import SwiftUI struct RecipientChip: View { - @Environment(\.window) private var window @AppStorage(UserDefaults.shared.key(.accentColor)) private var accentColor = DefaultPreferences.accentColor let recipient: Recipient @@ -41,7 +40,7 @@ struct RecipientChip: View { RecipientCell(recipient: recipient) .padding(.vertical, 8) .padding(.horizontal, 16) - .frame(maxWidth: min(304, 0.8 * (window?.screen.bounds.width ?? 304))) + .frame(maxWidth: 600) Templates.MenuButton(text: Text(MailResourcesStrings.Localizable.contactActionCopyEmailAddress), image: MailResourcesAsset.duplicate.swiftUIImage) { diff --git a/Mail/Views/NoMailboxView.swift b/Mail/Views/NoMailboxView.swift index 1e18c60d7..54b806c8d 100644 --- a/Mail/Views/NoMailboxView.swift +++ b/Mail/Views/NoMailboxView.swift @@ -21,10 +21,8 @@ import MailResources import SwiftUI struct NoMailboxView: View { - @Environment(\.window) var window - @State private var isShowingAddMailboxView = false - + @State private var isShowingLoginView = false let slide = Slide( id: 1, backgroundImage: MailResourcesAsset.onboardingBackground3.swiftUIImage, @@ -51,22 +49,21 @@ struct NoMailboxView: View { .mailButtonFullWidth(true) MailButton(label: MailResourcesStrings.Localizable.buttonLogInDifferentAccount) { - (window?.windowScene?.delegate as? SceneDelegate)?.showLoginView() + isShowingLoginView = true } .mailButtonStyle(.link) } .frame(height: UIConstants.onboardingButtonHeight + UIConstants.onboardingBottomButtonPadding, alignment: .top) .padding(.horizontal, 24) } + .matomoView(view: ["NoMailboxView"]) .sheet(isPresented: $isShowingAddMailboxView) { - AddMailboxView { _ in - DispatchQueue.main.async { - (window?.windowScene?.delegate as? SceneDelegate)?.showMainView() - } - } - .sheetViewStyle() + AddMailboxView() + .sheetViewStyle() + } + .fullScreenCover(isPresented: $isShowingLoginView) { + OnboardingView(page: 4, isScrollEnabled: false) } - .matomoView(view: ["NoMailboxView"]) } } diff --git a/Mail/Views/Onboarding/OnboardingView.swift b/Mail/Views/Onboarding/OnboardingView.swift index 405f75975..2c460c373 100644 --- a/Mail/Views/Onboarding/OnboardingView.swift +++ b/Mail/Views/Onboarding/OnboardingView.swift @@ -74,7 +74,7 @@ class LoginHandler: InfomaniakLoginDelegate, ObservableObject { @Published var isLoading = false @Published var isPresentingErrorAlert = false - var sceneDelegate: SceneDelegate? + @Published var shouldShowEmptyMailboxesView = false nonisolated func didCompleteLoginWith(code: String, verifier: String) { Task { @@ -127,10 +127,9 @@ class LoginHandler: InfomaniakLoginDelegate, ObservableObject { Task { do { _ = try await AccountManager.instance.createAndSetCurrentAccount(code: code, codeVerifier: verifier) - sceneDelegate?.showMainView() UIApplication.shared.registerForRemoteNotifications() } catch let error as MailError where error == MailError.noMailbox { - sceneDelegate?.showNoMailboxView() + shouldShowEmptyMailboxesView = true } catch { if let previousAccount = previousAccount { AccountManager.instance.switchAccount(newAccount: previousAccount) @@ -149,9 +148,10 @@ class LoginHandler: InfomaniakLoginDelegate, ObservableObject { } struct OnboardingView: View { - @Environment(\.window) private var window @Environment(\.dismiss) private var dismiss + @EnvironmentObject private var navigationState: NavigationState + @AppStorage(UserDefaults.shared.key(.accentColor)) private var accentColor = DefaultPreferences.accentColor @State private var selection: Int @@ -170,7 +170,8 @@ struct OnboardingView: View { var body: some View { VStack(spacing: 0) { Group { - if !isScrollEnabled, let slide = slides.first { $0.id == selection } { + if !isScrollEnabled, + let slide = slides.first(where: { $0.id == selection }) { SlideView(slide: slide, updateAnimationColors: updateAnimationColors) } else { TabView(selection: $selection) { @@ -194,7 +195,6 @@ struct OnboardingView: View { VStack(spacing: 24) { if selection == slides.count { MailButton(label: MailResourcesStrings.Localizable.buttonLogin) { - loginHandler.sceneDelegate = window?.windowScene?.delegate as? SceneDelegate loginHandler.login() } .mailButtonFullWidth(true) @@ -238,7 +238,6 @@ struct OnboardingView: View { .sheet(isPresented: $isPresentingCreateAccount) { RegisterView(registrationProcess: .mail) { viewController in guard let viewController else { return } - loginHandler.sceneDelegate = window?.windowScene?.delegate as? SceneDelegate loginHandler.loginAfterAccountCreation(from: viewController) } } @@ -250,7 +249,11 @@ struct OnboardingView: View { UIViewController.attemptRotationToDeviceOrientation() } } - .defaultAppStorage(.shared) + .onChange(of: loginHandler.shouldShowEmptyMailboxesView) { shouldShowEmptyMailboxesView in + if shouldShowEmptyMailboxesView { + navigationState.transitionToRootViewDestination(.noMailboxes) + } + } } // MARK: - Private methods diff --git a/Mail/Views/Onboarding/SlideView.swift b/Mail/Views/Onboarding/SlideView.swift index b7a67bf59..9af27c6ea 100644 --- a/Mail/Views/Onboarding/SlideView.swift +++ b/Mail/Views/Onboarding/SlideView.swift @@ -33,7 +33,6 @@ struct SlideView: View { @AppStorage(UserDefaults.shared.key(.accentColor)) private var accentColor = DefaultPreferences.accentColor - @Environment(\.window) private var window @Environment(\.colorScheme) private var colorScheme @State private var isVisible = false @@ -97,9 +96,6 @@ struct SlideView: View { .multilineTextAlignment(.center) .padding(.horizontal, 32) } - .onChange(of: accentColor) { _ in - (window?.windowScene?.delegate as? SceneDelegate)?.updateWindowUI() - } .onAppear { isVisible = true } diff --git a/Mail/Views/Search/SearchThreadsSectionView.swift b/Mail/Views/Search/SearchThreadsSectionView.swift index 688788f4e..30141c7ec 100644 --- a/Mail/Views/Search/SearchThreadsSectionView.swift +++ b/Mail/Views/Search/SearchThreadsSectionView.swift @@ -21,46 +21,29 @@ import MailResources import SwiftUI struct SearchThreadsSectionView: View { + @EnvironmentObject private var navigationState: NavigationState + @EnvironmentObject private var splitViewManager: SplitViewManager + @AppStorage(UserDefaults.shared.key(.threadDensity)) private var threadDensity = DefaultPreferences.threadDensity let viewModel: SearchViewModel - @Binding var editedMessageDraft: Draft? var body: some View { Section { ForEach(viewModel.threads) { thread in - Group { - if thread.shouldPresentAsDraft { - Button(action: { - DraftUtils.editDraft( - from: thread, - mailboxManager: viewModel.mailboxManager, - editedMessageDraft: $editedMessageDraft - ) - }, label: { - ThreadCell(thread: thread, density: threadDensity) - }) - } else { - ZStack { - NavigationLink(destination: { - ThreadView(thread: thread) - .onAppear { - viewModel.selectedThread = thread - } - }, label: { - EmptyView() - }) - .opacity(0) - - ThreadCell(thread: thread, density: threadDensity) - } + ThreadCell(thread: thread, density: threadDensity) + .onTapGesture { + didTapCell(thread: thread) + } + .background(SelectionBackground( + selectionType: viewModel.selectedThread == thread ? .single : .none, + paddingLeading: 4, + withAnimation: false + )) + .threadListCellAppearance() + .onAppear { + viewModel.loadNextPageIfNeeded(currentItem: thread) } - } - .listRowInsets(EdgeInsets()) - .padding(.leading, -4) - .onAppear { - viewModel.loadNextPageIfNeeded(currentItem: thread) - } } } header: { if threadDensity != .compact && !viewModel.threads.isEmpty { @@ -72,10 +55,24 @@ struct SearchThreadsSectionView: View { ProgressView() .frame(maxWidth: .infinity) .id(UUID()) + .threadListCellAppearance() } } - .listRowInsets(.init(top: 0, leading: 12, bottom: 0, trailing: 12)) - .listRowSeparator(.hidden) - .listRowBackground(MailResourcesAsset.backgroundColor.swiftUIColor) + } + + private func didTapCell(thread: Thread) { + if thread.shouldPresentAsDraft { + DraftUtils.editDraft( + from: thread, + mailboxManager: viewModel.mailboxManager, + editedMessageDraft: $navigationState.editedMessageDraft + ) + } else { + splitViewManager.adaptToProminentThreadView() + + // Update both viewModel and navigationState on the truth. + viewModel.selectedThread = thread + navigationState.threadPath = [thread] + } } } diff --git a/Mail/Views/Search/SearchView.swift b/Mail/Views/Search/SearchView.swift index a70a37d9e..cbc2632db 100644 --- a/Mail/Views/Search/SearchView.swift +++ b/Mail/Views/Search/SearchView.swift @@ -29,10 +29,7 @@ struct SearchView: View { @StateObject private var viewModel: SearchViewModel - @Binding private var editedMessageDraft: Draft? - - init(mailboxManager: MailboxManager, folder: Folder, editedMessageDraft: Binding) { - _editedMessageDraft = editedMessageDraft + init(mailboxManager: MailboxManager, folder: Folder) { _viewModel = StateObject(wrappedValue: SearchViewModel(mailboxManager: mailboxManager, folder: folder)) } @@ -71,7 +68,7 @@ struct SearchView: View { SearchHistorySectionView(viewModel: viewModel) } else if viewModel.searchState == .results { SearchContactsSectionView(viewModel: viewModel) - SearchThreadsSectionView(viewModel: viewModel, editedMessageDraft: $editedMessageDraft) + SearchThreadsSectionView(viewModel: viewModel) } } .listStyle(.plain) @@ -121,8 +118,6 @@ struct SearchView: View { struct SearchView_Previews: PreviewProvider { static var previews: some View { - SearchView(mailboxManager: PreviewHelper.sampleMailboxManager, - folder: PreviewHelper.sampleFolder, - editedMessageDraft: .constant(nil)) + SearchView(mailboxManager: PreviewHelper.sampleMailboxManager, folder: PreviewHelper.sampleFolder) } } diff --git a/Mail/Views/Settings/SettingsOptionView.swift b/Mail/Views/Settings/SettingsOptionView.swift index 3d5ab80f3..5dfab17f2 100644 --- a/Mail/Views/Settings/SettingsOptionView.swift +++ b/Mail/Views/Settings/SettingsOptionView.swift @@ -38,9 +38,13 @@ struct SettingsOptionView: View where OptionEnum: CaseIterable, Opti @State private var selectedValue: OptionEnum { didSet { UserDefaults.shared[keyPath: keyPath] = selectedValue + + // AppStorage updates the views only if directly called switch keyPath { - case \.theme, \.accentColor: - UIApplication.shared.connectedScenes.forEach { ($0.delegate as? SceneDelegate)?.updateWindowUI() } + case \.accentColor: + AppStorage(UserDefaults.shared.key(.accentColor)).wrappedValue = UserDefaults.shared.accentColor + case \.theme: + AppStorage(UserDefaults.shared.key(.theme)).wrappedValue = UserDefaults.shared.theme default: break } diff --git a/Mail/Views/Settings/SettingsToggleCell.swift b/Mail/Views/Settings/SettingsToggleCell.swift index 68eedacdb..7dfc570f4 100644 --- a/Mail/Views/Settings/SettingsToggleCell.swift +++ b/Mail/Views/Settings/SettingsToggleCell.swift @@ -36,6 +36,14 @@ struct SettingsToggleCell: View { @State private var toggleIsOn: Bool { didSet { UserDefaults.shared[keyPath: userDefaults] = toggleIsOn + + // AppStorage updates the views only if directly called + if userDefaults == \.isAppLockEnabled { + AppStorage(UserDefaults.shared.key(.appLock)).wrappedValue = UserDefaults.shared.isAppLockEnabled + if UserDefaults.shared.isAppLockEnabled { + appLockHelper.setTime() + } + } } } diff --git a/Mail/Views/SheetViewModifier.swift b/Mail/Views/SheetViewModifier.swift index c538c6fde..0e0735597 100644 --- a/Mail/Views/SheetViewModifier.swift +++ b/Mail/Views/SheetViewModifier.swift @@ -49,9 +49,6 @@ struct SheetViewModifier: ViewModifier { func body(content: Content) -> some View { NavigationView { content - .environment(\.dismissModal) { - dismiss() - } .toolbar { ToolbarItem(placement: .cancellationAction) { Button { @@ -63,5 +60,8 @@ struct SheetViewModifier: ViewModifier { } } .navigationViewStyle(.stack) + .environment(\.dismissModal) { + dismiss() + } } } diff --git a/Mail/Views/SplitView.swift b/Mail/Views/SplitView.swift index a4e5bcf97..5b1b971d3 100644 --- a/Mail/Views/SplitView.swift +++ b/Mail/Views/SplitView.swift @@ -31,40 +31,33 @@ public class SplitViewManager: ObservableObject { @Published var selectedFolder: Folder? var splitViewController: UISplitViewController? - init(folder: Folder?) { - selectedFolder = folder + func adaptToProminentThreadView() { + splitViewController?.hide(.primary) + if splitViewController?.splitBehavior == .overlay { + splitViewController?.hide(.supplementary) + } } } struct SplitView: View { - @Environment(\.horizontalSizeClass) var horizontalSizeClass - @Environment(\.verticalSizeClass) var verticalSizeClass - @Environment(\.window) var window + @Environment(\.isCompactWindow) private var isCompactWindow + @Environment(\.scenePhase) private var scenePhase + + @EnvironmentObject private var navigationState: NavigationState @State private var splitViewController: UISplitViewController? @State private var mailToURLComponents: IdentifiableURLComponents? @StateObject private var navigationDrawerController = NavigationDrawerState() - @StateObject private var navigationStore = NavigationStore() - @StateObject private var splitViewManager: SplitViewManager + @StateObject private var splitViewManager = SplitViewManager() let mailboxManager: MailboxManager - private var isCompact: Bool { - UIConstants.isCompact(horizontalSizeClass: horizontalSizeClass, verticalSizeClass: verticalSizeClass) - } - - init(mailboxManager: MailboxManager) { - self.mailboxManager = mailboxManager - _splitViewManager = - StateObject(wrappedValue: SplitViewManager(folder: mailboxManager.getFolder(with: .inbox))) - } - var body: some View { Group { - if isCompact { + if isCompactWindow { ZStack { - NBNavigationStack(path: $navigationStore.threadPath) { + NBNavigationStack(path: $navigationState.threadPath) { ThreadListManagerView() .accessibilityHidden(navigationDrawerController.isOpen) .nbNavigationDestination(for: Thread.self) { thread in @@ -83,7 +76,7 @@ struct SplitView: View { ThreadListManagerView() - if let thread = navigationStore.threadPath.last { + if let thread = navigationState.threadPath.last { ThreadView(thread: thread) } else { EmptyStateView.emptyThread(from: splitViewManager.selectedFolder) @@ -91,17 +84,24 @@ struct SplitView: View { } } } - .sheet(item: $navigationStore.messageReply) { messageReply in + .sheet(item: $navigationState.messageReply) { messageReply in ComposeMessageView.replyOrForwardMessage(messageReply: messageReply, mailboxManager: mailboxManager) } .sheet(item: $mailToURLComponents) { identifiableURLComponents in ComposeMessageView.mailTo(urlComponents: identifiableURLComponents.urlComponents, mailboxManager: mailboxManager) } - .onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in + .sheet(item: $navigationState.editedMessageDraft) { editedMessageDraft in + ComposeMessageView.edit(draft: editedMessageDraft, mailboxManager: mailboxManager) + } + .onChange(of: scenePhase) { newScenePhase in + guard newScenePhase == .active else { return } Task { try await mailboxManager.folders() } } + .onOpenURL { url in + handleOpenUrl(url) + } .onReceive(NotificationCenter.default.publisher(for: .onUserTappedNotification)) { notification in guard let notificationPayload = notification.object as? NotificationTappedPayload else { return } let realm = mailboxManager.getRealm() @@ -110,7 +110,7 @@ struct SplitView: View { let tappedNotificationMessage = realm.object(ofType: Message.self, forPrimaryKey: notificationPayload.messageId) // Original parent should always be in the inbox but maybe change in a later stage to always find the parent in inbox if let tappedNotificationThread = tappedNotificationMessage?.originalThread { - navigationStore.threadPath = [tappedNotificationThread] + navigationState.threadPath = [tappedNotificationThread] } else { IKSnackBar.showSnackBar(message: MailError.localMessageNotFound.errorDescription) } @@ -121,34 +121,28 @@ struct SplitView: View { .onAppear { AppDelegate.orientationLock = .all } - .task { + .task(id: mailboxManager.mailbox.objectId) { await fetchSignatures() } - .task { + .task(id: mailboxManager.mailbox.objectId) { await fetchFolders() - // On first launch, select inbox - if splitViewManager.selectedFolder == nil { - splitViewManager.selectedFolder = getInbox() - } + splitViewManager.selectedFolder = getInbox() } .onRotate { orientation in guard let interfaceOrientation = orientation else { return } setupBehaviour(orientation: interfaceOrientation) } .introspectSplitViewController { splitViewController in - guard let interfaceOrientation = window?.windowScene?.interfaceOrientation, + guard let interfaceOrientation = splitViewController.view.window?.windowScene?.interfaceOrientation, self.splitViewController != splitViewController else { return } self.splitViewController = splitViewController splitViewManager.splitViewController = splitViewController setupBehaviour(orientation: interfaceOrientation) } - .environment(\.realmConfiguration, mailboxManager.realmConfiguration) - .environment(\.isCompactWindow, horizontalSizeClass == .compact || verticalSizeClass == .compact) - .environmentObject(mailboxManager) .environmentObject(splitViewManager) .environmentObject(navigationDrawerController) - .environmentObject(navigationStore) - .defaultAppStorage(.shared) + .environmentObject(mailboxManager) + .environment(\.realmConfiguration, mailboxManager.realmConfiguration) } private func setupBehaviour(orientation: UIInterfaceOrientation) { @@ -183,4 +177,12 @@ struct SplitView: View { private func getInbox() -> Folder? { return mailboxManager.getFolder(with: .inbox) } + + private func handleOpenUrl(_ url: URL) { + guard let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: true) else { return } + + if Constants.isMailTo(url) { + mailToURLComponents = IdentifiableURLComponents(urlComponents: urlComponents) + } + } } diff --git a/Mail/Views/Switch User/AccountCellView.swift b/Mail/Views/Switch User/AccountCellView.swift index 5aaf82c64..6ef9e0dfe 100644 --- a/Mail/Views/Switch User/AccountCellView.swift +++ b/Mail/Views/Switch User/AccountCellView.swift @@ -26,7 +26,7 @@ import RealmSwift import SwiftUI struct AccountCellView: View { - @Environment(\.window) private var window + @Environment(\.dismissModal) var dismissModal let account: Account @Binding var selectedUserId: Int? @@ -49,10 +49,8 @@ struct AccountCellView: View { @InjectService var matomo: MatomoUtils matomo.track(eventWithCategory: .account, name: "switch") - withAnimation { - selectedUserId = selectedUserId == account.userId ? nil : account.userId - (window?.windowScene?.delegate as? SceneDelegate)?.switchAccount(account) - } + dismissModal() + AccountManager.instance.switchAccount(newAccount: account) } label: { AccountHeaderCell(account: account, isSelected: Binding(get: { isSelected diff --git a/Mail/Views/Switch User/AccountView.swift b/Mail/Views/Switch User/AccountView.swift index c52abba01..e885de801 100644 --- a/Mail/Views/Switch User/AccountView.swift +++ b/Mail/Views/Switch User/AccountView.swift @@ -28,13 +28,10 @@ import SwiftUI 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) if let nextAccount = AccountManager.instance.accounts.first { - (window?.windowScene?.delegate as? SceneDelegate)?.switchAccount(nextAccount) + AccountManager.instance.switchAccount(newAccount: nextAccount) IKSnackBar.showSnackBar(message: "Account deleted") - } else { - (window?.windowScene?.delegate as? SceneDelegate)?.showLoginView() } AccountManager.instance.saveAccounts() } @@ -49,80 +46,75 @@ struct AccountView: View { @Environment(\.dismiss) private var dismiss @EnvironmentObject private var mailboxManager: MailboxManager + @AppStorage(UserDefaults.shared.key(.accentColor)) private var accentColor = DefaultPreferences.accentColor @LazyInjectService private var matomo: MatomoUtils - private let account = AccountManager.instance.currentAccount! @State private var isShowingLogoutAlert = false @State private var isShowingDeleteAccount = false @State private var delegate = AccountViewDelegate() + let account: Account + var body: some View { - NavigationView { - VStack(spacing: 0) { - ScrollView { - AvatarView(avatarDisplayable: account.user, size: 104) - .padding(.top, 24) + VStack(spacing: 0) { + ScrollView { + AvatarView(avatarDisplayable: account.user, size: 104) + .padding(.top, 24) + .padding(.bottom, 16) + + VStack(spacing: 0) { + Text(account.user.displayName) + .textStyle(.header2) + .padding(.bottom, 4) + + Text(account.user.email) + .textStyle(.bodySmallSecondary) .padding(.bottom, 16) - VStack(spacing: 0) { - Text(account.user.displayName) - .textStyle(.header2) - .padding(.bottom, 4) - - Text(account.user.email) - .textStyle(.bodySmallSecondary) - .padding(.bottom, 16) - - NavigationLink { - AccountListView() - } label: { - Text(MailResourcesStrings.Localizable.buttonAccountSwitch) - .textStyle(.bodyMediumAccent) - } + NavigationLink { + AccountListView() + } label: { + Text(MailResourcesStrings.Localizable.buttonAccountSwitch) + .textStyle(.bodyMediumAccent) } + } - MailboxListView(currentMailbox: mailboxManager.mailbox) + MailboxListView(currentMailbox: mailboxManager.mailbox) - Spacer() - } + Spacer() + } - // Buttons - MailButton(label: MailResourcesStrings.Localizable.buttonAccountDisconnect) { - matomo.track(eventWithCategory: .account, name: "logOut") - isShowingLogoutAlert.toggle() - } - .mailButtonFullWidth(true) - .padding(.bottom, 24) - MailButton(label: MailResourcesStrings.Localizable.buttonAccountDelete) { - matomo.track(eventWithCategory: .account, name: "deleteAccount") - isShowingDeleteAccount.toggle() - } - .mailButtonStyle(.destructive) - .padding(.bottom, 24) + // Buttons + MailButton(label: MailResourcesStrings.Localizable.buttonAccountDisconnect) { + matomo.track(eventWithCategory: .account, name: "logOut") + isShowingLogoutAlert.toggle() + } + .mailButtonFullWidth(true) + .padding(.bottom, 24) + MailButton(label: MailResourcesStrings.Localizable.buttonAccountDelete) { + matomo.track(eventWithCategory: .account, name: "deleteAccount") + isShowingDeleteAccount.toggle() } - .padding(.horizontal, 16) - .navigationBarTitle(MailResourcesStrings.Localizable.titleMyAccount, displayMode: .inline) - .backButtonDisplayMode(.minimal) - .navigationBarItems(leading: Button { - dismiss() - } label: { - Label(MailResourcesStrings.Localizable.buttonClose, systemImage: "xmark") - }) + .mailButtonStyle(.destructive) + .padding(.bottom, 24) } + .padding(.horizontal, 16) + .navigationBarTitle(MailResourcesStrings.Localizable.titleMyAccount, displayMode: .inline) + .backButtonDisplayMode(.minimal) .sheet(isPresented: $isShowingDeleteAccount) { DeleteAccountView(account: account, delegate: delegate) } .customAlert(isPresented: $isShowingLogoutAlert) { LogoutConfirmationView(account: account) } - .defaultAppStorage(.shared) + .sheetViewStyle() .matomoView(view: [MatomoUtils.View.accountView.displayName, "Main"]) } } struct AccountView_Previews: PreviewProvider { static var previews: some View { - AccountView() + AccountView(account: PreviewHelper.sampleAccount) } } diff --git a/Mail/Views/Switch User/AddMailboxView.swift b/Mail/Views/Switch User/AddMailboxView.swift index dcdf65048..086799896 100644 --- a/Mail/Views/Switch User/AddMailboxView.swift +++ b/Mail/Views/Switch User/AddMailboxView.swift @@ -25,8 +25,6 @@ import SwiftUI struct AddMailboxView: View { @Environment(\.dismiss) var dismiss - var completion: (Mailbox?) -> Void - @State private var newAddress = "" @State private var password = "" @State private var showError = false @@ -92,11 +90,7 @@ struct AddMailboxView: View { private func addMailbox() { Task { do { - try await AccountManager.instance.addMailbox(mail: newAddress, password: password) { mailbox in - @InjectService var matomo: MatomoUtils - matomo.track(eventWithCategory: .account, name: "addMailboxConfirm") - completion(mailbox) - } + try await AccountManager.instance.addMailbox(mail: newAddress, password: password) } catch { withAnimation { showError = true @@ -110,6 +104,6 @@ struct AddMailboxView: View { struct AddMailboxView_Previews: PreviewProvider { static var previews: some View { - AddMailboxView { _ in /* Preview */ } + AddMailboxView() } } diff --git a/Mail/Views/Thread List/ThreadListCell.swift b/Mail/Views/Thread List/ThreadListCell.swift index 41351c0d4..3a96d4068 100644 --- a/Mail/Views/Thread List/ThreadListCell.swift +++ b/Mail/Views/Thread List/ThreadListCell.swift @@ -25,13 +25,11 @@ import SwiftUI struct ThreadListCell: View { @EnvironmentObject var splitViewManager: SplitViewManager - @EnvironmentObject var navigationStore: NavigationStore + @EnvironmentObject var navigationState: NavigationState let viewModel: ThreadListViewModel @ObservedObject var multipleSelectionViewModel: ThreadListMultipleSelectionViewModel - @Binding var editedMessageDraft: Draft? - let thread: Thread let threadDensity: ThreadDensity @@ -71,17 +69,14 @@ struct ThreadListCell: View { DraftUtils.editDraft( from: thread, mailboxManager: viewModel.mailboxManager, - editedMessageDraft: $editedMessageDraft + editedMessageDraft: $navigationState.editedMessageDraft ) } else { - splitViewManager.splitViewController?.hide(.primary) - if splitViewManager.splitViewController?.splitBehavior == .overlay { - splitViewManager.splitViewController?.hide(.supplementary) - } + splitViewManager.adaptToProminentThreadView() - // Update both viewModel and navigationStore on the truth. + // Update both viewModel and navigationState on the truth. viewModel.selectedThread = thread - navigationStore.threadPath = [thread] + navigationState.threadPath = [thread] } } } @@ -105,7 +100,6 @@ struct ThreadListCell_Previews: PreviewProvider { folder: PreviewHelper.sampleFolder, isCompact: false), multipleSelectionViewModel: ThreadListMultipleSelectionViewModel(mailboxManager: PreviewHelper.sampleMailboxManager), - editedMessageDraft: .constant(nil), thread: PreviewHelper.sampleThread, threadDensity: .large, isSelected: false, diff --git a/Mail/Views/Thread List/ThreadListModifiers.swift b/Mail/Views/Thread List/ThreadListModifiers.swift index dc3f702cf..4d92ca452 100644 --- a/Mail/Views/Thread List/ThreadListModifiers.swift +++ b/Mail/Views/Thread List/ThreadListModifiers.swift @@ -16,6 +16,7 @@ along with this program. If not, see . */ +import InfomaniakCore import InfomaniakCoreUI import InfomaniakDI import MailCore @@ -58,8 +59,9 @@ struct ThreadListToolbar: ViewModifier { @EnvironmentObject private var splitViewManager: SplitViewManager @EnvironmentObject private var navigationDrawerState: NavigationDrawerState + @EnvironmentObject private var navigationState: NavigationState - @State private var isShowingSwitchAccount = false + @State private var presentedCurrentAccount: Account? @State private var multipleSelectionActionsTarget: ActionsTarget? @Binding var flushAlert: FlushAlertState? @@ -121,11 +123,16 @@ struct ThreadListToolbar: ViewModifier { } Button { - isShowingSwitchAccount.toggle() + presentedCurrentAccount = AccountManager.instance.currentAccount } label: { - AvatarView(avatarDisplayable: AccountManager.instance.currentAccount.user) + if let currentAccountUser = AccountManager.instance.currentAccount?.user { + AvatarView(avatarDisplayable: currentAccountUser) + } } .accessibilityLabel(MailResourcesStrings.Localizable.contentDescriptionUserAvatar) + .sheet(item: $presentedCurrentAccount) { account in + AccountView(account: account) + } } } } @@ -166,8 +173,5 @@ struct ThreadListToolbar: ViewModifier { : "" ) .navigationBarTitleDisplayMode(.inline) - .sheet(isPresented: $isShowingSwitchAccount) { - AccountView() - } } } diff --git a/Mail/Views/Thread List/ThreadListView.swift b/Mail/Views/Thread List/ThreadListView.swift index 6b482d460..89fc58576 100644 --- a/Mail/Views/Thread List/ThreadListView.swift +++ b/Mail/Views/Thread List/ThreadListView.swift @@ -39,7 +39,7 @@ struct ThreadListView: View { @LazyInjectService private var matomo: MatomoUtils @EnvironmentObject var splitViewManager: SplitViewManager - @EnvironmentObject var navigationStore: NavigationStore + @EnvironmentObject var navigationState: NavigationState @AppStorage(UserDefaults.shared.key(.threadDensity)) private var threadDensity = DefaultPreferences.threadDensity @AppStorage(UserDefaults.shared.key(.accentColor)) private var accentColor = DefaultPreferences.accentColor @@ -55,9 +55,6 @@ struct ThreadListView: View { @StateObject var multipleSelectionViewModel: ThreadListMultipleSelectionViewModel @StateObject private var networkMonitor = NetworkMonitor() - @Binding private var editedMessageDraft: Draft? - @Binding private var messageReply: MessageReply? - private var shouldDisplayEmptyView: Bool { viewModel.folder.lastUpdate != nil && viewModel.sections.isEmpty && !viewModel.isLoadingPage } @@ -72,11 +69,7 @@ struct ThreadListView: View { init(mailboxManager: MailboxManager, folder: Folder, - editedMessageDraft: Binding, - messageReply: Binding, isCompact: Bool) { - _editedMessageDraft = editedMessageDraft - _messageReply = messageReply _viewModel = StateObject(wrappedValue: ThreadListViewModel(mailboxManager: mailboxManager, folder: folder, isCompact: isCompact)) @@ -123,7 +116,6 @@ struct ThreadListView: View { ForEach(section.threads) { thread in ThreadListCell(viewModel: viewModel, multipleSelectionViewModel: multipleSelectionViewModel, - editedMessageDraft: $editedMessageDraft, thread: thread, threadDensity: threadDensity, isSelected: viewModel.selectedThread?.uid == thread.uid, @@ -231,9 +223,9 @@ struct ThreadListView: View { } .onChange(of: viewModel.selectedThread) { newThread in if let newThread { - navigationStore.threadPath = [newThread] + navigationState.threadPath = [newThread] } else { - navigationStore.threadPath = [] + navigationState.threadPath = [] } } .onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in @@ -287,8 +279,6 @@ struct ThreadListView_Previews: PreviewProvider { ThreadListView( mailboxManager: PreviewHelper.sampleMailboxManager, folder: PreviewHelper.sampleFolder, - editedMessageDraft: .constant(nil), - messageReply: .constant(nil), isCompact: false ) } diff --git a/Mail/Views/Thread/MessageHeaderSummaryView.swift b/Mail/Views/Thread/MessageHeaderSummaryView.swift index b652537d1..95f2eaa26 100644 --- a/Mail/Views/Thread/MessageHeaderSummaryView.swift +++ b/Mail/Views/Thread/MessageHeaderSummaryView.swift @@ -26,7 +26,7 @@ import SwiftUI struct MessageHeaderSummaryView: View { @EnvironmentObject private var mailboxManager: MailboxManager - @EnvironmentObject private var navigationStore: NavigationStore + @EnvironmentObject private var navigationState: NavigationState @ObservedRealmObject var message: Message @@ -114,7 +114,7 @@ struct MessageHeaderSummaryView: View { if message.canReplyAll { replyOrReplyAllMessage = message } else { - navigationStore.messageReply = MessageReply(message: message, replyMode: .reply) + navigationState.messageReply = MessageReply(message: message, replyMode: .reply) } } label: { @@ -126,7 +126,7 @@ struct MessageHeaderSummaryView: View { .adaptivePanel(item: $replyOrReplyAllMessage) { message in ReplyActionsView(mailboxManager: mailboxManager, message: message, - messageReply: $navigationStore.messageReply) + messageReply: $navigationState.messageReply) } ActionsPanelButton(message: message) { MailResourcesAsset.plusActions.swiftUIImage diff --git a/Mail/Views/Thread/MessageHeaderView.swift b/Mail/Views/Thread/MessageHeaderView.swift index 5f9ae03f5..6006d1da8 100644 --- a/Mail/Views/Thread/MessageHeaderView.swift +++ b/Mail/Views/Thread/MessageHeaderView.swift @@ -27,7 +27,7 @@ import SwiftUI struct MessageHeaderView: View { @LazyInjectService private var matomo: MatomoUtils - @EnvironmentObject private var navigationStore: NavigationStore + @EnvironmentObject private var navigationState: NavigationState @EnvironmentObject private var mailboxManager: MailboxManager @State private var editedDraft: Draft? diff --git a/Mail/Views/Thread/ThreadView.swift b/Mail/Views/Thread/ThreadView.swift index 4e01acd83..b0470f144 100644 --- a/Mail/Views/Thread/ThreadView.swift +++ b/Mail/Views/Thread/ThreadView.swift @@ -40,7 +40,7 @@ struct ThreadView: View { @EnvironmentObject private var splitViewManager: SplitViewManager @EnvironmentObject private var mailboxManager: MailboxManager - @EnvironmentObject private var navigationStore: NavigationStore + @EnvironmentObject private var navigationState: NavigationState @State private var headerHeight: CGFloat = 0 @State private var displayNavigationTitle = false @@ -119,7 +119,7 @@ struct ThreadView: View { ReplyActionsView( mailboxManager: mailboxManager, message: message, - messageReply: $navigationStore.messageReply + messageReply: $navigationState.messageReply ) } } else { @@ -157,11 +157,11 @@ struct ThreadView: View { if message.canReplyAll { replyOrReplyAllMessage = message } else { - navigationStore.messageReply = MessageReply(message: message, replyMode: .reply) + navigationState.messageReply = MessageReply(message: message, replyMode: .reply) } case .forward: guard let message = thread.lastMessageToExecuteAction() else { return } - navigationStore.messageReply = MessageReply(message: message, replyMode: .forward) + navigationState.messageReply = MessageReply(message: message, replyMode: .forward) case .archive: Task { await tryOrDisplayError { diff --git a/Mail/Views/Thread/WebView.swift b/Mail/Views/Thread/WebView.swift index f30c9c728..fcf36592b 100644 --- a/Mail/Views/Thread/WebView.swift +++ b/Mail/Views/Thread/WebView.swift @@ -46,7 +46,8 @@ extension WKWebView { } final class WebViewController: UIViewController { - var model: WebViewModel? + var openURL: OpenURLAction? + var model: WebViewModel? var messageUid: String? private let widthSubject = PassthroughSubject() @@ -124,7 +125,7 @@ extension WebViewController: WKNavigationDelegate { ) { if let url = navigationAction.request.url, Constants.isMailTo(url) { decisionHandler(.cancel) - (view.window?.windowScene?.delegate as? SceneDelegate)?.handleUrlOpen(url) + openURL?(url) return } @@ -140,11 +141,14 @@ extension WebViewController: WKNavigationDelegate { } struct WebView: UIViewControllerRepresentable { + @Environment(\.openURL) private var openUrl + let model: WebViewModel let messageUid: String func makeUIViewController(context: Context) -> WebViewController { let controller = WebViewController() + controller.openURL = openUrl controller.model = model controller.messageUid = messageUid return controller diff --git a/Mail/Views/ThreadListManagerView.swift b/Mail/Views/ThreadListManagerView.swift index 54c18eced..8976a636a 100644 --- a/Mail/Views/ThreadListManagerView.swift +++ b/Mail/Views/ThreadListManagerView.swift @@ -27,41 +27,24 @@ struct ThreadListManagerView: View { @EnvironmentObject private var splitViewManager: SplitViewManager @EnvironmentObject private var mailboxManager: MailboxManager - @State private var shouldNavigateToNotificationThread = false - @State private var tappedNotificationThread: Thread? - @State private var editedMessageDraft: Draft? - @State private var messageReply: MessageReply? - var body: some View { Group { if let selectedFolder = splitViewManager.selectedFolder { if splitViewManager.showSearch { - SearchView( - mailboxManager: mailboxManager, - folder: selectedFolder, - editedMessageDraft: $editedMessageDraft - ) + SearchView(mailboxManager: mailboxManager, folder: selectedFolder) } else { - ThreadListView( - mailboxManager: mailboxManager, - folder: selectedFolder, - editedMessageDraft: $editedMessageDraft, - messageReply: $messageReply, - isCompact: isCompactWindow - ) + ThreadListView(mailboxManager: mailboxManager, folder: selectedFolder, isCompact: isCompactWindow) } } } + .id(mailboxManager.mailbox.id) .animation(.easeInOut(duration: 0.25), value: splitViewManager.showSearch) - .sheet(item: $editedMessageDraft) { draft in - ComposeMessageView.edit(draft: draft, mailboxManager: mailboxManager) - } } } struct ThreadListManagerView_Previews: PreviewProvider { static var previews: some View { ThreadListManagerView() - .environmentObject(PreviewHelper.sampleMailboxManager) + .environmentObject(PreviewHelper.sampleMailboxManager) } } diff --git a/Mail/Views/Unavailable Mailbox/UnavailableMailboxesView.swift b/Mail/Views/Unavailable Mailbox/UnavailableMailboxesView.swift index f1df61b74..46f4d8c74 100644 --- a/Mail/Views/Unavailable Mailbox/UnavailableMailboxesView.swift +++ b/Mail/Views/Unavailable Mailbox/UnavailableMailboxesView.swift @@ -25,10 +25,8 @@ import SwiftUI struct UnavailableMailboxesView: View { @LazyInjectService private var matomo: MatomoUtils - @Environment(\.window) private var window - - @State var isShowingNewAccountView = false - @State private var showAddMailbox = false + @State private var isShowingNewAccountView = false + @State private var isShowingAddMailboxView = false var body: some View { NavigationView { @@ -58,17 +56,12 @@ struct UnavailableMailboxesView: View { } Spacer() - NavigationLink(isActive: $showAddMailbox) { - AddMailboxView { mailbox in - DispatchQueue.main.async { - guard let mailbox = mailbox else { return } - (window?.windowScene?.delegate as? SceneDelegate)?.switchMailbox(mailbox) - } - } + NavigationLink(isActive: $isShowingAddMailboxView) { + AddMailboxView() } label: { MailButton(label: MailResourcesStrings.Localizable.buttonAddEmailAddress) { matomo.track(eventWithCategory: .noValidMailboxes, name: "addMailbox") - showAddMailbox.toggle() + isShowingAddMailboxView = true } .mailButtonFullWidth(true) .mailButtonStyle(.large) diff --git a/Mail/Views/Unavailable Mailbox/UpdateMailboxPasswordView.swift b/Mail/Views/Unavailable Mailbox/UpdateMailboxPasswordView.swift index 71d0f7994..503a308e3 100644 --- a/Mail/Views/Unavailable Mailbox/UpdateMailboxPasswordView.swift +++ b/Mail/Views/Unavailable Mailbox/UpdateMailboxPasswordView.swift @@ -25,8 +25,6 @@ import SwiftUI struct UpdateMailboxPasswordView: View { @LazyInjectService private var matomo: MatomoUtils - @Environment(\.window) private var window - @State private var updatedMailboxPassword = "" @State private var isShowingError = false @State private var isLoading = false @@ -104,7 +102,6 @@ struct UpdateMailboxPasswordView: View { isLoading = true do { try await AccountManager.instance.updateMailboxPassword(mailbox: mailbox, password: updatedMailboxPassword) - await (window?.windowScene?.delegate as? SceneDelegate)?.showMainView() } catch { isShowingError = true } @@ -117,7 +114,6 @@ struct UpdateMailboxPasswordView: View { isLoading = true do { try await AccountManager.instance.detachMailbox(mailbox: mailbox) - await (window?.windowScene?.delegate as? SceneDelegate)?.showMainView() } catch { isShowingError = true } diff --git a/MailCore/Cache/AccountManager.swift b/MailCore/Cache/AccountManager.swift index 077c0ebd8..949a0fe90 100644 --- a/MailCore/Cache/AccountManager.swift +++ b/MailCore/Cache/AccountManager.swift @@ -67,7 +67,7 @@ public extension InfomaniakNetworkLoginable { } } -public class AccountManager: RefreshTokenDelegate { +public class AccountManager: RefreshTokenDelegate, ObservableObject { @LazyInjectService var networkLoginService: InfomaniakNetworkLoginable @LazyInjectService var keychainHelper: KeychainHelper @LazyInjectService var bugTracker: BugTracker @@ -89,12 +89,14 @@ public class AccountManager: RefreshTokenDelegate { didSet { UserDefaults.shared.currentMailUserId = currentUserId setSentryUserId(userId: currentUserId) + objectWillChange.send() } } public var currentMailboxId: Int { didSet { UserDefaults.shared.currentMailboxId = currentMailboxId + objectWillChange.send() } } @@ -412,11 +414,15 @@ public class AccountManager: RefreshTokenDelegate { } } - public func addMailbox(mail: String, password: String, completion: (Mailbox?) -> Void) async throws { + public func addMailbox(mail: String, password: String) async throws { guard let apiFetcher = currentApiFetcher else { return } + _ = try await apiFetcher.addMailbox(mail: mail, password: password) try await updateUser(for: currentAccount) - completion(mailboxes.first(where: { $0.email == mail })) + guard let addedMailbox = mailboxes.first(where: { $0.email == mail }) else { return } + + matomo.track(eventWithCategory: .account, name: "addMailboxConfirm") + switchMailbox(newMailbox: addedMailbox) } public func updateMailboxPassword(mailbox: Mailbox, password: String) async throws { diff --git a/MailCore/Cache/MailboxManager.swift b/MailCore/Cache/MailboxManager.swift index 2af728a4c..4949b9fe9 100644 --- a/MailCore/Cache/MailboxManager.swift +++ b/MailCore/Cache/MailboxManager.swift @@ -1290,6 +1290,14 @@ public class MailboxManager: ObservableObject { } } +// MARK: - Equatable conformance + +extension MailboxManager: Equatable { + public static func == (lhs: MailboxManager, rhs: MailboxManager) -> Bool { + return lhs.mailbox.id == rhs.mailbox.id + } +} + public extension Realm { func uncheckedSafeWrite(_ block: () throws -> Void) throws { if isInWriteTransaction {