From 3a9ba3eba7db7780c9561317e6e1d85abdd01ee7 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Fri, 16 Jun 2023 14:51:21 +0200 Subject: [PATCH 01/21] feat(SwiftUI App Lifecycle): Basic conversion --- Mail/AppDelegate.swift | 204 +++++++++++------- Mail/Info.plist | 31 +-- Mail/LockedAppView.swift | 2 +- Mail/NotificationCenterDelegate.swift | 4 +- Mail/SceneDelegate.swift | 4 +- .../Views/Alerts/LogoutConfirmationView.swift | 4 +- .../MailboxManagement/MailboxCell.swift | 2 +- Mail/Views/NoMailboxView.swift | 4 +- Mail/Views/Onboarding/OnboardingView.swift | 12 +- Mail/Views/Onboarding/SlideView.swift | 2 +- Mail/Views/Settings/SettingsOptionView.swift | 3 +- Mail/Views/SplitView.swift | 30 +-- Mail/Views/Switch User/AccountCellView.swift | 2 +- Mail/Views/Switch User/AccountListView.swift | 2 +- Mail/Views/Switch User/AccountView.swift | 9 +- Mail/Views/Thread/WebView.swift | 2 +- MailCore/Cache/AccountManager.swift | 8 +- 17 files changed, 167 insertions(+), 158 deletions(-) diff --git a/Mail/AppDelegate.swift b/Mail/AppDelegate.swift index 5ee1d9267..11d00fa61 100644 --- a/Mail/AppDelegate.swift +++ b/Mail/AppDelegate.swift @@ -28,89 +28,10 @@ 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() +public struct EarlyDIHook { + public init() { + // setup DI ASAP setupDI() - DDLogInfo("Application starting in foreground ? \(UIApplication.shared.applicationState != .background)") - accountManager = AccountManager.instance - ApiFetcher.decoder.dateDecodingStrategy = .iso8601 - - UNUserNotificationCenter.current().delegate = notificationCenterDelegate - Task { - // Ask permission app launch - await NotificationsHelper.askForPermissions() - } - application.registerForRemoteNotifications() - - 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 { - guard account.token != nil else { continue } - let userApiFetcher = accountManager.getApiFetcher(for: account.userId, token: account.token) - Task { - await notificationService.updateRemoteNotificationsTokenIfNeeded(tokenData: deviceToken, - userApiFetcher: userApiFetcher) - } - } - } - - func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { - 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 refreshCacheData() { - guard let currentAccount = AccountManager.instance.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)") - } - } } func setupDI() { @@ -149,3 +70,122 @@ class AppDelegate: UIResponder, UIApplicationDelegate { SimpleResolver.sharedResolver.store(factory: avoider) } } + +@main +struct MailApp: App { + /// Making sure the DI is registered at a very early stage of the app launch. + private let dependencyInjectionHook = EarlyDIHook() + + @ObservedObject var accountManager = AccountManager.instance + + init() { + Logging.initLogging() + DDLogInfo("Application starting in foreground ? \(UIApplication.shared.applicationState != .background)") + ApiFetcher.decoder.dateDecodingStrategy = .iso8601 + } + + var body: some Scene { + WindowGroup { + Group { + if accountManager.currentAccount != nil, + let currentMailboxManager = accountManager.currentMailboxManager { + SplitView() + .environment(\.realmConfiguration, currentMailboxManager.realmConfiguration) + .environmentObject(currentMailboxManager) + } else { + OnboardingView() + } + } + .preferredColorScheme(.none) + .tint(UserDefaults.shared.accentColor.primary.swiftUIColor) + } + .defaultAppStorage(.shared) + } +} + +/* + 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 + + UNUserNotificationCenter.current().delegate = notificationCenterDelegate + Task { + // Ask permission app launch + await NotificationsHelper.askForPermissions() + } + application.registerForRemoteNotifications() + + 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 { + guard account.token != nil else { continue } + let userApiFetcher = accountManager.getApiFetcher(for: account.userId, token: account.token) + Task { + await notificationService.updateRemoteNotificationsTokenIfNeeded(tokenData: deviceToken, + userApiFetcher: userApiFetcher) + } + } + } + + func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { + 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 refreshCacheData() { + guard let currentAccount = AccountManager.instance.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/Info.plist b/Mail/Info.plist index 6a58d81b8..d4c59762d 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 @@ -101,10 +80,10 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - NSAppTransportSecurity - - NSAllowsArbitraryLoadsInWebContent - - + NSAppTransportSecurity + + NSAllowsArbitraryLoadsInWebContent + + diff --git a/Mail/LockedAppView.swift b/Mail/LockedAppView.swift index 9e471c78d..12bf12c49 100644 --- a/Mail/LockedAppView.swift +++ b/Mail/LockedAppView.swift @@ -61,7 +61,7 @@ struct LockedAppView: View { private func unlockApp() { Task { if (try? await appLockHelper.evaluatePolicy(reason: MailResourcesStrings.Localizable.lockAppTitle)) == true { - await (window?.windowScene?.delegate as? SceneDelegate)?.showMainView() + // await (window?.windowScene?.delegate as? SceneDelegate)?.showMainView() } } } diff --git a/Mail/NotificationCenterDelegate.swift b/Mail/NotificationCenterDelegate.swift index eb45bc2bd..2e2cf9249 100644 --- a/Mail/NotificationCenterDelegate.swift +++ b/Mail/NotificationCenterDelegate.swift @@ -39,10 +39,10 @@ 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) + // (scene?.delegate as? SceneDelegate)?.switchAccount(switchedAccount, mailbox: mailbox) } } else { - (scene?.delegate as? SceneDelegate)?.switchMailbox(mailbox) + AccountManager.instance.switchMailbox(newMailbox: mailbox) } } diff --git a/Mail/SceneDelegate.swift b/Mail/SceneDelegate.swift index 110d2ef8e..b02c33188 100644 --- a/Mail/SceneDelegate.swift +++ b/Mail/SceneDelegate.swift @@ -23,7 +23,7 @@ import MailCore import MailResources import SwiftUI import UIKit - +/* class SceneDelegate: UIResponder, UIWindowSceneDelegate, AccountManagerDelegate { var window: UIWindow? @@ -191,4 +191,4 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, AccountManagerDelegate return true } -} +}*/ diff --git a/Mail/Views/Alerts/LogoutConfirmationView.swift b/Mail/Views/Alerts/LogoutConfirmationView.swift index d05b62460..8f2ce4057 100644 --- a/Mail/Views/Alerts/LogoutConfirmationView.swift +++ b/Mail/Views/Alerts/LogoutConfirmationView.swift @@ -48,9 +48,9 @@ struct LogoutConfirmationView: View { } AccountManager.instance.removeTokenAndAccount(token: account.token) if let nextAccount = AccountManager.instance.accounts.first { - (window?.windowScene?.delegate as? SceneDelegate)?.switchAccount(nextAccount) + //(window?.windowScene?.delegate as? SceneDelegate)?.switchAccount(nextAccount) } else { - (window?.windowScene?.delegate as? SceneDelegate)?.showLoginView() + // (window?.windowScene?.delegate as? SceneDelegate)?.showLoginView() } AccountManager.instance.saveAccounts() } diff --git a/Mail/Views/Menu Drawer/MailboxManagement/MailboxCell.swift b/Mail/Views/Menu Drawer/MailboxManagement/MailboxCell.swift index 793d3de0f..be3fb72e6 100644 --- a/Mail/Views/Menu Drawer/MailboxManagement/MailboxCell.swift +++ b/Mail/Views/Menu Drawer/MailboxManagement/MailboxCell.swift @@ -78,7 +78,7 @@ struct MailboxCell: View { case .account: matomo.track(eventWithCategory: .account, name: "switchMailbox") } - (window?.windowScene?.delegate as? SceneDelegate)?.switchMailbox(mailbox) + AccountManager.instance.switchMailbox(newMailbox: mailbox) } } } diff --git a/Mail/Views/NoMailboxView.swift b/Mail/Views/NoMailboxView.swift index 1c26956c0..82df193c9 100644 --- a/Mail/Views/NoMailboxView.swift +++ b/Mail/Views/NoMailboxView.swift @@ -46,14 +46,14 @@ struct NoMailboxView: View { MailButton(icon: MailResourcesAsset.plus, label: MailResourcesStrings.Localizable.buttonAddEmailAddress) { AddMailboxView { _ in DispatchQueue.main.async { - (window?.windowScene?.delegate as? SceneDelegate)?.showMainView() + // (window?.windowScene?.delegate as? SceneDelegate)?.showMainView() } } } .mailButtonFullWidth(true) MailButton(label: MailResourcesStrings.Localizable.buttonLogInDifferentAccount) { - (window?.windowScene?.delegate as? SceneDelegate)?.showLoginView() + // (window?.windowScene?.delegate as? SceneDelegate)?.showLoginView() } .mailButtonStyle(.link) } diff --git a/Mail/Views/Onboarding/OnboardingView.swift b/Mail/Views/Onboarding/OnboardingView.swift index 405f75975..b107808d9 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? + //var sceneDelegate: SceneDelegate? nonisolated func didCompleteLoginWith(code: String, verifier: String) { Task { @@ -127,10 +127,10 @@ class LoginHandler: InfomaniakLoginDelegate, ObservableObject { Task { do { _ = try await AccountManager.instance.createAndSetCurrentAccount(code: code, codeVerifier: verifier) - sceneDelegate?.showMainView() + // sceneDelegate?.showMainView() UIApplication.shared.registerForRemoteNotifications() } catch let error as MailError where error == MailError.noMailbox { - sceneDelegate?.showNoMailboxView() + //sceneDelegate?.showNoMailboxView() } catch { if let previousAccount = previousAccount { AccountManager.instance.switchAccount(newAccount: previousAccount) @@ -194,7 +194,7 @@ struct OnboardingView: View { VStack(spacing: 24) { if selection == slides.count { MailButton(label: MailResourcesStrings.Localizable.buttonLogin) { - loginHandler.sceneDelegate = window?.windowScene?.delegate as? SceneDelegate + //loginHandler.sceneDelegate = window?.windowScene?.delegate as? SceneDelegate loginHandler.login() } .mailButtonFullWidth(true) @@ -238,7 +238,7 @@ struct OnboardingView: View { .sheet(isPresented: $isPresentingCreateAccount) { RegisterView(registrationProcess: .mail) { viewController in guard let viewController else { return } - loginHandler.sceneDelegate = window?.windowScene?.delegate as? SceneDelegate + // loginHandler.sceneDelegate = window?.windowScene?.delegate as? SceneDelegate loginHandler.loginAfterAccountCreation(from: viewController) } } @@ -246,7 +246,7 @@ struct OnboardingView: View { if UIDevice.current.userInterfaceIdiom == .phone { UIDevice.current .setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation") - AppDelegate.orientationLock = .portrait + // AppDelegate.orientationLock = .portrait UIViewController.attemptRotationToDeviceOrientation() } } diff --git a/Mail/Views/Onboarding/SlideView.swift b/Mail/Views/Onboarding/SlideView.swift index b7a67bf59..f94835ef3 100644 --- a/Mail/Views/Onboarding/SlideView.swift +++ b/Mail/Views/Onboarding/SlideView.swift @@ -98,7 +98,7 @@ struct SlideView: View { .padding(.horizontal, 32) } .onChange(of: accentColor) { _ in - (window?.windowScene?.delegate as? SceneDelegate)?.updateWindowUI() + //(window?.windowScene?.delegate as? SceneDelegate)?.updateWindowUI() } .onAppear { isVisible = true diff --git a/Mail/Views/Settings/SettingsOptionView.swift b/Mail/Views/Settings/SettingsOptionView.swift index 3d5ab80f3..24ddaaec5 100644 --- a/Mail/Views/Settings/SettingsOptionView.swift +++ b/Mail/Views/Settings/SettingsOptionView.swift @@ -40,7 +40,8 @@ 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 + //UIApplication.shared.connectedScenes.forEach { ($0.delegate as? SceneDelegate)?.updateWindowUI() } default: break } diff --git a/Mail/Views/SplitView.swift b/Mail/Views/SplitView.swift index 04b644c97..937b622bd 100644 --- a/Mail/Views/SplitView.swift +++ b/Mail/Views/SplitView.swift @@ -30,10 +30,6 @@ public class SplitViewManager: ObservableObject { @Published var showSearch = false @Published var selectedFolder: Folder? var splitViewController: UISplitViewController? - - init(folder: Folder?) { - selectedFolder = folder - } } struct SplitView: View { @@ -41,24 +37,18 @@ struct SplitView: View { @Environment(\.verticalSizeClass) var verticalSizeClass @Environment(\.window) var window + @EnvironmentObject var mailboxManager: MailboxManager + @State var splitViewController: UISplitViewController? @StateObject private var navigationDrawerController = NavigationDrawerState() @StateObject private var navigationStore = NavigationStore() - @StateObject private var splitViewManager: SplitViewManager - - let mailboxManager: MailboxManager + @StateObject private var splitViewManager = SplitViewManager() 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 { @@ -112,17 +102,14 @@ struct SplitView: View { } } .onAppear { - AppDelegate.orientationLock = .all + // 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 } @@ -135,13 +122,10 @@ struct SplitView: View { 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) } private func setupBehaviour(orientation: UIInterfaceOrientation) { diff --git a/Mail/Views/Switch User/AccountCellView.swift b/Mail/Views/Switch User/AccountCellView.swift index f3b0271f7..ca991f82b 100644 --- a/Mail/Views/Switch User/AccountCellView.swift +++ b/Mail/Views/Switch User/AccountCellView.swift @@ -49,7 +49,7 @@ struct AccountCellView: View { matomo.track(eventWithCategory: .account, name: "switch") withAnimation { selectedUserId = selectedUserId == account.userId ? nil : account.userId - (window?.windowScene?.delegate as? SceneDelegate)?.switchAccount(account) + //(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..98648fad3 100644 --- a/Mail/Views/Switch User/AccountListView.swift +++ b/Mail/Views/Switch User/AccountListView.swift @@ -79,7 +79,7 @@ struct AccountListView: View { isShowingNewAccountView = true } .fullScreenCover(isPresented: $isShowingNewAccountView, onDismiss: { - AppDelegate.orientationLock = .all + // AppDelegate.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..8d4b8d27e 100644 --- a/Mail/Views/Switch User/AccountView.swift +++ b/Mail/Views/Switch User/AccountView.swift @@ -31,10 +31,10 @@ class AccountViewDelegate: DeleteAccountDelegate { 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) + //(window?.windowScene?.delegate as? SceneDelegate)?.switchAccount(nextAccount) IKSnackBar.showSnackBar(message: "Account deleted") } else { - (window?.windowScene?.delegate as? SceneDelegate)?.showLoginView() + // (window?.windowScene?.delegate as? SceneDelegate)?.showLoginView() } AccountManager.instance.saveAccounts() } @@ -101,8 +101,8 @@ struct AccountView: View { NavigationLink { AddMailboxView { mailbox in DispatchQueue.main.async { - guard let mailbox = mailbox else { return } - (window?.windowScene?.delegate as? SceneDelegate)?.switchMailbox(mailbox) + guard let mailbox else { return } + AccountManager.instance.switchMailbox(newMailbox: mailbox) } } } label: { @@ -159,7 +159,6 @@ struct AccountView: View { .customAlert(isPresented: $isShowingLogoutAlert) { LogoutConfirmationView(account: account) } - .defaultAppStorage(.shared) .matomoView(view: [MatomoUtils.View.accountView.displayName, "Main"]) } } diff --git a/Mail/Views/Thread/WebView.swift b/Mail/Views/Thread/WebView.swift index 332c430bc..9ec2fdc24 100644 --- a/Mail/Views/Thread/WebView.swift +++ b/Mail/Views/Thread/WebView.swift @@ -58,7 +58,7 @@ struct WebView: UIViewRepresentable { ) { if let url = navigationAction.request.url, Constants.isMailTo(url) { decisionHandler(.cancel) - (parent.window?.windowScene?.delegate as? SceneDelegate)?.handleUrlOpen(url) + //(parent.window?.windowScene?.delegate as? SceneDelegate)?.handleUrlOpen(url) return } diff --git a/MailCore/Cache/AccountManager.swift b/MailCore/Cache/AccountManager.swift index 2a62a3116..200de5ad0 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,18 @@ public class AccountManager: RefreshTokenDelegate { didSet { UserDefaults.shared.currentMailUserId = currentUserId setSentryUserId(userId: currentUserId) + Task { @MainActor in + objectWillChange.send() + } } } public var currentMailboxId: Int { didSet { UserDefaults.shared.currentMailboxId = currentMailboxId + Task { @MainActor in + objectWillChange.send() + } } } From cb0284683a2c787a1de6302908fca1bea1661d25 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Mon, 19 Jun 2023 14:59:20 +0200 Subject: [PATCH 02/21] feat(SwiftUI App Lifecycle): Account Switch / Theme / Accent / AppDelegate --- Mail/AppDelegate.swift | 186 +++--------------- Mail/LockedAppView.swift | 12 +- Mail/MailApp.swift | 163 +++++++++++++++ Mail/NotificationCenterDelegate.swift | 3 +- .../Views/Alerts/LogoutConfirmationView.swift | 4 +- Mail/Views/Settings/SettingsOptionView.swift | 11 +- Mail/Views/Switch User/AccountCellView.swift | 2 +- Mail/Views/Switch User/AccountView.swift | 5 +- 8 files changed, 214 insertions(+), 172 deletions(-) create mode 100644 Mail/MailApp.swift diff --git a/Mail/AppDelegate.swift b/Mail/AppDelegate.swift index 11d00fa61..0f1ca774c 100644 --- a/Mail/AppDelegate.swift +++ b/Mail/AppDelegate.swift @@ -17,175 +17,47 @@ */ 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() - } +class AppDelegate: UIResponder, UIApplicationDelegate { + private let notificationCenterDelegate = NotificationCenterDelegate() + static var orientationLock = UIInterfaceOrientationMask.all - 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() + func application( + _ application: UIApplication, + willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + ) -> Bool { + UNUserNotificationCenter.current().delegate = notificationCenterDelegate + Task { + // Ask permission app launch + await NotificationsHelper.askForPermissions() } + application.registerForRemoteNotifications() - 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) - } -} - -@main -struct MailApp: App { - /// Making sure the DI is registered at a very early stage of the app launch. - private let dependencyInjectionHook = EarlyDIHook() - - @ObservedObject var accountManager = AccountManager.instance - - init() { - Logging.initLogging() - DDLogInfo("Application starting in foreground ? \(UIApplication.shared.applicationState != .background)") - ApiFetcher.decoder.dateDecodingStrategy = .iso8601 + return true } - var body: some Scene { - WindowGroup { - Group { - if accountManager.currentAccount != nil, - let currentMailboxManager = accountManager.currentMailboxManager { - SplitView() - .environment(\.realmConfiguration, currentMailboxManager.realmConfiguration) - .environmentObject(currentMailboxManager) - } else { - OnboardingView() - } + func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { + @InjectService var notificationService: InfomaniakNotifications + for account in AccountManager.instance.accounts { + guard account.token != nil else { continue } + let userApiFetcher = AccountManager.instance.getApiFetcher(for: account.userId, token: account.token) + Task { + await notificationService.updateRemoteNotificationsTokenIfNeeded(tokenData: deviceToken, + userApiFetcher: userApiFetcher) } - .preferredColorScheme(.none) - .tint(UserDefaults.shared.accentColor.primary.swiftUIColor) } - .defaultAppStorage(.shared) } -} - -/* - 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 - - UNUserNotificationCenter.current().delegate = notificationCenterDelegate - Task { - // Ask permission app launch - await NotificationsHelper.askForPermissions() - } - application.registerForRemoteNotifications() - - 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 { - guard account.token != nil else { continue } - let userApiFetcher = accountManager.getApiFetcher(for: account.userId, token: account.token) - Task { - await notificationService.updateRemoteNotificationsTokenIfNeeded(tokenData: deviceToken, - userApiFetcher: userApiFetcher) - } - } - } - - func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { - 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 refreshCacheData() { - guard let currentAccount = AccountManager.instance.currentAccount else { - return - } - - Task { - do { - try await accountManager.updateUser(for: currentAccount) - accountManager.enableBugTrackerIfAvailable() + func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { + DDLogError("Failed registering for notifications: \(error)") + } - try await accountManager.currentContactManager?.fetchContactsAndAddressBooks() - } catch { - DDLogError("Error while updating user account: \(error)") - } - } - } - } - */ + func application(_ application: UIApplication, + supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask { + return AppDelegate.orientationLock + } +} diff --git a/Mail/LockedAppView.swift b/Mail/LockedAppView.swift index 12bf12c49..11ebbfb5f 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 + + @Binding var isAppLocked: Bool var body: some View { ZStack { @@ -61,7 +62,12 @@ 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 { @MainActor in + withAnimation { + isAppLocked = false + } + } } } } @@ -69,6 +75,6 @@ struct LockedAppView: View { struct LockedAppView_Previews: PreviewProvider { static var previews: some View { - LockedAppView() + LockedAppView(isAppLocked: .constant(true)) } } diff --git a/Mail/MailApp.swift b/Mail/MailApp.swift new file mode 100644 index 000000000..343311ea3 --- /dev/null +++ b/Mail/MailApp.swift @@ -0,0 +1,163 @@ +/* + 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() + } + + 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) + } +} + +@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 + + @State var isAppLocked = false + @ObservedObject var accountManager = AccountManager.instance + + init() { + Logging.initLogging() + DDLogInfo("Application starting in foreground ? \(UIApplication.shared.applicationState != .background)") + ApiFetcher.decoder.dateDecodingStrategy = .iso8601 + } + + var body: some Scene { + WindowGroup { + ZStack { + if isAppLocked { + LockedAppView(isAppLocked: $isAppLocked) + } else if accountManager.currentAccount != nil, + let currentMailboxManager = accountManager.currentMailboxManager { + SplitView() + .environment(\.realmConfiguration, currentMailboxManager.realmConfiguration) + .environmentObject(currentMailboxManager) + } else { + OnboardingView() + } + } + .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() + isAppLocked = UserDefaults.shared.isAppLockEnabled && appLockHelper.isAppLocked + case .background: + if UserDefaults.shared.isAppLockEnabled && !isAppLocked { + appLockHelper.setTime() + } + case .inactive: + break + @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.instance.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 2e2cf9249..2d1c9840f 100644 --- a/Mail/NotificationCenterDelegate.swift +++ b/Mail/NotificationCenterDelegate.swift @@ -39,7 +39,8 @@ 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 { AccountManager.instance.switchMailbox(newMailbox: mailbox) diff --git a/Mail/Views/Alerts/LogoutConfirmationView.swift b/Mail/Views/Alerts/LogoutConfirmationView.swift index 8f2ce4057..d91f67ac6 100644 --- a/Mail/Views/Alerts/LogoutConfirmationView.swift +++ b/Mail/Views/Alerts/LogoutConfirmationView.swift @@ -48,9 +48,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/Settings/SettingsOptionView.swift b/Mail/Views/Settings/SettingsOptionView.swift index 24ddaaec5..eb038f6a9 100644 --- a/Mail/Views/Settings/SettingsOptionView.swift +++ b/Mail/Views/Settings/SettingsOptionView.swift @@ -38,10 +38,15 @@ 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: - break - //UIApplication.shared.connectedScenes.forEach { ($0.delegate as? SceneDelegate)?.updateWindowUI() } + case \.accentColor: + @AppStorage(UserDefaults.shared.key(.accentColor)) var accentColor = DefaultPreferences.accentColor + accentColor = UserDefaults.shared.accentColor + case \.theme: + @AppStorage(UserDefaults.shared.key(.theme)) var theme = DefaultPreferences.theme + theme = UserDefaults.shared.theme default: break } diff --git a/Mail/Views/Switch User/AccountCellView.swift b/Mail/Views/Switch User/AccountCellView.swift index ca991f82b..968bb7698 100644 --- a/Mail/Views/Switch User/AccountCellView.swift +++ b/Mail/Views/Switch User/AccountCellView.swift @@ -49,7 +49,7 @@ struct AccountCellView: View { matomo.track(eventWithCategory: .account, name: "switch") withAnimation { selectedUserId = selectedUserId == account.userId ? nil : account.userId - //(window?.windowScene?.delegate as? SceneDelegate)?.switchAccount(account) + AccountManager.instance.switchAccount(newAccount: account) } } label: { AccountHeaderCell(account: account, isSelected: Binding(get: { diff --git a/Mail/Views/Switch User/AccountView.swift b/Mail/Views/Switch User/AccountView.swift index 8d4b8d27e..65a79c98c 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() } From dca5b7fb65e3814aaa68be959721c7cd5016f1c4 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Fri, 23 Jun 2023 08:12:55 +0200 Subject: [PATCH 03/21] feat(SwiftUI App Lifecycle): RootView switching --- Mail/LockedAppView.swift | 10 ++- Mail/MailApp.swift | 80 ++++++++++++---------- Mail/RootView.swift | 41 +++++++++++ Mail/Utils/NavigationStore.swift | 72 ++++++++++++++++++- Mail/Views/Onboarding/OnboardingView.swift | 4 -- Mail/Views/Onboarding/SlideView.swift | 3 - Mail/Views/SplitView.swift | 21 +++--- 7 files changed, 167 insertions(+), 64 deletions(-) create mode 100644 Mail/RootView.swift diff --git a/Mail/LockedAppView.swift b/Mail/LockedAppView.swift index 11ebbfb5f..5523b70dd 100644 --- a/Mail/LockedAppView.swift +++ b/Mail/LockedAppView.swift @@ -25,7 +25,7 @@ import SwiftUI struct LockedAppView: View { @LazyInjectService var appLockHelper: AppLockHelper - @Binding var isAppLocked: Bool + @EnvironmentObject var navigationState: NavigationStore var body: some View { ZStack { @@ -63,10 +63,8 @@ struct LockedAppView: View { Task { if (try? await appLockHelper.evaluatePolicy(reason: MailResourcesStrings.Localizable.lockAppTitle)) == true { appLockHelper.setTime() - Task { @MainActor in - withAnimation { - isAppLocked = false - } + Task { + navigationState.transitionToRootViewDestination(.mainView) } } } @@ -75,6 +73,6 @@ struct LockedAppView: View { struct LockedAppView_Previews: PreviewProvider { static var previews: some View { - LockedAppView(isAppLocked: .constant(true)) + LockedAppView() } } diff --git a/Mail/MailApp.swift b/Mail/MailApp.swift index 343311ea3..ce1bbf600 100644 --- a/Mail/MailApp.swift +++ b/Mail/MailApp.swift @@ -78,12 +78,15 @@ struct MailApp: App { @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 - @State var isAppLocked = false - @ObservedObject var accountManager = AccountManager.instance + @StateObject private var navigationStore = NavigationStore() + + @ObservedObject private var accountManager = AccountManager.instance init() { Logging.initLogging() @@ -93,45 +96,48 @@ struct MailApp: App { var body: some Scene { WindowGroup { - ZStack { - if isAppLocked { - LockedAppView(isAppLocked: $isAppLocked) - } else if accountManager.currentAccount != nil, - let currentMailboxManager = accountManager.currentMailboxManager { - SplitView() - .environment(\.realmConfiguration, currentMailboxManager.realmConfiguration) - .environmentObject(currentMailboxManager) - } else { - OnboardingView() + RootView(rootViewState: navigationStore.rootViewState) + .environmentObject(navigationStore) + .onAppear { + updateUI(accent: accentColor, theme: theme) } - } - .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: + .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() + if UserDefaults.shared.isAppLockEnabled + && appLockHelper.isAppLocked + && accountManager.currentAccount != nil { + navigationStore.transitionToRootViewDestination(.appLocked) + } + case .background: + if UserDefaults.shared.isAppLockEnabled && navigationStore.rootViewState != .appLocked { + appLockHelper.setTime() + } + case .inactive: + Task { + await NotificationsHelper.updateUnreadCountBadge() + } + @unknown default: + break + } + } + .onChange(of: accountManager.currentAccount) { _ in refreshCacheData() - isAppLocked = UserDefaults.shared.isAppLockEnabled && appLockHelper.isAppLocked - case .background: - if UserDefaults.shared.isAppLockEnabled && !isAppLocked { - appLockHelper.setTime() + } + .onChange(of: accountManager.currentMailboxId) { _ in + if accountManager.currentAccount == nil { + navigationStore.transitionToRootViewDestination(.onboarding) + } else if navigationStore.rootViewState != .appLocked { + navigationStore.transitionToRootViewDestination(.mainView) } - case .inactive: - break - @unknown default: - break } - } - .onChange(of: accountManager.currentAccount) { _ in - refreshCacheData() - } } .defaultAppStorage(.shared) } diff --git a/Mail/RootView.swift b/Mail/RootView.swift new file mode 100644 index 000000000..92ab04676 --- /dev/null +++ b/Mail/RootView.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 SwiftUI + +struct RootView: View { + @Environment(\.horizontalSizeClass) var horizontalSizeClass + @Environment(\.verticalSizeClass) var verticalSizeClass + + let rootViewState: RootViewState + var body: some View { + ZStack { + switch rootViewState { + case .appLocked: + LockedAppView() + case .mainView(let currentMailboxManager): + SplitView(mailboxManager: currentMailboxManager) + case .onboarding: + OnboardingView() + } + } + .id(rootViewState) + .animation(.easeInOut(duration: 0.3), value: rootViewState) + .environment(\.isCompactWindow, horizontalSizeClass == .compact || verticalSizeClass == .compact) + } +} diff --git a/Mail/Utils/NavigationStore.swift b/Mail/Utils/NavigationStore.swift index 9c34f259e..97962a511 100644 --- a/Mail/Utils/NavigationStore.swift +++ b/Mail/Utils/NavigationStore.swift @@ -17,10 +17,80 @@ */ import Foundation -import SwiftUI 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 (.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 appLocked + case mainView(MailboxManager) + case onboarding +} + +enum RootViewDestination { + case appLocked + case mainView + case onboarding +} +@MainActor class NavigationStore: ObservableObject { + private let accountManager = AccountManager.instance + @Published private(set) var rootViewState: RootViewState @Published var messageReply: MessageReply? @Published var threadPath = [Thread]() + + init() { + if accountManager.currentAccount != nil, + let currentMailboxManager = accountManager.currentMailboxManager { + rootViewState = .mainView(currentMailboxManager) + } else { + rootViewState = .onboarding + } + } + + func transitionToRootViewDestination(_ destination: RootViewDestination) { + withAnimation { + switch destination { + case .appLocked: + rootViewState = .appLocked + case .mainView: + if accountManager.currentAccount != nil, + let currentMailboxManager = accountManager.currentMailboxManager { + rootViewState = .mainView(currentMailboxManager) + } else { + rootViewState = .onboarding + } + case .onboarding: + rootViewState = .onboarding + } + } + } } diff --git a/Mail/Views/Onboarding/OnboardingView.swift b/Mail/Views/Onboarding/OnboardingView.swift index b107808d9..1cba0894b 100644 --- a/Mail/Views/Onboarding/OnboardingView.swift +++ b/Mail/Views/Onboarding/OnboardingView.swift @@ -74,7 +74,6 @@ class LoginHandler: InfomaniakLoginDelegate, ObservableObject { @Published var isLoading = false @Published var isPresentingErrorAlert = false - //var sceneDelegate: SceneDelegate? nonisolated func didCompleteLoginWith(code: String, verifier: String) { Task { @@ -127,7 +126,6 @@ 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() @@ -194,7 +192,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 +235,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) } } diff --git a/Mail/Views/Onboarding/SlideView.swift b/Mail/Views/Onboarding/SlideView.swift index f94835ef3..8fbbbaf89 100644 --- a/Mail/Views/Onboarding/SlideView.swift +++ b/Mail/Views/Onboarding/SlideView.swift @@ -97,9 +97,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/SplitView.swift b/Mail/Views/SplitView.swift index 937b622bd..79e137b8b 100644 --- a/Mail/Views/SplitView.swift +++ b/Mail/Views/SplitView.swift @@ -33,25 +33,20 @@ public class SplitViewManager: ObservableObject { } struct SplitView: View { - @Environment(\.horizontalSizeClass) var horizontalSizeClass - @Environment(\.verticalSizeClass) var verticalSizeClass - @Environment(\.window) var window + @Environment(\.isCompactWindow) var isCompactWindow - @EnvironmentObject var mailboxManager: MailboxManager + @EnvironmentObject var navigationStore: NavigationStore @State var splitViewController: UISplitViewController? @StateObject private var navigationDrawerController = NavigationDrawerState() - @StateObject private var navigationStore = NavigationStore() @StateObject private var splitViewManager = SplitViewManager() - private var isCompact: Bool { - UIConstants.isCompact(horizontalSizeClass: horizontalSizeClass, verticalSizeClass: verticalSizeClass) - } + let mailboxManager: MailboxManager var body: some View { Group { - if isCompact { + if isCompactWindow { ZStack { NBNavigationStack(path: $navigationStore.threadPath) { ThreadListManagerView() @@ -102,7 +97,7 @@ struct SplitView: View { } } .onAppear { - // AppDelegate.orientationLock = .all + AppDelegate.orientationLock = .all } .task(id: mailboxManager.mailbox.objectId) { await fetchSignatures() @@ -116,16 +111,16 @@ struct SplitView: View { 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(\.isCompactWindow, horizontalSizeClass == .compact || verticalSizeClass == .compact) .environmentObject(splitViewManager) .environmentObject(navigationDrawerController) - .environmentObject(navigationStore) + .environmentObject(mailboxManager) + .environment(\.realmConfiguration, mailboxManager.realmConfiguration) } private func setupBehaviour(orientation: UIInterfaceOrientation) { From 8e6090952dc77b24c54d958496c869e9306eeed1 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Fri, 23 Jun 2023 10:14:13 +0200 Subject: [PATCH 04/21] feat(SwiftUI App Lifecycle): Account switching --- Mail/MailApp.swift | 19 +-- Mail/RootView.swift | 7 +- Mail/Utils/NavigationStore.swift | 39 ++++- Mail/Views/SheetViewModifier.swift | 6 +- Mail/Views/SplitView.swift | 10 +- Mail/Views/Switch User/AccountCellView.swift | 8 +- Mail/Views/Switch User/AccountView.swift | 137 ++++++++---------- .../Thread List/ThreadListModifiers.swift | 7 +- MailCore/Cache/AccountManager.swift | 8 +- 9 files changed, 121 insertions(+), 120 deletions(-) diff --git a/Mail/MailApp.swift b/Mail/MailApp.swift index ce1bbf600..dbe1ffca0 100644 --- a/Mail/MailApp.swift +++ b/Mail/MailApp.swift @@ -86,7 +86,7 @@ struct MailApp: App { @StateObject private var navigationStore = NavigationStore() - @ObservedObject private var accountManager = AccountManager.instance + private let accountManager = AccountManager.instance init() { Logging.initLogging() @@ -96,7 +96,7 @@ struct MailApp: App { var body: some Scene { WindowGroup { - RootView(rootViewState: navigationStore.rootViewState) + RootView() .environmentObject(navigationStore) .onAppear { updateUI(accent: accentColor, theme: theme) @@ -111,11 +111,7 @@ struct MailApp: App { switch newScenePhase { case .active: refreshCacheData() - if UserDefaults.shared.isAppLockEnabled - && appLockHelper.isAppLocked - && accountManager.currentAccount != nil { - navigationStore.transitionToRootViewDestination(.appLocked) - } + navigationStore.transitionToLockViewIfNeeded() case .background: if UserDefaults.shared.isAppLockEnabled && navigationStore.rootViewState != .appLocked { appLockHelper.setTime() @@ -131,13 +127,6 @@ struct MailApp: App { .onChange(of: accountManager.currentAccount) { _ in refreshCacheData() } - .onChange(of: accountManager.currentMailboxId) { _ in - if accountManager.currentAccount == nil { - navigationStore.transitionToRootViewDestination(.onboarding) - } else if navigationStore.rootViewState != .appLocked { - navigationStore.transitionToRootViewDestination(.mainView) - } - } } .defaultAppStorage(.shared) } @@ -151,7 +140,7 @@ struct MailApp: App { } func refreshCacheData() { - guard let currentAccount = AccountManager.instance.currentAccount else { + guard let currentAccount = accountManager.currentAccount else { return } diff --git a/Mail/RootView.swift b/Mail/RootView.swift index 92ab04676..e745a2b14 100644 --- a/Mail/RootView.swift +++ b/Mail/RootView.swift @@ -22,10 +22,11 @@ struct RootView: View { @Environment(\.horizontalSizeClass) var horizontalSizeClass @Environment(\.verticalSizeClass) var verticalSizeClass - let rootViewState: RootViewState + @EnvironmentObject var navigationStore: NavigationStore + var body: some View { ZStack { - switch rootViewState { + switch navigationStore.rootViewState { case .appLocked: LockedAppView() case .mainView(let currentMailboxManager): @@ -34,8 +35,6 @@ struct RootView: View { OnboardingView() } } - .id(rootViewState) - .animation(.easeInOut(duration: 0.3), value: rootViewState) .environment(\.isCompactWindow, horizontalSizeClass == .compact || verticalSizeClass == .compact) } } diff --git a/Mail/Utils/NavigationStore.swift b/Mail/Utils/NavigationStore.swift index 97962a511..e40eafe4a 100644 --- a/Mail/Utils/NavigationStore.swift +++ b/Mail/Utils/NavigationStore.swift @@ -16,7 +16,10 @@ along with this program. If not, see . */ +import Combine import Foundation +import InfomaniakCoreUI +import InfomaniakDI import MailCore import SwiftUI @@ -24,7 +27,7 @@ enum RootViewState: Equatable, Hashable, Identifiable { var id: Int { return hashValue } - + static func == (lhs: RootViewState, rhs: RootViewState) -> Bool { switch (lhs, rhs) { case (.appLocked, .appLocked): @@ -62,7 +65,11 @@ enum RootViewDestination { @MainActor class NavigationStore: 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 threadPath = [Thread]() @@ -74,6 +81,10 @@ class NavigationStore: ObservableObject { } else { rootViewState = .onboarding } + + accountManagerObservation = accountManager.objectWillChange.receive(on: RunLoop.main).sink { [weak self] in + self?.switchToCurrentMailboxManagerIfPossible() + } } func transitionToRootViewDestination(_ destination: RootViewDestination) { @@ -82,15 +93,29 @@ class NavigationStore: ObservableObject { case .appLocked: rootViewState = .appLocked case .mainView: - if accountManager.currentAccount != nil, - let currentMailboxManager = accountManager.currentMailboxManager { - rootViewState = .mainView(currentMailboxManager) - } else { - rootViewState = .onboarding - } + switchToCurrentMailboxManagerIfPossible() case .onboarding: rootViewState = .onboarding } } } + + 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 { + rootViewState = .onboarding + } + } } 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 79e137b8b..1e6116ed5 100644 --- a/Mail/Views/SplitView.swift +++ b/Mail/Views/SplitView.swift @@ -33,11 +33,12 @@ public class SplitViewManager: ObservableObject { } struct SplitView: View { - @Environment(\.isCompactWindow) var isCompactWindow + @Environment(\.isCompactWindow) private var isCompactWindow + @Environment(\.scenePhase) private var scenePhase - @EnvironmentObject var navigationStore: NavigationStore + @EnvironmentObject private var navigationStore: NavigationStore - @State var splitViewController: UISplitViewController? + @State private var splitViewController: UISplitViewController? @StateObject private var navigationDrawerController = NavigationDrawerState() @StateObject private var splitViewManager = SplitViewManager() @@ -78,7 +79,8 @@ struct SplitView: View { .sheet(item: $navigationStore.messageReply) { messageReply in ComposeMessageView.replyOrForwardMessage(messageReply: messageReply, mailboxManager: mailboxManager) } - .onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in + .onChange(of: scenePhase) { newScenePhase in + guard newScenePhase == .active else { return } Task { try await mailboxManager.folders() } diff --git a/Mail/Views/Switch User/AccountCellView.swift b/Mail/Views/Switch User/AccountCellView.swift index 968bb7698..d84388268 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? @@ -47,10 +47,8 @@ struct AccountCellView: View { Button { @InjectService var matomo: MatomoUtils matomo.track(eventWithCategory: .account, name: "switch") - withAnimation { - selectedUserId = selectedUserId == account.userId ? nil : account.userId - AccountManager.instance.switchAccount(newAccount: 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 65a79c98c..50914eaa8 100644 --- a/Mail/Views/Switch User/AccountView.swift +++ b/Mail/Views/Switch User/AccountView.swift @@ -43,9 +43,6 @@ class AccountViewDelegate: DeleteAccountDelegate { } struct AccountView: View { - @Environment(\.dismiss) private var dismiss - @Environment(\.window) private var window - @AppStorage(UserDefaults.shared.key(.accentColor)) private var accentColor = DefaultPreferences.accentColor @LazyInjectService private var matomo: MatomoUtils @@ -63,99 +60,93 @@ struct AccountView: View { } var body: some View { - NavigationView { - VStack(spacing: 0) { - ScrollView { - AvatarView(avatarDisplayable: account.user, size: 104) - .padding(.top, 24) - .padding(.bottom, 16) + 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) + VStack(spacing: 0) { + Text(account.user.displayName) + .textStyle(.header2) + .padding(.bottom, 4) - Text(account.user.email) - .textStyle(.bodySmallSecondary) - .padding(.bottom, 16) + 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) } + } - // Email list - VStack(alignment: .leading, spacing: 12) { - HStack(alignment: .center) { - Text(MailResourcesStrings.Localizable.buttonAccountAssociatedEmailAddresses) - .textStyle(.bodySmallSecondary) + // Email list + VStack(alignment: .leading, spacing: 12) { + HStack(alignment: .center) { + Text(MailResourcesStrings.Localizable.buttonAccountAssociatedEmailAddresses) + .textStyle(.bodySmallSecondary) - Spacer() + Spacer() - NavigationLink { - AddMailboxView { mailbox in - DispatchQueue.main.async { - guard let mailbox else { return } - AccountManager.instance.switchMailbox(newMailbox: mailbox) - } + NavigationLink { + AddMailboxView { mailbox in + DispatchQueue.main.async { + guard let mailbox else { return } + AccountManager.instance.switchMailbox(newMailbox: mailbox) } - } label: { - MailResourcesAsset.addCircle.swiftUIImage - .resizable() - .foregroundColor(accentColor.primary) - .frame(width: 16, height: 16) } + } label: { + MailResourcesAsset.addCircle.swiftUIImage + .resizable() + .foregroundColor(accentColor.primary) + .frame(width: 16, height: 16) } - .padding(.bottom, 16) - - if let currentMailbox = selectedMailbox { - MailboxCell(mailbox: currentMailbox) - .mailboxCellStyle(.account) - } + } + .padding(.bottom, 16) - ForEach(otherMailbox) { mailbox in - MailboxCell(mailbox: mailbox) - .mailboxCellStyle(.account) - } + if let currentMailbox = selectedMailbox { + MailboxCell(mailbox: currentMailbox) + .mailboxCellStyle(.account) } - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.top, 24) - Spacer() + ForEach(otherMailbox) { mailbox in + MailboxCell(mailbox: mailbox) + .mailboxCellStyle(.account) + } } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.top, 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() - } - .mailButtonStyle(.destructive) - .padding(.bottom, 24) + 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() } - .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) } + .sheetViewStyle() .matomoView(view: [MatomoUtils.View.accountView.displayName, "Main"]) } } diff --git a/Mail/Views/Thread List/ThreadListModifiers.swift b/Mail/Views/Thread List/ThreadListModifiers.swift index bd4d8bc6a..e58275987 100644 --- a/Mail/Views/Thread List/ThreadListModifiers.swift +++ b/Mail/Views/Thread List/ThreadListModifiers.swift @@ -58,6 +58,7 @@ struct ThreadListToolbar: ViewModifier { @EnvironmentObject private var splitViewManager: SplitViewManager @EnvironmentObject private var navigationDrawerState: NavigationDrawerState + @EnvironmentObject private var navigationStore: NavigationStore @State private var isShowingSwitchAccount = false @State private var multipleSelectionActionsTarget: ActionsTarget? @@ -126,6 +127,9 @@ struct ThreadListToolbar: ViewModifier { AvatarView(avatarDisplayable: AccountManager.instance.currentAccount.user) } .accessibilityLabel(MailResourcesStrings.Localizable.contentDescriptionUserAvatar) + .sheet(isPresented: $isShowingSwitchAccount) { + AccountView(mailboxes: AccountManager.instance.mailboxes) + } } } } @@ -166,8 +170,5 @@ struct ThreadListToolbar: ViewModifier { : "" ) .navigationBarTitleDisplayMode(.inline) - .sheet(isPresented: $isShowingSwitchAccount) { - AccountView(mailboxes: AccountManager.instance.mailboxes) - } } } diff --git a/MailCore/Cache/AccountManager.swift b/MailCore/Cache/AccountManager.swift index 200de5ad0..0cea3a8dd 100644 --- a/MailCore/Cache/AccountManager.swift +++ b/MailCore/Cache/AccountManager.swift @@ -89,18 +89,14 @@ public class AccountManager: RefreshTokenDelegate, ObservableObject { didSet { UserDefaults.shared.currentMailUserId = currentUserId setSentryUserId(userId: currentUserId) - Task { @MainActor in - objectWillChange.send() - } + objectWillChange.send() } } public var currentMailboxId: Int { didSet { UserDefaults.shared.currentMailboxId = currentMailboxId - Task { @MainActor in - objectWillChange.send() - } + objectWillChange.send() } } From b7b455b34d547d00d8e59614200657e773ec169d Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Fri, 23 Jun 2023 11:02:53 +0200 Subject: [PATCH 05/21] feat(SwiftUI App Lifecycle): ThreadListManagerView - Ensure ViewModel recreation --- Mail/Views/ThreadListManagerView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Mail/Views/ThreadListManagerView.swift b/Mail/Views/ThreadListManagerView.swift index b838b9af0..e0514912f 100644 --- a/Mail/Views/ThreadListManagerView.swift +++ b/Mail/Views/ThreadListManagerView.swift @@ -52,6 +52,7 @@ struct ThreadListManagerView: View { } } } + .id(mailboxManager.mailbox.id) .animation(.easeInOut(duration: 0.25), value: splitViewManager.showSearch) .sheet(item: $editedMessageDraft) { draft in ComposeMessageView.editDraft(draft: draft, mailboxManager: mailboxManager) From 6a4dca2a3318e4adeea9503dde2564edbe933f94 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Wed, 28 Jun 2023 14:57:48 +0200 Subject: [PATCH 06/21] feat(SwiftUI App Lifecycle): MenuDrawerView - Ensure FolderListView update --- Mail/Views/Menu Drawer/FolderListView.swift | 28 ++++++++++------ .../MailboxManagement/MailboxCell.swift | 3 +- .../MailboxesManagementView.swift | 5 +++ Mail/Views/Menu Drawer/MenuDrawerView.swift | 33 +++++++++---------- MailCore/Cache/MailboxManager.swift | 8 +++++ 5 files changed, 48 insertions(+), 29 deletions(-) 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 be3fb72e6..a39273fb7 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 let mailbox: Mailbox @@ -79,6 +79,7 @@ struct MailboxCell: View { matomo.track(eventWithCategory: .account, name: "switchMailbox") } AccountManager.instance.switchMailbox(newMailbox: mailbox) + navigationDrawerState.close() } } } 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..7065f5435 100644 --- a/Mail/Views/Menu Drawer/MenuDrawerView.swift +++ b/Mail/Views/Menu Drawer/MenuDrawerView.swift @@ -138,33 +138,30 @@ struct MenuDrawerView: View { ScrollView { VStack(spacing: 0) { - Group { - MailboxesManagementView() + MailboxesManagementView() - IKDivider(hasVerticalPadding: true, horizontalPadding: UIConstants.menuDrawerHorizontalPadding) + IKDivider(hasVerticalPadding: true, horizontalPadding: UIConstants.menuDrawerHorizontalPadding) - FolderListView(mailboxManager: mailboxManager) + FolderListView(mailboxManager: mailboxManager) - IKDivider(hasVerticalPadding: true, horizontalPadding: UIConstants.menuDrawerHorizontalPadding) - } - Group { - MenuDrawerItemsAdvancedListView( - mailboxCanRestoreEmails: mailboxManager.mailbox.permissions?.canRestoreEmails == true - ) + IKDivider(hasVerticalPadding: true, horizontalPadding: UIConstants.menuDrawerHorizontalPadding) - IKDivider(hasVerticalPadding: true, horizontalPadding: UIConstants.menuDrawerHorizontalPadding) + MenuDrawerItemsAdvancedListView( + mailboxCanRestoreEmails: mailboxManager.mailbox.permissions?.canRestoreEmails == true + ) - MenuDrawerItemsHelpListView() - if mailboxManager.mailbox.isLimited, let quotas = mailboxManager.mailbox.quotas { - IKDivider(hasVerticalPadding: true, horizontalPadding: UIConstants.menuDrawerHorizontalPadding) - - 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/MailCore/Cache/MailboxManager.swift b/MailCore/Cache/MailboxManager.swift index f716ee9bd..99522b5c0 100644 --- a/MailCore/Cache/MailboxManager.swift +++ b/MailCore/Cache/MailboxManager.swift @@ -1316,6 +1316,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 { From a2af0809932711fd56842de1596018d06aa30957 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Thu, 29 Jun 2023 13:02:39 +0200 Subject: [PATCH 07/21] feat(SwiftUI App Lifecycle): Remove @Environment(\.window) --- Mail/Views/Alerts/LogoutConfirmationView.swift | 2 -- Mail/Views/New Message/Recipients/RecipientChip.swift | 3 +-- Mail/Views/NoMailboxView.swift | 2 -- Mail/Views/Onboarding/OnboardingView.swift | 1 - Mail/Views/Onboarding/SlideView.swift | 1 - Mail/Views/Thread/WebView.swift | 2 -- 6 files changed, 1 insertion(+), 10 deletions(-) diff --git a/Mail/Views/Alerts/LogoutConfirmationView.swift b/Mail/Views/Alerts/LogoutConfirmationView.swift index d91f67ac6..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 { 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 82df193c9..e81bb2aec 100644 --- a/Mail/Views/NoMailboxView.swift +++ b/Mail/Views/NoMailboxView.swift @@ -21,8 +21,6 @@ import MailResources import SwiftUI struct NoMailboxView: View { - @Environment(\.window) var window - let slide = Slide( id: 1, backgroundImage: MailResourcesAsset.onboardingBackground3.swiftUIImage, diff --git a/Mail/Views/Onboarding/OnboardingView.swift b/Mail/Views/Onboarding/OnboardingView.swift index 1cba0894b..870638cfe 100644 --- a/Mail/Views/Onboarding/OnboardingView.swift +++ b/Mail/Views/Onboarding/OnboardingView.swift @@ -147,7 +147,6 @@ class LoginHandler: InfomaniakLoginDelegate, ObservableObject { } struct OnboardingView: View { - @Environment(\.window) private var window @Environment(\.dismiss) private var dismiss @AppStorage(UserDefaults.shared.key(.accentColor)) private var accentColor = DefaultPreferences.accentColor diff --git a/Mail/Views/Onboarding/SlideView.swift b/Mail/Views/Onboarding/SlideView.swift index 8fbbbaf89..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 diff --git a/Mail/Views/Thread/WebView.swift b/Mail/Views/Thread/WebView.swift index e2ecd424a..301dce414 100644 --- a/Mail/Views/Thread/WebView.swift +++ b/Mail/Views/Thread/WebView.swift @@ -21,8 +21,6 @@ import SwiftUI import WebKit struct WebView: UIViewRepresentable { - @Environment(\.window) private var window - @ObservedObject var model: WebViewModel let messageUid: String From 48bbde01df5016e279faf16b7bb6af07dd61ff06 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Thu, 29 Jun 2023 13:08:32 +0200 Subject: [PATCH 08/21] feat(SwiftUI App Lifecycle): NavigationDrawer stop using @Environment(\.window) --- Mail/Views/Menu Drawer/MenuDrawerView.swift | 89 ++++++++++----------- 1 file changed, 44 insertions(+), 45 deletions(-) diff --git a/Mail/Views/Menu Drawer/MenuDrawerView.swift b/Mail/Views/Menu Drawer/MenuDrawerView.swift index 7065f5435..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 { From cd00a9c4990bef996dee8ce87498a9b76472bbaa Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Thu, 29 Jun 2023 13:50:18 +0200 Subject: [PATCH 09/21] feat(SwiftUI App Lifecycle): Handle mailto urls --- Mail/Views/SplitView.swift | 11 +++++++++++ Mail/Views/Thread/WebView.swift | 4 +++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Mail/Views/SplitView.swift b/Mail/Views/SplitView.swift index 68589ad19..aaf09c0f5 100644 --- a/Mail/Views/SplitView.swift +++ b/Mail/Views/SplitView.swift @@ -89,6 +89,9 @@ struct SplitView: View { 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() @@ -164,4 +167,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/Thread/WebView.swift b/Mail/Views/Thread/WebView.swift index 301dce414..409a9008d 100644 --- a/Mail/Views/Thread/WebView.swift +++ b/Mail/Views/Thread/WebView.swift @@ -21,6 +21,8 @@ import SwiftUI import WebKit struct WebView: UIViewRepresentable { + @Environment(\.openURL) private var openUrl + @ObservedObject var model: WebViewModel let messageUid: String @@ -61,7 +63,7 @@ struct WebView: UIViewRepresentable { ) { if let url = navigationAction.request.url, Constants.isMailTo(url) { decisionHandler(.cancel) - //(parent.window?.windowScene?.delegate as? SceneDelegate)?.handleUrlOpen(url) + parent.openUrl.callAsFunction(url) return } From a8d7b72c1fce515d85b785935d5e3fe19acde233 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Thu, 29 Jun 2023 15:04:47 +0200 Subject: [PATCH 10/21] feat(SwiftUI App Lifecycle): NoMailboxView stop using SceneDelegate --- Mail/Views/NoMailboxView.swift | 17 +++++++++++------ Mail/Views/Switch User/AccountView.swift | 7 +------ Mail/Views/Switch User/AddMailboxView.swift | 10 ++-------- MailCore/Cache/AccountManager.swift | 8 ++++++-- 4 files changed, 20 insertions(+), 22 deletions(-) diff --git a/Mail/Views/NoMailboxView.swift b/Mail/Views/NoMailboxView.swift index e81bb2aec..54b806c8d 100644 --- a/Mail/Views/NoMailboxView.swift +++ b/Mail/Views/NoMailboxView.swift @@ -21,6 +21,8 @@ import MailResources import SwiftUI struct NoMailboxView: View { + @State private var isShowingAddMailboxView = false + @State private var isShowingLoginView = false let slide = Slide( id: 1, backgroundImage: MailResourcesAsset.onboardingBackground3.swiftUIImage, @@ -42,16 +44,12 @@ struct NoMailboxView: View { VStack(spacing: 24) { MailButton(icon: MailResourcesAsset.plus, label: MailResourcesStrings.Localizable.buttonAddEmailAddress) { - AddMailboxView { _ in - DispatchQueue.main.async { - // (window?.windowScene?.delegate as? SceneDelegate)?.showMainView() - } - } + isShowingAddMailboxView = true } .mailButtonFullWidth(true) MailButton(label: MailResourcesStrings.Localizable.buttonLogInDifferentAccount) { - // (window?.windowScene?.delegate as? SceneDelegate)?.showLoginView() + isShowingLoginView = true } .mailButtonStyle(.link) } @@ -59,6 +57,13 @@ struct NoMailboxView: View { .padding(.horizontal, 24) } .matomoView(view: ["NoMailboxView"]) + .sheet(isPresented: $isShowingAddMailboxView) { + AddMailboxView() + .sheetViewStyle() + } + .fullScreenCover(isPresented: $isShowingLoginView) { + OnboardingView(page: 4, isScrollEnabled: false) + } } } diff --git a/Mail/Views/Switch User/AccountView.swift b/Mail/Views/Switch User/AccountView.swift index 50914eaa8..e662881a4 100644 --- a/Mail/Views/Switch User/AccountView.swift +++ b/Mail/Views/Switch User/AccountView.swift @@ -92,12 +92,7 @@ struct AccountView: View { Spacer() NavigationLink { - AddMailboxView { mailbox in - DispatchQueue.main.async { - guard let mailbox else { return } - AccountManager.instance.switchMailbox(newMailbox: mailbox) - } - } + AddMailboxView() } label: { MailResourcesAsset.addCircle.swiftUIImage .resizable() 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/MailCore/Cache/AccountManager.swift b/MailCore/Cache/AccountManager.swift index 0cea3a8dd..853f9b61f 100644 --- a/MailCore/Cache/AccountManager.swift +++ b/MailCore/Cache/AccountManager.swift @@ -396,11 +396,15 @@ public class AccountManager: RefreshTokenDelegate, ObservableObject { } } - 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 setCurrentAccount(account: Account) { From abf4794111d08d4cd93f090af9015599659da810 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Thu, 29 Jun 2023 15:06:09 +0200 Subject: [PATCH 11/21] feat(SwiftUI App Lifecycle): Remove SceneDelegate --- Mail/SceneDelegate.swift | 189 --------------------------------------- 1 file changed, 189 deletions(-) delete mode 100644 Mail/SceneDelegate.swift diff --git a/Mail/SceneDelegate.swift b/Mail/SceneDelegate.swift deleted file mode 100644 index a70e4d351..000000000 --- a/Mail/SceneDelegate.swift +++ /dev/null @@ -1,189 +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 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. - (UIApplication.shared.delegate as? AppDelegate)?.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) - (UIApplication.shared.delegate as? AppDelegate)?.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 { - showNoMailboxView(animated: animated) - } - } - - func showLockView() { - setRootView(LockedAppView(), animated: false) - } - - // 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 - } -}*/ From 91c3404059e9050483f903fd876ea0c2cdd5f0a7 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Thu, 29 Jun 2023 15:11:35 +0200 Subject: [PATCH 12/21] feat(SwiftUI App Lifecycle): Make SonarCloud happy --- Mail/Views/Settings/SettingsOptionView.swift | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Mail/Views/Settings/SettingsOptionView.swift b/Mail/Views/Settings/SettingsOptionView.swift index eb038f6a9..5dfab17f2 100644 --- a/Mail/Views/Settings/SettingsOptionView.swift +++ b/Mail/Views/Settings/SettingsOptionView.swift @@ -38,15 +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 \.accentColor: - @AppStorage(UserDefaults.shared.key(.accentColor)) var accentColor = DefaultPreferences.accentColor - accentColor = UserDefaults.shared.accentColor + AppStorage(UserDefaults.shared.key(.accentColor)).wrappedValue = UserDefaults.shared.accentColor case \.theme: - @AppStorage(UserDefaults.shared.key(.theme)) var theme = DefaultPreferences.theme - theme = UserDefaults.shared.theme + AppStorage(UserDefaults.shared.key(.theme)).wrappedValue = UserDefaults.shared.theme default: break } From b48038f81c53a803676ebeddf9e12e757530592e Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Fri, 30 Jun 2023 09:16:37 +0200 Subject: [PATCH 13/21] feat(SwiftUI App Lifecycle): Use NavigationStore for editedDraft --- Mail/Utils/NavigationStore.swift | 1 + .../Search/SearchThreadsSectionView.swift | 5 ++-- Mail/Views/Search/SearchView.swift | 11 +++------ Mail/Views/SplitView.swift | 3 +++ Mail/Views/Thread List/ThreadListCell.swift | 5 +--- Mail/Views/Thread List/ThreadListView.swift | 10 -------- Mail/Views/ThreadListManagerView.swift | 24 +++---------------- 7 files changed, 14 insertions(+), 45 deletions(-) diff --git a/Mail/Utils/NavigationStore.swift b/Mail/Utils/NavigationStore.swift index 198a59d20..9ea299972 100644 --- a/Mail/Utils/NavigationStore.swift +++ b/Mail/Utils/NavigationStore.swift @@ -73,6 +73,7 @@ class NavigationStore: ObservableObject { @Published private(set) var rootViewState: RootViewState @Published var messageReply: MessageReply? + @Published var editedMessageDraft: Draft? /// Represents the state of navigation /// diff --git a/Mail/Views/Search/SearchThreadsSectionView.swift b/Mail/Views/Search/SearchThreadsSectionView.swift index 688788f4e..238b0ac51 100644 --- a/Mail/Views/Search/SearchThreadsSectionView.swift +++ b/Mail/Views/Search/SearchThreadsSectionView.swift @@ -21,10 +21,11 @@ import MailResources import SwiftUI struct SearchThreadsSectionView: View { + @EnvironmentObject private var navigationStore: NavigationStore + @AppStorage(UserDefaults.shared.key(.threadDensity)) private var threadDensity = DefaultPreferences.threadDensity let viewModel: SearchViewModel - @Binding var editedMessageDraft: Draft? var body: some View { Section { @@ -35,7 +36,7 @@ struct SearchThreadsSectionView: View { DraftUtils.editDraft( from: thread, mailboxManager: viewModel.mailboxManager, - editedMessageDraft: $editedMessageDraft + editedMessageDraft: $navigationStore.editedMessageDraft ) }, label: { ThreadCell(thread: thread, density: threadDensity) 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/SplitView.swift b/Mail/Views/SplitView.swift index aaf09c0f5..cd7e75add 100644 --- a/Mail/Views/SplitView.swift +++ b/Mail/Views/SplitView.swift @@ -83,6 +83,9 @@ struct SplitView: View { .sheet(item: $mailToURLComponents) { identifiableURLComponents in ComposeMessageView.mailTo(urlComponents: identifiableURLComponents.urlComponents, mailboxManager: mailboxManager) } + .sheet(item: $navigationStore.editedMessageDraft) { editedMessageDraft in + ComposeMessageView.edit(draft: editedMessageDraft, mailboxManager: mailboxManager) + } .onChange(of: scenePhase) { newScenePhase in guard newScenePhase == .active else { return } Task { diff --git a/Mail/Views/Thread List/ThreadListCell.swift b/Mail/Views/Thread List/ThreadListCell.swift index 41351c0d4..174598546 100644 --- a/Mail/Views/Thread List/ThreadListCell.swift +++ b/Mail/Views/Thread List/ThreadListCell.swift @@ -30,8 +30,6 @@ struct ThreadListCell: View { let viewModel: ThreadListViewModel @ObservedObject var multipleSelectionViewModel: ThreadListMultipleSelectionViewModel - @Binding var editedMessageDraft: Draft? - let thread: Thread let threadDensity: ThreadDensity @@ -71,7 +69,7 @@ struct ThreadListCell: View { DraftUtils.editDraft( from: thread, mailboxManager: viewModel.mailboxManager, - editedMessageDraft: $editedMessageDraft + editedMessageDraft: $navigationStore.editedMessageDraft ) } else { splitViewManager.splitViewController?.hide(.primary) @@ -105,7 +103,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/ThreadListView.swift b/Mail/Views/Thread List/ThreadListView.swift index 6b482d460..5828cfa70 100644 --- a/Mail/Views/Thread List/ThreadListView.swift +++ b/Mail/Views/Thread List/ThreadListView.swift @@ -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, @@ -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/ThreadListManagerView.swift b/Mail/Views/ThreadListManagerView.swift index 9c716f6ad..8976a636a 100644 --- a/Mail/Views/ThreadListManagerView.swift +++ b/Mail/Views/ThreadListManagerView.swift @@ -27,42 +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) } } From 5c5b1ce07984b135c9cd712fab6e56d6012303da Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Fri, 30 Jun 2023 10:14:56 +0200 Subject: [PATCH 14/21] feat(SwiftUI App Lifecycle): Same navigation for SearchView and ThreadListView --- .../Search/SearchThreadsSectionView.swift | 64 +++++++++---------- Mail/Views/SplitView.swift | 7 ++ Mail/Views/Thread List/ThreadListCell.swift | 5 +- 3 files changed, 38 insertions(+), 38 deletions(-) diff --git a/Mail/Views/Search/SearchThreadsSectionView.swift b/Mail/Views/Search/SearchThreadsSectionView.swift index 238b0ac51..eecda6aa7 100644 --- a/Mail/Views/Search/SearchThreadsSectionView.swift +++ b/Mail/Views/Search/SearchThreadsSectionView.swift @@ -22,6 +22,7 @@ import SwiftUI struct SearchThreadsSectionView: View { @EnvironmentObject private var navigationStore: NavigationStore + @EnvironmentObject private var splitViewManager: SplitViewManager @AppStorage(UserDefaults.shared.key(.threadDensity)) private var threadDensity = DefaultPreferences.threadDensity @@ -30,38 +31,19 @@ struct SearchThreadsSectionView: View { var body: some View { Section { ForEach(viewModel.threads) { thread in - Group { - if thread.shouldPresentAsDraft { - Button(action: { - DraftUtils.editDraft( - from: thread, - mailboxManager: viewModel.mailboxManager, - editedMessageDraft: $navigationStore.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 { @@ -73,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: $navigationStore.editedMessageDraft + ) + } else { + splitViewManager.adaptToProminentThreadView() + + // Update both viewModel and navigationStore on the truth. + viewModel.selectedThread = thread + navigationStore.threadPath = [thread] + } } } diff --git a/Mail/Views/SplitView.swift b/Mail/Views/SplitView.swift index cd7e75add..24a6d9c8f 100644 --- a/Mail/Views/SplitView.swift +++ b/Mail/Views/SplitView.swift @@ -30,6 +30,13 @@ public class SplitViewManager: ObservableObject { @Published var showSearch = false @Published var selectedFolder: Folder? var splitViewController: UISplitViewController? + + func adaptToProminentThreadView() { + splitViewController?.hide(.primary) + if splitViewController?.splitBehavior == .overlay { + splitViewController?.hide(.supplementary) + } + } } struct SplitView: View { diff --git a/Mail/Views/Thread List/ThreadListCell.swift b/Mail/Views/Thread List/ThreadListCell.swift index 174598546..a3307ba4e 100644 --- a/Mail/Views/Thread List/ThreadListCell.swift +++ b/Mail/Views/Thread List/ThreadListCell.swift @@ -72,10 +72,7 @@ struct ThreadListCell: View { editedMessageDraft: $navigationStore.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. viewModel.selectedThread = thread From 9ab89195b57379ad0dfac02abea6513a178c91b8 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Fri, 30 Jun 2023 10:55:52 +0200 Subject: [PATCH 15/21] feat(SwiftUI App Lifecycle): PR feedback --- Mail/RootView.swift | 6 +++--- Mail/Views/Thread/WebView.swift | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Mail/RootView.swift b/Mail/RootView.swift index e745a2b14..447867559 100644 --- a/Mail/RootView.swift +++ b/Mail/RootView.swift @@ -19,10 +19,10 @@ import SwiftUI struct RootView: View { - @Environment(\.horizontalSizeClass) var horizontalSizeClass - @Environment(\.verticalSizeClass) var verticalSizeClass + @Environment(\.horizontalSizeClass) private var horizontalSizeClass + @Environment(\.verticalSizeClass) private var verticalSizeClass - @EnvironmentObject var navigationStore: NavigationStore + @EnvironmentObject private var navigationStore: NavigationStore var body: some View { ZStack { diff --git a/Mail/Views/Thread/WebView.swift b/Mail/Views/Thread/WebView.swift index 409a9008d..21f3266d3 100644 --- a/Mail/Views/Thread/WebView.swift +++ b/Mail/Views/Thread/WebView.swift @@ -63,7 +63,7 @@ struct WebView: UIViewRepresentable { ) { if let url = navigationAction.request.url, Constants.isMailTo(url) { decisionHandler(.cancel) - parent.openUrl.callAsFunction(url) + parent.openUrl(url) return } From 5ee2dfc95808ac4fe5792030e476043ff5b83145 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Tue, 4 Jul 2023 13:58:02 +0200 Subject: [PATCH 16/21] feat(SwiftUI App Lifecycle): Prevent initial lock --- Mail/Views/Settings/SettingsToggleCell.swift | 8 ++++++++ Mail/Views/SplitView.swift | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) 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/SplitView.swift b/Mail/Views/SplitView.swift index 24a6d9c8f..466bb88a6 100644 --- a/Mail/Views/SplitView.swift +++ b/Mail/Views/SplitView.swift @@ -30,7 +30,7 @@ public class SplitViewManager: ObservableObject { @Published var showSearch = false @Published var selectedFolder: Folder? var splitViewController: UISplitViewController? - + func adaptToProminentThreadView() { splitViewController?.hide(.primary) if splitViewController?.splitBehavior == .overlay { From d93b65fd8c2533b0ef65b4c2018b0eb6f2655a4c Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Thu, 6 Jul 2023 14:50:25 +0200 Subject: [PATCH 17/21] feat(SwiftUI App Lifecycle): Restore orientation lock behaviour --- Mail/Views/Onboarding/OnboardingView.swift | 4 ++-- Mail/Views/Switch User/AccountListView.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Mail/Views/Onboarding/OnboardingView.swift b/Mail/Views/Onboarding/OnboardingView.swift index 870638cfe..4e4b3838d 100644 --- a/Mail/Views/Onboarding/OnboardingView.swift +++ b/Mail/Views/Onboarding/OnboardingView.swift @@ -128,7 +128,7 @@ class LoginHandler: InfomaniakLoginDelegate, ObservableObject { _ = try await AccountManager.instance.createAndSetCurrentAccount(code: code, codeVerifier: verifier) UIApplication.shared.registerForRemoteNotifications() } catch let error as MailError where error == MailError.noMailbox { - //sceneDelegate?.showNoMailboxView() + // sceneDelegate?.showNoMailboxView() } catch { if let previousAccount = previousAccount { AccountManager.instance.switchAccount(newAccount: previousAccount) @@ -241,7 +241,7 @@ struct OnboardingView: View { if UIDevice.current.userInterfaceIdiom == .phone { UIDevice.current .setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation") - // AppDelegate.orientationLock = .portrait + AppDelegate.orientationLock = .portrait UIViewController.attemptRotationToDeviceOrientation() } } diff --git a/Mail/Views/Switch User/AccountListView.swift b/Mail/Views/Switch User/AccountListView.swift index 98648fad3..8e309e603 100644 --- a/Mail/Views/Switch User/AccountListView.swift +++ b/Mail/Views/Switch User/AccountListView.swift @@ -79,7 +79,7 @@ struct AccountListView: View { isShowingNewAccountView = true } .fullScreenCover(isPresented: $isShowingNewAccountView, onDismiss: { - // AppDelegate.orientationLock = .all + AppDelegate.orientationLock = .all }, content: { OnboardingView(page: 4, isScrollEnabled: false) }) From c80acaea84eeded5ad33befc30bcf2ed8a22adeb Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Fri, 7 Jul 2023 09:13:39 +0200 Subject: [PATCH 18/21] feat(SwiftUI App Lifecycle): Rename navigationStore to navigationState --- Mail/LockedAppView.swift | 2 +- Mail/MailApp.swift | 8 ++++---- Mail/RootView.swift | 4 ++-- .../{NavigationStore.swift => NavigationState.swift} | 2 +- .../Actions/ActionsPanelViewModifier.swift | 4 ++-- Mail/Views/Search/SearchThreadsSectionView.swift | 8 ++++---- Mail/Views/SplitView.swift | 12 ++++++------ Mail/Views/Thread List/ThreadListCell.swift | 8 ++++---- Mail/Views/Thread List/ThreadListModifiers.swift | 2 +- Mail/Views/Thread List/ThreadListView.swift | 6 +++--- Mail/Views/Thread/MessageHeaderSummaryView.swift | 6 +++--- Mail/Views/Thread/MessageHeaderView.swift | 2 +- Mail/Views/Thread/ThreadView.swift | 8 ++++---- 13 files changed, 36 insertions(+), 36 deletions(-) rename Mail/Utils/{NavigationStore.swift => NavigationState.swift} (98%) diff --git a/Mail/LockedAppView.swift b/Mail/LockedAppView.swift index 5523b70dd..582dcb2bc 100644 --- a/Mail/LockedAppView.swift +++ b/Mail/LockedAppView.swift @@ -25,7 +25,7 @@ import SwiftUI struct LockedAppView: View { @LazyInjectService var appLockHelper: AppLockHelper - @EnvironmentObject var navigationState: NavigationStore + @EnvironmentObject var navigationState: NavigationState var body: some View { ZStack { diff --git a/Mail/MailApp.swift b/Mail/MailApp.swift index e11e91e49..2f8c5c2f1 100644 --- a/Mail/MailApp.swift +++ b/Mail/MailApp.swift @@ -88,7 +88,7 @@ struct MailApp: App { @AppStorage(UserDefaults.shared.key(.accentColor)) private var accentColor = DefaultPreferences.accentColor @AppStorage(UserDefaults.shared.key(.theme)) private var theme = DefaultPreferences.theme - @StateObject private var navigationStore = NavigationStore() + @StateObject private var navigationState = NavigationState() private let accountManager = AccountManager.instance @@ -101,7 +101,7 @@ struct MailApp: App { var body: some Scene { WindowGroup { RootView() - .environmentObject(navigationStore) + .environmentObject(navigationState) .onAppear { updateUI(accent: accentColor, theme: theme) } @@ -115,9 +115,9 @@ struct MailApp: App { switch newScenePhase { case .active: refreshCacheData() - navigationStore.transitionToLockViewIfNeeded() + navigationState.transitionToLockViewIfNeeded() case .background: - if UserDefaults.shared.isAppLockEnabled && navigationStore.rootViewState != .appLocked { + if UserDefaults.shared.isAppLockEnabled && navigationState.rootViewState != .appLocked { appLockHelper.setTime() } case .inactive: diff --git a/Mail/RootView.swift b/Mail/RootView.swift index 447867559..5944efe51 100644 --- a/Mail/RootView.swift +++ b/Mail/RootView.swift @@ -22,11 +22,11 @@ struct RootView: View { @Environment(\.horizontalSizeClass) private var horizontalSizeClass @Environment(\.verticalSizeClass) private var verticalSizeClass - @EnvironmentObject private var navigationStore: NavigationStore + @EnvironmentObject private var navigationState: NavigationState var body: some View { ZStack { - switch navigationStore.rootViewState { + switch navigationState.rootViewState { case .appLocked: LockedAppView() case .mainView(let currentMailboxManager): diff --git a/Mail/Utils/NavigationStore.swift b/Mail/Utils/NavigationState.swift similarity index 98% rename from Mail/Utils/NavigationStore.swift rename to Mail/Utils/NavigationState.swift index 9ea299972..2e6dac8eb 100644 --- a/Mail/Utils/NavigationStore.swift +++ b/Mail/Utils/NavigationState.swift @@ -65,7 +65,7 @@ enum RootViewDestination { @MainActor /// Something that represents the state of navigation -class NavigationStore: ObservableObject { +class NavigationState: ObservableObject { @LazyInjectService private var appLockHelper: AppLockHelper private let accountManager = AccountManager.instance 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/Search/SearchThreadsSectionView.swift b/Mail/Views/Search/SearchThreadsSectionView.swift index eecda6aa7..30141c7ec 100644 --- a/Mail/Views/Search/SearchThreadsSectionView.swift +++ b/Mail/Views/Search/SearchThreadsSectionView.swift @@ -21,7 +21,7 @@ import MailResources import SwiftUI struct SearchThreadsSectionView: View { - @EnvironmentObject private var navigationStore: NavigationStore + @EnvironmentObject private var navigationState: NavigationState @EnvironmentObject private var splitViewManager: SplitViewManager @AppStorage(UserDefaults.shared.key(.threadDensity)) private var threadDensity = DefaultPreferences.threadDensity @@ -65,14 +65,14 @@ struct SearchThreadsSectionView: View { DraftUtils.editDraft( from: thread, mailboxManager: viewModel.mailboxManager, - editedMessageDraft: $navigationStore.editedMessageDraft + editedMessageDraft: $navigationState.editedMessageDraft ) } else { 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] } } } diff --git a/Mail/Views/SplitView.swift b/Mail/Views/SplitView.swift index 466bb88a6..5b1b971d3 100644 --- a/Mail/Views/SplitView.swift +++ b/Mail/Views/SplitView.swift @@ -43,7 +43,7 @@ struct SplitView: View { @Environment(\.isCompactWindow) private var isCompactWindow @Environment(\.scenePhase) private var scenePhase - @EnvironmentObject private var navigationStore: NavigationStore + @EnvironmentObject private var navigationState: NavigationState @State private var splitViewController: UISplitViewController? @State private var mailToURLComponents: IdentifiableURLComponents? @@ -57,7 +57,7 @@ struct SplitView: View { Group { if isCompactWindow { ZStack { - NBNavigationStack(path: $navigationStore.threadPath) { + NBNavigationStack(path: $navigationState.threadPath) { ThreadListManagerView() .accessibilityHidden(navigationDrawerController.isOpen) .nbNavigationDestination(for: Thread.self) { thread in @@ -76,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) @@ -84,13 +84,13 @@ 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) } - .sheet(item: $navigationStore.editedMessageDraft) { editedMessageDraft in + .sheet(item: $navigationState.editedMessageDraft) { editedMessageDraft in ComposeMessageView.edit(draft: editedMessageDraft, mailboxManager: mailboxManager) } .onChange(of: scenePhase) { newScenePhase in @@ -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) } diff --git a/Mail/Views/Thread List/ThreadListCell.swift b/Mail/Views/Thread List/ThreadListCell.swift index a3307ba4e..3a96d4068 100644 --- a/Mail/Views/Thread List/ThreadListCell.swift +++ b/Mail/Views/Thread List/ThreadListCell.swift @@ -25,7 +25,7 @@ import SwiftUI struct ThreadListCell: View { @EnvironmentObject var splitViewManager: SplitViewManager - @EnvironmentObject var navigationStore: NavigationStore + @EnvironmentObject var navigationState: NavigationState let viewModel: ThreadListViewModel @ObservedObject var multipleSelectionViewModel: ThreadListMultipleSelectionViewModel @@ -69,14 +69,14 @@ struct ThreadListCell: View { DraftUtils.editDraft( from: thread, mailboxManager: viewModel.mailboxManager, - editedMessageDraft: $navigationStore.editedMessageDraft + editedMessageDraft: $navigationState.editedMessageDraft ) } else { 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] } } } diff --git a/Mail/Views/Thread List/ThreadListModifiers.swift b/Mail/Views/Thread List/ThreadListModifiers.swift index 71e0711a5..3ba93ddc5 100644 --- a/Mail/Views/Thread List/ThreadListModifiers.swift +++ b/Mail/Views/Thread List/ThreadListModifiers.swift @@ -58,7 +58,7 @@ struct ThreadListToolbar: ViewModifier { @EnvironmentObject private var splitViewManager: SplitViewManager @EnvironmentObject private var navigationDrawerState: NavigationDrawerState - @EnvironmentObject private var navigationStore: NavigationStore + @EnvironmentObject private var navigationState: NavigationState @State private var isShowingSwitchAccount = false @State private var multipleSelectionActionsTarget: ActionsTarget? diff --git a/Mail/Views/Thread List/ThreadListView.swift b/Mail/Views/Thread List/ThreadListView.swift index 5828cfa70..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 @@ -223,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 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 { From 4a3209bd41af99313ef0e3db07ea4cee2d6f1a5f Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Fri, 7 Jul 2023 10:34:31 +0200 Subject: [PATCH 19/21] feat(SwiftUI App Lifecycle): AccountView remove duplicated view after bad merge --- Mail/Views/Switch User/AccountView.swift | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/Mail/Views/Switch User/AccountView.swift b/Mail/Views/Switch User/AccountView.swift index 14c0cd0b4..b30d28f8f 100644 --- a/Mail/Views/Switch User/AccountView.swift +++ b/Mail/Views/Switch User/AccountView.swift @@ -79,29 +79,7 @@ struct AccountView: View { } } - // Email list - VStack(alignment: .leading, spacing: 12) { - HStack(alignment: .center) { - Text(MailResourcesStrings.Localizable.buttonAccountAssociatedEmailAddresses) - .textStyle(.bodySmallSecondary) - - Spacer() - - NavigationLink { - AddMailboxView() - } label: { - MailResourcesAsset.addCircle.swiftUIImage - .resizable() - .foregroundColor(accentColor.primary) - .frame(width: 16, height: 16) - } - } - .padding(.bottom, 16) - - MailboxListView(currentMailbox: mailboxManager.mailbox) - } - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.top, 24) + MailboxListView(currentMailbox: mailboxManager.mailbox) Spacer() } From b889e626d53d206bee53398a9c1ad39a18a1d94d Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Fri, 7 Jul 2023 13:22:24 +0200 Subject: [PATCH 20/21] feat(SwiftUI App Lifecycle): NoMailboxView | UnavailableMailboxesView --- Mail/Components/MailboxListView.swift | 2 -- .../UnavailableMailboxListView.swift | 2 -- Mail/RootView.swift | 4 ++++ Mail/Utils/Environment+Extension.swift | 11 ---------- Mail/Utils/NavigationState.swift | 20 +++++++++++++++++++ Mail/Views/NoMailboxView.swift | 3 --- Mail/Views/Onboarding/OnboardingView.swift | 14 ++++++++++--- .../UnavailableMailboxesView.swift | 10 ++++------ .../UpdateMailboxPasswordView.swift | 4 ---- 9 files changed, 39 insertions(+), 31 deletions(-) diff --git a/Mail/Components/MailboxListView.swift b/Mail/Components/MailboxListView.swift index 81dc8b4b4..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( diff --git a/Mail/Components/UnavailableMailboxListView.swift b/Mail/Components/UnavailableMailboxListView.swift index 9b73df307..13870f69e 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/RootView.swift b/Mail/RootView.swift index 5944efe51..6fc7891a7 100644 --- a/Mail/RootView.swift +++ b/Mail/RootView.swift @@ -33,6 +33,10 @@ struct RootView: View { SplitView(mailboxManager: currentMailboxManager) case .onboarding: OnboardingView() + case .noMailboxes: + NoMailboxView() + case .unavailableMailboxes: + UnavailableMailboxesView() } } .environment(\.isCompactWindow, horizontalSizeClass == .compact || verticalSizeClass == .compact) 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 index 2e6dac8eb..d082ac9c4 100644 --- a/Mail/Utils/NavigationState.swift +++ b/Mail/Utils/NavigationState.swift @@ -34,6 +34,10 @@ enum RootViewState: Equatable, Hashable, Identifiable { 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: @@ -49,18 +53,26 @@ enum RootViewState: Equatable, Hashable, Identifiable { 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 @@ -84,6 +96,8 @@ class NavigationState: ObservableObject { 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 } @@ -102,6 +116,10 @@ class NavigationState: ObservableObject { switchToCurrentMailboxManagerIfPossible() case .onboarding: rootViewState = .onboarding + case .noMailboxes: + rootViewState = .noMailboxes + case .unavailableMailboxes: + rootViewState = .unavailableMailboxes } } } @@ -120,6 +138,8 @@ class NavigationState: ObservableObject { 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/Views/NoMailboxView.swift b/Mail/Views/NoMailboxView.swift index 5bd9d9455..54b806c8d 100644 --- a/Mail/Views/NoMailboxView.swift +++ b/Mail/Views/NoMailboxView.swift @@ -56,9 +56,6 @@ struct NoMailboxView: View { .frame(height: UIConstants.onboardingButtonHeight + UIConstants.onboardingBottomButtonPadding, alignment: .top) .padding(.horizontal, 24) } - .sheet(isPresented: $isShowingAddMailboxView) { - AddMailboxView() - } .matomoView(view: ["NoMailboxView"]) .sheet(isPresented: $isShowingAddMailboxView) { AddMailboxView() diff --git a/Mail/Views/Onboarding/OnboardingView.swift b/Mail/Views/Onboarding/OnboardingView.swift index 4e4b3838d..2c460c373 100644 --- a/Mail/Views/Onboarding/OnboardingView.swift +++ b/Mail/Views/Onboarding/OnboardingView.swift @@ -74,6 +74,7 @@ class LoginHandler: InfomaniakLoginDelegate, ObservableObject { @Published var isLoading = false @Published var isPresentingErrorAlert = false + @Published var shouldShowEmptyMailboxesView = false nonisolated func didCompleteLoginWith(code: String, verifier: String) { Task { @@ -128,7 +129,7 @@ class LoginHandler: InfomaniakLoginDelegate, ObservableObject { _ = try await AccountManager.instance.createAndSetCurrentAccount(code: code, codeVerifier: verifier) 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,6 +150,8 @@ class LoginHandler: InfomaniakLoginDelegate, ObservableObject { struct OnboardingView: View { @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 @@ -167,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) { @@ -245,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/Unavailable Mailbox/UnavailableMailboxesView.swift b/Mail/Views/Unavailable Mailbox/UnavailableMailboxesView.swift index 6062d0bf9..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,12 +56,12 @@ struct UnavailableMailboxesView: View { } Spacer() - NavigationLink(isActive: $showAddMailbox) { + 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 0beded17c..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 } From b1ce19386d6b1f584f92c792ce2fdac0df54b21b Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Fri, 7 Jul 2023 13:38:51 +0200 Subject: [PATCH 21/21] feat(SwiftUI App Lifecycle): Fix disconnect crash --- Mail/Views/Switch User/AccountView.swift | 5 +++-- Mail/Views/Thread List/ThreadListModifiers.swift | 13 ++++++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Mail/Views/Switch User/AccountView.swift b/Mail/Views/Switch User/AccountView.swift index b30d28f8f..e885de801 100644 --- a/Mail/Views/Switch User/AccountView.swift +++ b/Mail/Views/Switch User/AccountView.swift @@ -50,11 +50,12 @@ struct AccountView: View { @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 { VStack(spacing: 0) { ScrollView { @@ -114,6 +115,6 @@ struct AccountView: View { struct AccountView_Previews: PreviewProvider { static var previews: some View { - AccountView() + AccountView(account: PreviewHelper.sampleAccount) } } diff --git a/Mail/Views/Thread List/ThreadListModifiers.swift b/Mail/Views/Thread List/ThreadListModifiers.swift index 3ba93ddc5..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 @@ -60,7 +61,7 @@ struct ThreadListToolbar: ViewModifier { @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? @@ -122,13 +123,15 @@ 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(isPresented: $isShowingSwitchAccount) { - AccountView() + .sheet(item: $presentedCurrentAccount) { account in + AccountView(account: account) } } }