From 67b56dae19c818eb4f389208039c1953ce240773 Mon Sep 17 00:00:00 2001 From: mormaer Date: Sat, 9 Sep 2023 10:50:30 +0100 Subject: [PATCH] introduce `AppFlow` to support different application flows --- Mlem.xcodeproj/project.pbxproj | 12 ++- Mlem/API/APIClient/APIClient.swift | 17 +++- Mlem/App State.swift | 55 +++++------ Mlem/AppDelegate.swift | 3 + Mlem/AppFlow.swift | 17 ++++ Mlem/ContentView.swift | 51 +++++----- .../Environment - Force Onboard.swift | 20 ---- Mlem/Extensions/Environment+AppFlow.swift | 20 ++++ Mlem/MlemApp.swift | 14 ++- .../Trackers/Favorite Community Tracker.swift | 15 ++- .../Trackers/Saved Account Tracker.swift | 15 +-- .../Views/Shared/Accounts/Accounts Page.swift | 92 +++++++++++-------- .../Shared/Accounts/Add Account View.swift | 16 +--- .../Shared/Accounts/DeleteAccountView.swift | 37 +++++--- .../Shared/Accounts/Onboarding View.swift | 10 +- .../Shared/Comments/Comment Item Logic.swift | 4 +- .../Shared/Posts/ExpandedPostLogic.swift | 2 +- Mlem/Views/Shared/Posts/Feed Post.swift | 2 +- Mlem/Views/Tabs/Feeds/Feed Root.swift | 3 +- Mlem/Views/Tabs/Inbox/Inbox View.swift | 7 +- Mlem/Views/Tabs/Profile/User View.swift | 2 +- .../TabBar/TabBarSettingsView.swift | 16 +++- .../Views/General/GeneralSettingsView.swift | 2 +- Mlem/Window.swift | 51 +++++----- 24 files changed, 268 insertions(+), 215 deletions(-) create mode 100644 Mlem/AppFlow.swift delete mode 100644 Mlem/Extensions/Environment - Force Onboard.swift create mode 100644 Mlem/Extensions/Environment+AppFlow.swift diff --git a/Mlem.xcodeproj/project.pbxproj b/Mlem.xcodeproj/project.pbxproj index 1ce8c92a3..3b1240c0a 100644 --- a/Mlem.xcodeproj/project.pbxproj +++ b/Mlem.xcodeproj/project.pbxproj @@ -34,6 +34,8 @@ 500C168E2A66FAAB006F243B /* HapticManager+Dependency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 500C168D2A66FAAB006F243B /* HapticManager+Dependency.swift */; }; 5016A2B12A67EB8600B257E8 /* UIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5016A2B02A67EB8600B257E8 /* UIViewController.swift */; }; 5016A2B32A67EC0700B257E8 /* NotificationDisplayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5016A2B22A67EC0700B257E8 /* NotificationDisplayer.swift */; }; + 503422562AAB784000EFE88D /* Environment+AppFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 503422552AAB784000EFE88D /* Environment+AppFlow.swift */; }; + 503422582AAB798600EFE88D /* AppFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 503422572AAB798600EFE88D /* AppFlow.swift */; }; 503A5D752A78EF3C00488C38 /* Encodable+Export.swift in Sources */ = {isa = PBXBuildFile; fileRef = 503A5D742A78EF3C00488C38 /* Encodable+Export.swift */; }; 503BA26F2A2C94540052516C /* URL+Identifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 503BA26E2A2C94540052516C /* URL+Identifiable.swift */; }; 504106CD2A744D7F000AAEF8 /* CommentRepository+Dependency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504106CC2A744D7F000AAEF8 /* CommentRepository+Dependency.swift */; }; @@ -385,7 +387,6 @@ CDE6A8162A490AE00062D161 /* Inbox Message View.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDE6A8152A490AE00062D161 /* Inbox Message View.swift */; }; CDE6A8182A490AF20062D161 /* Inbox Mention View.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDE6A8172A490AF20062D161 /* Inbox Mention View.swift */; }; CDE6A81A2A490B970062D161 /* Inbox Reply View.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDE6A8192A490B970062D161 /* Inbox Reply View.swift */; }; - CDE8F2392A68DA7D00E0AE68 /* Environment - Force Onboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDE8F2382A68DA7D00E0AE68 /* Environment - Force Onboard.swift */; }; CDE9CE4C2A7B0831002B97DD /* Gentle Info.ahap in Resources */ = {isa = PBXBuildFile; fileRef = CDE9CE4B2A7B0831002B97DD /* Gentle Info.ahap */; }; CDE9CE4F2A7B0B1B002B97DD /* Haptic.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDE9CE4E2A7B0B1B002B97DD /* Haptic.swift */; }; CDE9CE512A7B0C66002B97DD /* Firmer Info.ahap in Resources */ = {isa = PBXBuildFile; fileRef = CDE9CE502A7B0C66002B97DD /* Firmer Info.ahap */; }; @@ -469,6 +470,8 @@ 500C168D2A66FAAB006F243B /* HapticManager+Dependency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HapticManager+Dependency.swift"; sourceTree = ""; }; 5016A2B02A67EB8600B257E8 /* UIViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewController.swift; sourceTree = ""; }; 5016A2B22A67EC0700B257E8 /* NotificationDisplayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationDisplayer.swift; sourceTree = ""; }; + 503422552AAB784000EFE88D /* Environment+AppFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Environment+AppFlow.swift"; sourceTree = ""; }; + 503422572AAB798600EFE88D /* AppFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppFlow.swift; sourceTree = ""; }; 503A5D742A78EF3C00488C38 /* Encodable+Export.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Encodable+Export.swift"; sourceTree = ""; }; 503BA26E2A2C94540052516C /* URL+Identifiable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Identifiable.swift"; sourceTree = ""; }; 504106CC2A744D7F000AAEF8 /* CommentRepository+Dependency.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CommentRepository+Dependency.swift"; sourceTree = ""; }; @@ -818,7 +821,6 @@ CDE6A8152A490AE00062D161 /* Inbox Message View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Inbox Message View.swift"; sourceTree = ""; }; CDE6A8172A490AF20062D161 /* Inbox Mention View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Inbox Mention View.swift"; sourceTree = ""; }; CDE6A8192A490B970062D161 /* Inbox Reply View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Inbox Reply View.swift"; sourceTree = ""; }; - CDE8F2382A68DA7D00E0AE68 /* Environment - Force Onboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Environment - Force Onboard.swift"; sourceTree = ""; }; CDE9CE4B2A7B0831002B97DD /* Gentle Info.ahap */ = {isa = PBXFileReference; lastKnownFileType = text; path = "Gentle Info.ahap"; sourceTree = ""; }; CDE9CE4E2A7B0B1B002B97DD /* Haptic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Haptic.swift; sourceTree = ""; }; CDE9CE502A7B0C66002B97DD /* Firmer Info.ahap */ = {isa = PBXFileReference; lastKnownFileType = text; path = "Firmer Info.ahap"; sourceTree = ""; }; @@ -1285,7 +1287,6 @@ E4DDB4312A81819300B3A7E0 /* Double.swift */, 503A5D742A78EF3C00488C38 /* Encodable+Export.swift */, B1955A202A6145C00056CF99 /* Environment - EasterFlagSetter.swift */, - CDE8F2382A68DA7D00E0AE68 /* Environment - Force Onboard.swift */, 507573902A5AD53C00AA7ABD /* Error+Equatable.swift */, 6D8601ED2A43C0B1002A56FC /* Image.swift */, 6DA61F842A568F99001EA633 /* Int.swift */, @@ -1302,6 +1303,7 @@ 503BA26E2A2C94540052516C /* URL+Identifiable.swift */, 63F0C7BA2A058CB700A18C5D /* URLSessionWebSocketTask - Send Ping.swift */, 50CC4A712A9CB07F0074C845 /* TimeInterval+Period.swift */, + 503422552AAB784000EFE88D /* Environment+AppFlow.swift */, ); path = Extensions; sourceTree = ""; @@ -1417,6 +1419,7 @@ 6363D5C427EE196700E34822 /* MlemApp.swift */, B157E0C32A507B8000B02C8B /* Window.swift */, 6363D5C627EE196700E34822 /* ContentView.swift */, + 503422572AAB798600EFE88D /* AppFlow.swift */, 6386E02B2A03D1EC006B3C1D /* App State.swift */, 63DF71F02A02999C002AC14E /* App Constants.swift */, B1B78D632A51D53900F72485 /* AppDelegate.swift */, @@ -2486,7 +2489,6 @@ 637218752A3A2AAD008C4816 /* GetCommunity.swift in Sources */, CDF1EF142A6B6D6E003594B6 /* Feed View Logic.swift in Sources */, 6DFF50432A48DED3001E648D /* Inbox View.swift in Sources */, - CDE8F2392A68DA7D00E0AE68 /* Environment - Force Onboard.swift in Sources */, CDF8426F2A4A385A00723DA0 /* Inbox Item Type.swift in Sources */, CD1446232A5B336900610EF1 /* LicensesView.swift in Sources */, CDDCF6432A66343D003DA3AC /* FancyTabBar.swift in Sources */, @@ -2499,6 +2501,7 @@ 6372186F2A3A2AAD008C4816 /* SearchRequest.swift in Sources */, CD05E7812A4F7A4B0081D102 /* Inbox Tracker.swift in Sources */, 50811B362A920519006BA3F2 /* APISite+Mock.swift in Sources */, + 503422562AAB784000EFE88D /* Environment+AppFlow.swift in Sources */, CDDCF6512A677E1B003DA3AC /* FancyTabItemPreferenceKeys.swift in Sources */, CDEBC32A2A9A580B00518D9D /* Post Model.swift in Sources */, CDC6A8CA2A6F1C8D00CC11AC /* AssociatedIconProtocol.swift in Sources */, @@ -2523,6 +2526,7 @@ 6317ABCB2A37292700603D76 /* FeedType.swift in Sources */, CDC65D8F2A86B6DD007205E5 /* DeleteUser.swift in Sources */, CD6483382A3A0F2200EE6CA3 /* NSFW Tag.swift in Sources */, + 503422582AAB798600EFE88D /* AppFlow.swift in Sources */, 637218642A3A2AAD008C4816 /* GetPost.swift in Sources */, 63E5D3942A13CF3600EC1FBD /* Favorite Community.swift in Sources */, CD4E98A12A69BE980026C4D9 /* AlternativeIconCell.swift in Sources */, diff --git a/Mlem/API/APIClient/APIClient.swift b/Mlem/API/APIClient/APIClient.swift index d6e5f180e..4201934de 100644 --- a/Mlem/API/APIClient/APIClient.swift +++ b/Mlem/API/APIClient/APIClient.swift @@ -57,10 +57,19 @@ class APIClient { // MARK: - Public methods - /// Configures the clients session based on the passed in account - /// - Parameter account: a `SavedAccount` to use when configuring the clients session - func configure(for account: SavedAccount) { - session = .authenticated(account.instanceLink, account.accessToken) + /// Configures the clients session based on the passed in flow + /// - Parameter flow: The application flow which the client should be configured for + func configure(for flow: AppFlow) { + switch flow { + case let .account(account): + session = .authenticated(account.instanceLink, account.accessToken) + case .onboarding: + // no calls to our `APIClient` should be made during onboarding + // excluding a _login_ call which requires an explicit session to be provided + // setting to `.undefined` here ensures that errors will be throw should a call + // be attempted + session = .undefined + } } @discardableResult diff --git a/Mlem/App State.swift b/Mlem/App State.swift index 01f4bc3d3..d03aa5a60 100644 --- a/Mlem/App State.swift +++ b/Mlem/App State.swift @@ -13,47 +13,36 @@ class AppState: ObservableObject { @Dependency(\.apiClient) var apiClient @AppStorage("defaultAccountId") var defaultAccountId: Int? - @Binding private var selectedAccount: SavedAccount? - @Published private(set) var currentActiveAccount: SavedAccount - @Published private(set) var currentNickname: String - /// Initialises our app state - /// - Parameters: - /// - defaultAccount: The account the application should start with - /// - selectedAccount: A `Binding` to the selected account at the `Window` level - init(defaultAccount: SavedAccount, selectedAccount: Binding) { - _selectedAccount = selectedAccount - self.currentActiveAccount = defaultAccount - self.currentNickname = defaultAccount.nickname - self.defaultAccountId = currentActiveAccount.id - accountUpdated() - } + @Published private(set) var currentActiveAccount: SavedAccount? + @Published private(set) var currentNickname: String? + /// A method to set the current active account + /// - Important: If you wish to _clear_ the current active account please use the `\.setAppFlow` method available via the environment to reset to our `.onboarding` flow + /// - Parameter account: The `SavedAccount` which should become the active account func setActiveAccount(_ account: SavedAccount) { - // update our stored token and set the account... AppConstants.keychain["\(account.id)_accessToken"] = account.accessToken + // we configure the client here to ensure any updated session tokens are updated + apiClient.configure(for: .account(account)) currentActiveAccount = account - defaultAccountId = currentActiveAccount.id - - // if the account we just set is not the existing one from the session - // then the user is switching accounts, so we pass the value up to the - // `Window` layer which will re-create our `ContentView` and the new - // account will restart on the feed page with a clean slate - if account.id != selectedAccount?.id { - selectedAccount = account - return - } - - accountUpdated() + currentNickname = account.nickname + defaultAccountId = account.id } - /// Update the nickname. This is needed to quickly propagate changes from settings over to the tab bar, since nickname doesn't affect account identity and so changing it doesn't always prompt redraws - func changeDisplayedNickname(to nickname: String) { - currentNickname = nickname + /// A method to clear the currentlly active account + /// - Important: It is unlikely you will want to call this method directly but instead use the `\.setAppFlow` method available via the environment + func clearActiveAccount() { + currentActiveAccount = nil + currentNickname = nil } - private func accountUpdated() { - // ensure our client session is updated - apiClient.configure(for: currentActiveAccount) + func isCurrentAccountId(_ id: Int) -> Bool { + guard let currentActiveAccount else { return false } + // TODO: we likely need to improve this check as comparing just the id might not be enough (same id, different instances) + // I'm going to leave this for now as if we wanted to move to using a value like `.actorId` then we'll need to + // to start storing it in the `SavedAccount` object first etc, which is getting well outside the scope of this PR... + // although the _check_ has moved in this PR, it's performing the same check that was being done elsewhere so there + // should be no regression introduced by only checking the `.id` + return currentActiveAccount.id == id } } diff --git a/Mlem/AppDelegate.swift b/Mlem/AppDelegate.swift index 11031c648..b953226ed 100644 --- a/Mlem/AppDelegate.swift +++ b/Mlem/AppDelegate.swift @@ -9,6 +9,9 @@ import Foundation import SwiftUI import UIKit +// TODO: we need to do a bit of work to ensure we also switch tab when responding to these +// as currently it launches you into the app, but if the app was already running you're left +// on the tab/screen you were on - despite the shortcuts being design to take you to the feeds var shortcutItemToProcess: UIApplicationShortcutItem? class AppDelegate: UIResponder, UIApplicationDelegate, UIWindowSceneDelegate { diff --git a/Mlem/AppFlow.swift b/Mlem/AppFlow.swift new file mode 100644 index 000000000..89f0193ba --- /dev/null +++ b/Mlem/AppFlow.swift @@ -0,0 +1,17 @@ +// +// AppFlow.swift +// Mlem +// +// Created by mormaer on 08/09/2023. +// +// + +import Foundation + +/// An enumeration that describes the types of flow that are supported by the application +enum AppFlow: Equatable { + /// The onboarding flow + case onboarding + /// An 'signed in'' session with the users `SavedAccount` as an associated value + case account(SavedAccount) +} diff --git a/Mlem/ContentView.swift b/Mlem/ContentView.swift index d6d44a202..98ae226f4 100644 --- a/Mlem/ContentView.swift +++ b/Mlem/ContentView.swift @@ -47,26 +47,33 @@ struct ContentView: View { activeSymbolName: "scroll.fill" ) } - InboxView() - .fancyTabItem(tag: TabSelection.inbox) { - FancyTabBarLabel( - tag: TabSelection.inbox, - symbolName: "mail.stack", - activeSymbolName: "mail.stack.fill", - badgeCount: showInboxUnreadBadge ? unreadTracker.total : 0 - ) - } - ProfileView(userID: appState.currentActiveAccount.id) - .fancyTabItem(tag: TabSelection.profile) { - FancyTabBarLabel( - tag: TabSelection.profile, - customText: computeUsername(account: appState.currentActiveAccount), - symbolName: "person.circle", - activeSymbolName: "person.circle.fill" - ) - .simultaneousGesture(accountSwitchLongPress) - } + // wrapping these two behind a check for an active user, as of now we'll always have one + // but when guest mode arrives we'll either omit these entirely, or replace them with a + // guest mode specific tab for sign in / change instance screen. + if let account = appState.currentActiveAccount { + InboxView() + .fancyTabItem(tag: TabSelection.inbox) { + FancyTabBarLabel( + tag: TabSelection.inbox, + symbolName: "mail.stack", + activeSymbolName: "mail.stack.fill", + badgeCount: showInboxUnreadBadge ? unreadTracker.total : 0 + ) + } + + ProfileView(userID: account.id) + .fancyTabItem(tag: TabSelection.profile) { + FancyTabBarLabel( + tag: TabSelection.profile, + customText: computeUsername(account: account), + symbolName: "person.circle", + activeSymbolName: "person.circle.fill" + ) + .simultaneousGesture(accountSwitchLongPress) + } + } + SearchView() .fancyTabItem(tag: TabSelection.search) { FancyTabBarLabel( @@ -89,8 +96,8 @@ struct ContentView: View { accountChanged() } .onReceive(errorHandler.$sessionExpired) { expired in - if expired { - NotificationDisplayer.presentTokenRefreshFlow(for: appState.currentActiveAccount) { updatedAccount in + if expired, let account = appState.currentActiveAccount { + NotificationDisplayer.presentTokenRefreshFlow(for: account) { updatedAccount in appState.setActiveAccount(updatedAccount) } } @@ -149,7 +156,7 @@ struct ContentView: View { switch profileTabLabel { case .username: return account.username case .instance: return account.hostName ?? account.username - case .nickname: return appState.currentNickname + case .nickname: return appState.currentNickname ?? account.username case .anonymous: return "Profile" } } diff --git a/Mlem/Extensions/Environment - Force Onboard.swift b/Mlem/Extensions/Environment - Force Onboard.swift deleted file mode 100644 index 0c7a9f967..000000000 --- a/Mlem/Extensions/Environment - Force Onboard.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// Environment - Force Onboard.swift -// Mlem -// -// Created by Eric Andrews on 2023-07-19. -// - -import Foundation -import SwiftUI - -private struct ForceOnboardSetter: EnvironmentKey { - static let defaultValue: () -> Void = {} -} - -extension EnvironmentValues { - var forceOnboard: () -> Void { - get { self[ForceOnboardSetter.self] } - set { self[ForceOnboardSetter.self] = newValue } - } -} diff --git a/Mlem/Extensions/Environment+AppFlow.swift b/Mlem/Extensions/Environment+AppFlow.swift new file mode 100644 index 000000000..6593164d1 --- /dev/null +++ b/Mlem/Extensions/Environment+AppFlow.swift @@ -0,0 +1,20 @@ +// +// Environment+AppFlow.swift +// Mlem +// +// Created by mormaer on 08/09/2023. +// +// + +import SwiftUI + +private struct AppFlowSetter: EnvironmentKey { + static let defaultValue: (AppFlow) -> Void = { _ in } +} + +extension EnvironmentValues { + var setAppFlow: (AppFlow) -> Void { + get { self[AppFlowSetter.self] } + set { self[AppFlowSetter.self] = newValue } + } +} diff --git a/Mlem/MlemApp.swift b/Mlem/MlemApp.swift index aa8f8ecfe..e5a616f3c 100644 --- a/Mlem/MlemApp.swift +++ b/Mlem/MlemApp.swift @@ -13,7 +13,6 @@ import XCTestDynamicOverlay @main struct MlemApp: App { - @Dependency(\.accountsTracker) var accountsTracker @AppStorage("lightOrDarkMode") var lightOrDarkMode: UIUserInterfaceStyle = .unspecified @@ -24,7 +23,7 @@ struct MlemApp: App { var body: some Scene { WindowGroup { if !_XCTIsTesting { - Window(selectedAccount: accountsTracker.defaultAccount) + Window(flow: initialFlow) .onAppear { var imageConfig = ImagePipeline.Configuration.withDataCache(name: "main", sizeLimit: AppConstants.cacheSize) imageConfig.dataLoadingQueue = OperationQueue(maxConcurrentCount: 8) @@ -73,7 +72,7 @@ struct MlemApp: App { } } - func setupAppShortcuts() { + private func setupAppShortcuts() { guard accountsTracker.savedAccounts.first != nil else { return } // Subscribed Feed @@ -112,4 +111,13 @@ struct MlemApp: App { allFeedItem ] } + + /// A variable describing the initial flow the application should run after start-up + private var initialFlow: AppFlow { + guard let account = accountsTracker.defaultAccount else { + return .onboarding + } + + return .account(account) + } } diff --git a/Mlem/Models/Trackers/Favorite Community Tracker.swift b/Mlem/Models/Trackers/Favorite Community Tracker.swift index 8303bc8b9..b15845d13 100644 --- a/Mlem/Models/Trackers/Favorite Community Tracker.swift +++ b/Mlem/Models/Trackers/Favorite Community Tracker.swift @@ -30,11 +30,18 @@ class FavoriteCommunitiesTracker: ObservableObject { // MARK: - Public Methods + /// A method to associate an account with the favorites tracker, once an account has been set calling it's methods will store/remove/return communities relating to the account + /// - Parameter account: The `SavedAccount` to associate with the tracker func configure(for account: SavedAccount) { self.account = account favoritesForCurrentAccount = favoriteCommunities .filter { $0.forAccountID == account.id } - .map { $0.community } + .map(\.community) + } + + /// A method to clear the account that is currently associated with the favorites tracker + func clearStoredAccount() { + account = nil } func favorite(_ community: APICommunity) { @@ -57,7 +64,7 @@ class FavoriteCommunitiesTracker: ObservableObject { } func isFavorited(_ community: APICommunity) -> Bool { - return favoritesForCurrentAccount.contains(community) + favoritesForCurrentAccount.contains(community) } func clearCurrentFavourites() { @@ -66,7 +73,7 @@ class FavoriteCommunitiesTracker: ObservableObject { return } - let filteredFavorites = favoriteCommunities.filter { $0.forAccountID != account.id } + let filteredFavorites = favoriteCommunities.filter { $0.forAccountID != account.id } favoriteCommunities = filteredFavorites } @@ -76,7 +83,7 @@ class FavoriteCommunitiesTracker: ObservableObject { if let account { favoritesForCurrentAccount = newValue .filter { $0.forAccountID == account.id } - .map { $0.community } + .map(\.community) } Task { [weak self] in diff --git a/Mlem/Models/Trackers/Saved Account Tracker.swift b/Mlem/Models/Trackers/Saved Account Tracker.swift index 084c1ded4..dfc34981d 100644 --- a/Mlem/Models/Trackers/Saved Account Tracker.swift +++ b/Mlem/Models/Trackers/Saved Account Tracker.swift @@ -13,9 +13,10 @@ import SwiftUI private let defaultInstanceGroupKey = "Other" class SavedAccountTracker: ObservableObject { - @Dependency(\.persistenceRepository) private var persistenceRepository + @Environment(\.setAppFlow) private var setFlow + @AppStorage("defaultAccountId") var defaultAccountId: Int? @Published var savedAccounts = [SavedAccount]() @@ -59,27 +60,19 @@ class SavedAccountTracker: ObservableObject { savedAccounts[index] = account } - // TODO: pass in AppState using a dependency or something nice like that - func removeAccount(account: SavedAccount, appState: AppState, forceOnboard: () -> Void) { + func removeAccount(account: SavedAccount) { guard let index = savedAccounts.firstIndex(of: account) else { assertionFailure("Tried to remove an account that does not exist") return } savedAccounts.remove(at: index) - - // if another account exists, swap to it; otherwise force onboarding - if let firstAccount: SavedAccount = savedAccounts.first { - appState.setActiveAccount(firstAccount) - } else { - forceOnboard() - } } // MARK: - Private methods private func accountsDidChange(_ newValue: [SavedAccount]) { - self.accountsByInstance = Dictionary( + accountsByInstance = Dictionary( grouping: newValue, by: { $0.hostName ?? defaultInstanceGroupKey } ) diff --git a/Mlem/Views/Shared/Accounts/Accounts Page.swift b/Mlem/Views/Shared/Accounts/Accounts Page.swift index d0f5f3bf6..26d49aa4a 100644 --- a/Mlem/Views/Shared/Accounts/Accounts Page.swift +++ b/Mlem/Views/Shared/Accounts/Accounts Page.swift @@ -1,5 +1,5 @@ // -// Instance and Community List View.swift +// AccountsPage.swift // Mlem // // Created by David Bureš on 27.03.2022. @@ -11,20 +11,13 @@ import SwiftUI struct AccountsPage: View { @Dependency(\.accountsTracker) var accountsTracker + @Environment(\.setAppFlow) private var setFlow + @EnvironmentObject var appState: AppState - - @Environment(\.forceOnboard) var forceOnboard @State private var isShowingInstanceAdditionSheet: Bool = false - @State var selectedAccount: SavedAccount? - - @State var isShowingDeleteConfirm: Bool = false - - let onboarding: Bool - init(onboarding: Bool = false) { - self.onboarding = onboarding - } + @State var accountForDeletion: SavedAccount? @Environment(\.dismiss) var dismiss @@ -32,11 +25,8 @@ struct AccountsPage: View { let instances = Array(accountsTracker.accountsByInstance.keys).sorted() Group { - if onboarding || instances.isEmpty || isShowingInstanceAdditionSheet { - AddSavedInstanceView( - onboarding: onboarding, - currentAccount: $selectedAccount - ) + if instances.isEmpty || isShowingInstanceAdditionSheet { + AddSavedInstanceView(onboarding: false) } else { List { ForEach(instances, id: \.self) { instance in @@ -44,18 +34,30 @@ struct AccountsPage: View { ForEach(accountsTracker.accountsByInstance[instance] ?? []) { account in Button(account.username) { dismiss() - - // this tiny delay prevents the modal dismiss animation from being cancelled - DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { - appState.setActiveAccount(account) - } + setFlow(using: account) } .swipeActions { Button("Remove", role: .destructive) { - accountsTracker.removeAccount(account: account, appState: appState, forceOnboard: forceOnboard) + dismiss() + accountsTracker.removeAccount(account: account) + if account == appState.currentActiveAccount { + // if we just deleted the current account we (currently!) have a decision to make + if let first = accountsTracker.savedAccounts.first { + // if we have another account to go to that... + + // TODO: once onboarding is updated to support showing a users + // current accounts we can scrap this and always go to onboarding + // which leaves the decision of which account to re-enter as in the + // users hands as opposed to us picking one at random with `.first`. + setFlow(using: first) + } else { + // no accounts, so go to onboarding + setFlow(using: nil) + } + } } } - .foregroundColor(appState.currentActiveAccount == account ? .secondary : .primary) + .foregroundColor(color(for: account)) } } } @@ -63,31 +65,43 @@ struct AccountsPage: View { Button { isShowingInstanceAdditionSheet = true } label: { - Text("Add Account") + Label("Add Account", systemImage: AppConstants.switchUserSymbolName) } .accessibilityLabel("Add a new account.") - Button(role: .destructive) { - isShowingDeleteConfirm = true - } label: { - Label("Delete Current Account", systemImage: "trash") - .foregroundColor(.red) + if let account = appState.currentActiveAccount { + Button(role: .destructive) { + accountForDeletion = account + } label: { + Label("Delete Current Account", systemImage: "trash") + .foregroundColor(.red) + } } } } } - .onAppear { - selectedAccount = appState.currentActiveAccount - } - .onChange(of: selectedAccount) { account in - guard let account else { return } - appState.setActiveAccount(account) - } .sheet(isPresented: $isShowingInstanceAdditionSheet) { - AddSavedInstanceView(onboarding: false, currentAccount: $selectedAccount) + AddSavedInstanceView(onboarding: false) } - .sheet(isPresented: $isShowingDeleteConfirm) { - DeleteAccountView() + .sheet(item: $accountForDeletion) { account in + DeleteAccountView(account: account) + } + } + + private func color(for account: SavedAccount) -> Color { + guard let currentAccount = appState.currentActiveAccount else { return .primary } + return account == currentAccount ? .secondary : .primary + } + + private func setFlow(using account: SavedAccount?) { + // this tiny delay prevents the modal dismiss animation from being cancelled + DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { + if let account { + setFlow(.account(account)) + return + } + + setFlow(.onboarding) } } } diff --git a/Mlem/Views/Shared/Accounts/Add Account View.swift b/Mlem/Views/Shared/Accounts/Add Account View.swift index b47a4cbc2..7828a383c 100644 --- a/Mlem/Views/Shared/Accounts/Add Account View.swift +++ b/Mlem/Views/Shared/Accounts/Add Account View.swift @@ -43,6 +43,7 @@ struct AddSavedInstanceView: View { @Environment(\.dismiss) var dismiss @Environment(\.openURL) private var openURL + @Environment(\.setAppFlow) private var setFlow @State private var enteredInstance: String = "" @State private var username = "" @@ -58,7 +59,6 @@ struct AddSavedInstanceView: View { let onboarding: Bool let givenInstance: String? // if present, will override manual instance entry - @Binding var currentAccount: SavedAccount? var instance: String { givenInstance ?? enteredInstance } var badCredentialsMessage: String { onboarding @@ -70,11 +70,9 @@ struct AddSavedInstanceView: View { init( onboarding: Bool, - currentAccount: Binding, givenInstance: String? = nil ) { self.onboarding = onboarding - self._currentAccount = currentAccount self.givenInstance = givenInstance } @@ -331,12 +329,7 @@ struct AddSavedInstanceView: View { dismiss() DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { - currentAccount = newAccount - - // appState is not present on onboarding screen - if !onboarding { - appState.setActiveAccount(newAccount) - } + setFlow(.account(newAccount)) } } catch { handle(error) @@ -415,10 +408,7 @@ struct AddSavedInstanceView: View { struct AddSavedInstanceView_Previews: PreviewProvider { static var previews: some View { - AddSavedInstanceView( - onboarding: true, - currentAccount: .constant(.mock()) - ) + AddSavedInstanceView(onboarding: true) } } diff --git a/Mlem/Views/Shared/Accounts/DeleteAccountView.swift b/Mlem/Views/Shared/Accounts/DeleteAccountView.swift index d17e6c3e6..f6e38dc2f 100644 --- a/Mlem/Views/Shared/Accounts/DeleteAccountView.swift +++ b/Mlem/Views/Shared/Accounts/DeleteAccountView.swift @@ -10,27 +10,26 @@ import Foundation import SwiftUI struct DeleteAccountView: View { - @Dependency(\.accountsTracker) var accountsTracker - - @EnvironmentObject var appState: AppState + @EnvironmentObject var appState: AppState // TODO: this is only needed while onboarding does not support existing accounts + @Environment(\.setAppFlow) private var setFlow @Environment(\.dismiss) var dismiss - @Environment(\.forceOnboard) var forceOnboard + @Dependency(\.accountsTracker) var accountsTracker: SavedAccountTracker @Dependency(\.apiClient) private var apiClient @Dependency(\.errorHandler) var errorHandler + let account: SavedAccount + @State private var password = "" var body: some View { VStack(alignment: .center, spacing: 20) { - Text("Really delete \(appState.currentActiveAccount.username)?") + Text("Really delete \(account.username)?") .font(.title) .fontWeight(.bold) - // swiftlint:disable line_length - Text("Please note that this will *permanently* remove it from \(appState.currentActiveAccount.hostName ?? "the instance"), not just Mlem!") - // swiftlint:enable line_length + Text("Please note that this will *permanently* remove it from \(account.hostName ?? "the instance"), not just Mlem!") Text("To confirm, please enter your password:") @@ -55,12 +54,22 @@ struct DeleteAccountView: View { func deleteAccount() { Task { do { - try await apiClient.deleteUser(user: appState.currentActiveAccount, password: password) - accountsTracker.removeAccount( - account: appState.currentActiveAccount, - appState: appState, - forceOnboard: forceOnboard - ) + try await apiClient.deleteUser(user: account, password: password) + accountsTracker.removeAccount(account: account) + if account == appState.currentActiveAccount { + // if we just deleted the current account we (currently!) have a decision to make + if let first = accountsTracker.savedAccounts.first { + // if we have another account to go to do that... + // TODO: once onboarding is updated to support showing a users + // current accounts we can scrap this and always go to onboarding + // which leaves the decision of which account to re-enter as in the + // users hands as opposed to us picking one at random with `.first`. + setFlow(.account(first)) + } else { + // no accounts, so go to onboarding + setFlow(.onboarding) + } + } } catch { errorHandler.handle(.init(underlyingError: error)) } diff --git a/Mlem/Views/Shared/Accounts/Onboarding View.swift b/Mlem/Views/Shared/Accounts/Onboarding View.swift index 5de334dae..fa4673d78 100644 --- a/Mlem/Views/Shared/Accounts/Onboarding View.swift +++ b/Mlem/Views/Shared/Accounts/Onboarding View.swift @@ -13,7 +13,7 @@ struct OnboardingView: View { case welcome, about, instances, addAccount } - @Binding var selectedAccount: SavedAccount? + @Binding var flow: AppFlow @State var selectedTab: OnboardingTab = .welcome @State var hideNav: Bool = true @@ -31,12 +31,8 @@ struct OnboardingView: View { instancesTab .tag(OnboardingTab.instances) - AddSavedInstanceView( - onboarding: true, - currentAccount: $selectedAccount, - givenInstance: selectedInstance?.url.description - ) - .tag(OnboardingTab.addAccount) + AddSavedInstanceView(onboarding: true, givenInstance: selectedInstance?.url.absoluteString) + .tag(OnboardingTab.addAccount) } .onChange(of: selectedInstance) { _ in selectedTab = .addAccount diff --git a/Mlem/Views/Shared/Comments/Comment Item Logic.swift b/Mlem/Views/Shared/Comments/Comment Item Logic.swift index 99fedc49a..8436d8b91 100644 --- a/Mlem/Views/Shared/Comments/Comment Item Logic.swift +++ b/Mlem/Views/Shared/Comments/Comment Item Logic.swift @@ -199,7 +199,7 @@ extension CommentItem { }) // edit - if hierarchicalComment.commentView.creator.id == appState.currentActiveAccount.id { + if appState.isCurrentAccountId(hierarchicalComment.commentView.creator.id) { ret.append(MenuFunction( text: "Edit", imageName: "pencil", @@ -211,7 +211,7 @@ extension CommentItem { } // delete - if hierarchicalComment.commentView.creator.id == appState.currentActiveAccount.id { + if appState.isCurrentAccountId(hierarchicalComment.commentView.creator.id) { ret.append(MenuFunction( text: "Delete", imageName: "trash", diff --git a/Mlem/Views/Shared/Posts/ExpandedPostLogic.swift b/Mlem/Views/Shared/Posts/ExpandedPostLogic.swift index 2119a4bbe..b243d2666 100644 --- a/Mlem/Views/Shared/Posts/ExpandedPostLogic.swift +++ b/Mlem/Views/Shared/Posts/ExpandedPostLogic.swift @@ -147,7 +147,7 @@ extension ExpandedPost { replyToPost() }) - if post.creator.id == appState.currentActiveAccount.id { + if appState.isCurrentAccountId(post.creator.id) { // edit ret.append(MenuFunction( text: "Edit", diff --git a/Mlem/Views/Shared/Posts/Feed Post.swift b/Mlem/Views/Shared/Posts/Feed Post.swift index 9754a3a70..e0f6d6e29 100644 --- a/Mlem/Views/Shared/Posts/Feed Post.swift +++ b/Mlem/Views/Shared/Posts/Feed Post.swift @@ -344,7 +344,7 @@ struct FeedPost: View { replyToPost() }) - if post.creator.id == appState.currentActiveAccount.id { + if appState.isCurrentAccountId(post.creator.id) { // edit ret.append(MenuFunction( text: "Edit", diff --git a/Mlem/Views/Tabs/Feeds/Feed Root.swift b/Mlem/Views/Tabs/Feeds/Feed Root.swift index d96ef43cf..12453477e 100644 --- a/Mlem/Views/Tabs/Feeds/Feed Root.swift +++ b/Mlem/Views/Tabs/Feeds/Feed Root.swift @@ -25,7 +25,6 @@ struct FeedRoot: View { var body: some View { NavigationSplitView { CommunityListView(selectedCommunity: $rootDetails) - .id(appState.currentActiveAccount.id) } detail: { if let rootDetails { NavigationStack(path: $navigationPath) { @@ -38,7 +37,7 @@ struct FeedRoot: View { .environmentObject(appState) .handleLemmyViews() } - .id(rootDetails.id + appState.currentActiveAccount.id) + .id(rootDetails.id) } else { Text("Please select a community") } diff --git a/Mlem/Views/Tabs/Inbox/Inbox View.swift b/Mlem/Views/Tabs/Inbox/Inbox View.swift index 4067658e0..973079fac 100644 --- a/Mlem/Views/Tabs/Inbox/Inbox View.swift +++ b/Mlem/Views/Tabs/Inbox/Inbox View.swift @@ -47,9 +47,6 @@ struct InboxView: View { @AppStorage("internetSpeed") var internetSpeed: InternetSpeed = .fast // MARK: Internal - - // id of the last account loaded with - @State var lastKnownAccountId: Int = 0 // error handling @State var errorOccurred: Bool = false @@ -143,14 +140,12 @@ struct InboxView: View { // if a tracker is empty or the account has changed, refresh if mentionsTracker.items.isEmpty || messagesTracker.items.isEmpty || - repliesTracker.items.isEmpty || - lastKnownAccountId != appState.currentActiveAccount.id { + repliesTracker.items.isEmpty { print("Inbox tracker is empty") await refreshFeed() } else { print("Inbox tracker is not empty") } - lastKnownAccountId = appState.currentActiveAccount.id } } diff --git a/Mlem/Views/Tabs/Profile/User View.swift b/Mlem/Views/Tabs/Profile/User View.swift index fa89b884b..26150d3ab 100644 --- a/Mlem/Views/Tabs/Profile/User View.swift +++ b/Mlem/Views/Tabs/Profile/User View.swift @@ -174,7 +174,7 @@ struct UserView: View { } private func isShowingOwnProfile() -> Bool { - userID == appState.currentActiveAccount.id + appState.isCurrentAccountId(userID) } @MainActor diff --git a/Mlem/Views/Tabs/Settings/Components/Views/Appearance/TabBar/TabBarSettingsView.swift b/Mlem/Views/Tabs/Settings/Components/Views/Appearance/TabBar/TabBarSettingsView.swift index 3217e0ad1..15f3a77bf 100644 --- a/Mlem/Views/Tabs/Settings/Components/Views/Appearance/TabBar/TabBarSettingsView.swift +++ b/Mlem/Views/Tabs/Settings/Components/Views/Appearance/TabBar/TabBarSettingsView.swift @@ -9,7 +9,6 @@ import Dependencies import SwiftUI struct TabBarSettingsView: View { - @Dependency(\.accountsTracker) var accountsTracker @AppStorage("profileTabLabel") var profileTabLabel: ProfileTabLabel = .username @@ -22,6 +21,8 @@ struct TabBarSettingsView: View { var body: some View { Form { + // TODO: options like this will need to be updated to only show when there is an active account + // present once guest mode is fully implemented Section { SelectableSettingsItem( settingIconSystemName: "person.text.rectangle", @@ -32,15 +33,19 @@ struct TabBarSettingsView: View { if profileTabLabel == .nickname { Label { - TextField(text: $textFieldEntry, prompt: Text(appState.currentNickname)) { + TextField(text: $textFieldEntry, prompt: Text(appState.currentNickname ?? "")) { Text("Nickname") } .autocorrectionDisabled(true) .textInputAutocapitalization(.never) .onSubmit { print(textFieldEntry) - let newAccount = SavedAccount(from: appState.currentActiveAccount, storedNickname: textFieldEntry) - appState.changeDisplayedNickname(to: textFieldEntry) + guard let existingAccount = appState.currentActiveAccount else { + return + } + + let newAccount = SavedAccount(from: existingAccount, storedNickname: textFieldEntry) + appState.setActiveAccount(newAccount) accountsTracker.update(with: newAccount) } } icon: { @@ -68,7 +73,8 @@ struct TabBarSettingsView: View { .navigationTitle("Tab Bar") .navigationBarColor() .animation(.easeIn, value: profileTabLabel) - .onChange(of: appState.currentActiveAccount.nickname) { nickname in + .onChange(of: appState.currentActiveAccount?.nickname) { nickname in + guard let nickname else { return } print("new nickname: \(nickname)") textFieldEntry = nickname } diff --git a/Mlem/Views/Tabs/Settings/Components/Views/General/GeneralSettingsView.swift b/Mlem/Views/Tabs/Settings/Components/Views/General/GeneralSettingsView.swift index 73c552f9f..d756431b1 100644 --- a/Mlem/Views/Tabs/Settings/Components/Views/General/GeneralSettingsView.swift +++ b/Mlem/Views/Tabs/Settings/Components/Views/General/GeneralSettingsView.swift @@ -55,7 +55,7 @@ struct GeneralSettingsView: View { ) } footer: { // swiftlint:disable line_length - Text("Blurs content flagged as Not Safe For Work until tapped. You can disable NSFW content from appearing entirely in Account Settings on \(appState.currentActiveAccount.instanceLink.host ?? "your instance's webpage").") + Text("Blurs content flagged as Not Safe For Work until tapped. You can disable NSFW content from appearing entirely in Account Settings on \(appState.currentActiveAccount?.instanceLink.host ?? "your instance's webpage").") // swiftlint:enable line_length } diff --git a/Mlem/Window.swift b/Mlem/Window.swift index c653e7c60..1aa743452 100644 --- a/Mlem/Window.swift +++ b/Mlem/Window.swift @@ -20,55 +20,62 @@ struct Window: View { @StateObject var filtersTracker: FiltersTracker = .init() @StateObject var recentSearchesTracker: RecentSearchesTracker = .init() @StateObject var layoutWidgetTracker: LayoutWidgetTracker = .init() + @StateObject var appState: AppState = .init() - @State var selectedAccount: SavedAccount? + @State var flow: AppFlow var body: some View { content - .onChange(of: selectedAccount) { _ in onLogin() } - .onAppear(perform: onLogin) - .environment(\.forceOnboard, forceOnboard) + .id(appState.currentActiveAccount?.id ?? 0) + .onChange(of: flow) { _ in flowDidChange() } + .onAppear(perform: flowDidChange) + .environment(\.setAppFlow, setFlow) } - func onLogin() { + func flowDidChange() { hapticManager.initEngine() + apiClient.configure(for: flow) - guard let selectedAccount else { return } - - apiClient.configure(for: selectedAccount) - favoriteCommunitiesTracker.configure(for: selectedAccount) - siteInformation.load() - - if let host = selectedAccount.instanceLink.host(), - let instance = RecognizedLemmyInstances(rawValue: host) { - easterFlagsTracker.setEasterFlag(.login(host: instance)) + switch flow { + case .onboarding: + appState.clearActiveAccount() + favoriteCommunitiesTracker.clearStoredAccount() + case let .account(account): + appState.setActiveAccount(account) + favoriteCommunitiesTracker.configure(for: account) + siteInformation.load() + + if let host = account.instanceLink.host(), + let instance = RecognizedLemmyInstances(rawValue: host) { + easterFlagsTracker.setEasterFlag(.login(host: instance)) + } } } @ViewBuilder private var content: some View { - if let selectedAccount { - view(for: selectedAccount) - } else { + switch flow { + case .onboarding: NavigationStack { - OnboardingView(selectedAccount: $selectedAccount) + OnboardingView(flow: $flow) } + case let .account(account): + view(for: account) } } @ViewBuilder private func view(for account: SavedAccount) -> some View { ContentView() - .id(account.id) .environmentObject(filtersTracker) - .environmentObject(AppState(defaultAccount: account, selectedAccount: $selectedAccount)) + .environmentObject(appState) .environmentObject(communitySearchResultsTracker) .environmentObject(recentSearchesTracker) .environmentObject(easterFlagsTracker) .environmentObject(layoutWidgetTracker) } - func forceOnboard() { - selectedAccount = nil + private func setFlow(_ flow: AppFlow) { + self.flow = flow } }