diff --git a/IceCubesApp.xcodeproj/project.pbxproj b/IceCubesApp.xcodeproj/project.pbxproj index c6cb5c0f..eded3df6 100644 --- a/IceCubesApp.xcodeproj/project.pbxproj +++ b/IceCubesApp.xcodeproj/project.pbxproj @@ -12,6 +12,8 @@ 9F2B92F6295AE04800DE16D0 /* Tabs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2B92F5295AE04800DE16D0 /* Tabs.swift */; }; 9F2B92FA295DA7D700DE16D0 /* AddAccountsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2B92F9295DA7D700DE16D0 /* AddAccountsView.swift */; }; 9F2B92FC295DA94500DE16D0 /* InstanceInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2B92FB295DA94500DE16D0 /* InstanceInfoView.swift */; }; + 9F2B92FF295EB87100DE16D0 /* AppAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2B92FE295EB87100DE16D0 /* AppAccountView.swift */; }; + 9F2B9301295EB8A100DE16D0 /* AppAccountViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2B9300295EB8A100DE16D0 /* AppAccountViewModel.swift */; }; 9F35DB44294F9A7D00B3281A /* Status in Frameworks */ = {isa = PBXBuildFile; productRef = 9F35DB43294F9A7D00B3281A /* Status */; }; 9F35DB4729506F6600B3281A /* NotificationTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F35DB4629506F6600B3281A /* NotificationTab.swift */; }; 9F35DB4A29506FA100B3281A /* Notifications in Frameworks */ = {isa = PBXBuildFile; productRef = 9F35DB4929506FA100B3281A /* Notifications */; }; @@ -40,6 +42,8 @@ 9F2B92F5295AE04800DE16D0 /* Tabs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tabs.swift; sourceTree = ""; }; 9F2B92F9295DA7D700DE16D0 /* AddAccountsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAccountsView.swift; sourceTree = ""; }; 9F2B92FB295DA94500DE16D0 /* InstanceInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceInfoView.swift; sourceTree = ""; }; + 9F2B92FE295EB87100DE16D0 /* AppAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppAccountView.swift; sourceTree = ""; }; + 9F2B9300295EB8A100DE16D0 /* AppAccountViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppAccountViewModel.swift; sourceTree = ""; }; 9F35DB42294F9A2900B3281A /* Status */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Status; path = Packages/Status; sourceTree = ""; }; 9F35DB45294FA04C00B3281A /* DesignSystem */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = DesignSystem; path = Packages/DesignSystem; sourceTree = ""; }; 9F35DB4629506F6600B3281A /* NotificationTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationTab.swift; sourceTree = ""; }; @@ -120,6 +124,8 @@ children = ( 9FAE4AD029379AD600772766 /* AppAccount.swift */, 9FAE4AD22937A0C600772766 /* AppAccountsManager.swift */, + 9F2B92FE295EB87100DE16D0 /* AppAccountView.swift */, + 9F2B9300295EB8A100DE16D0 /* AppAccountViewModel.swift */, ); path = AppAccounts; sourceTree = ""; @@ -269,9 +275,11 @@ 9F35DB4C2952005C00B3281A /* AccountTab.swift in Sources */, 9FAE4ACB293783B000772766 /* SettingsTab.swift in Sources */, 9FAE4AD32937A0C600772766 /* AppAccountsManager.swift in Sources */, + 9F2B92FF295EB87100DE16D0 /* AppAccountView.swift in Sources */, 9F2B92F6295AE04800DE16D0 /* Tabs.swift in Sources */, 9F398AB329360A4C00A889F2 /* TimelineTab.swift in Sources */, 9F398AA62935FE8A00A889F2 /* AppRouteur.swift in Sources */, + 9F2B9301295EB8A100DE16D0 /* AppAccountViewModel.swift in Sources */, 9FBFE63D292A715500C250E9 /* IceCubesApp.swift in Sources */, 9F2B92FA295DA7D700DE16D0 /* AddAccountsView.swift in Sources */, 9F35DB4729506F6600B3281A /* NotificationTab.swift in Sources */, diff --git a/IceCubesApp/App/AppAccounts/AppAccount.swift b/IceCubesApp/App/AppAccounts/AppAccount.swift index 690b78f7..58b8eb97 100644 --- a/IceCubesApp/App/AppAccounts/AppAccount.swift +++ b/IceCubesApp/App/AppAccounts/AppAccount.swift @@ -4,10 +4,14 @@ import Network import KeychainSwift import Models -struct AppAccount: Codable { +struct AppAccount: Codable, Identifiable { let server: String let oauthToken: OauthToken? + var id: String { + key + } + var key: String { if let oauthToken { return "\(server):\(oauthToken.createdAt)" diff --git a/IceCubesApp/App/AppAccounts/AppAccountView.swift b/IceCubesApp/App/AppAccounts/AppAccountView.swift new file mode 100644 index 00000000..f69bb229 --- /dev/null +++ b/IceCubesApp/App/AppAccounts/AppAccountView.swift @@ -0,0 +1,37 @@ +import SwiftUI +import DesignSystem + +struct AppAccountView: View { + @EnvironmentObject var appAccounts: AppAccountsManager + @StateObject var viewModel: AppAccountViewModel + + var body: some View { + HStack { + if let account = viewModel.account { + ZStack(alignment: .topTrailing) { + AvatarView(url: account.avatar) + if viewModel.appAccount.id == appAccounts.currentAccount.id { + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.green) + .offset(x: 5, y: -5) + } + } + } + VStack(alignment: .leading) { + Text(viewModel.appAccount.server) + .font(.headline) + if let account = viewModel.account { + Text(account.displayName) + Text(account.username) + .font(.footnote) + .foregroundColor(.gray) + } + } + } + .onAppear { + Task { + await viewModel.fetchAccount() + } + } + } +} diff --git a/IceCubesApp/App/AppAccounts/AppAccountViewModel.swift b/IceCubesApp/App/AppAccounts/AppAccountViewModel.swift new file mode 100644 index 00000000..f20418e0 --- /dev/null +++ b/IceCubesApp/App/AppAccounts/AppAccountViewModel.swift @@ -0,0 +1,24 @@ +import SwiftUI +import Models +import Network + +@MainActor +public class AppAccountViewModel: ObservableObject { + let appAccount: AppAccount + let client: Client + + @Published var account: Account? + + init(appAccount: AppAccount) { + self.appAccount = appAccount + self.client = .init(server: appAccount.server, oauthToken: appAccount.oauthToken) + } + + func fetchAccount() async { + do { + account = try await client.get(endpoint: Accounts.verifyCredentials) + } catch { + print(error) + } + } +} diff --git a/IceCubesApp/App/AppAccounts/AppAccountsManager.swift b/IceCubesApp/App/AppAccounts/AppAccountsManager.swift index 4f92ce0c..6e49e380 100644 --- a/IceCubesApp/App/AppAccounts/AppAccountsManager.swift +++ b/IceCubesApp/App/AppAccounts/AppAccountsManager.swift @@ -2,8 +2,11 @@ import SwiftUI import Network class AppAccountsManager: ObservableObject { + @AppStorage("latestCurrentAccountKey") static public var latestCurrentAccountKey: String = "" + @Published var currentAccount: AppAccount { didSet { + Self.latestCurrentAccountKey = currentAccount.id currentClient = .init(server: currentAccount.server, oauthToken: currentAccount.oauthToken) } @@ -16,10 +19,15 @@ class AppAccountsManager: ObservableObject { do { let keychainAccounts = try AppAccount.retrieveAll() availableAccounts = keychainAccounts - defaultAccount = keychainAccounts.last ?? defaultAccount - } catch {} + if let currentAccount = keychainAccounts.first(where: { $0.id == Self.latestCurrentAccountKey }) { + defaultAccount = currentAccount + } else { + defaultAccount = keychainAccounts.last ?? defaultAccount + } + } catch { + availableAccounts = [defaultAccount] + } currentAccount = defaultAccount - availableAccounts = [defaultAccount] currentClient = .init(server: defaultAccount.server, oauthToken: defaultAccount.oauthToken) } @@ -27,12 +35,15 @@ class AppAccountsManager: ObservableObject { do { try account.save() currentAccount = account + availableAccounts.append(account) } catch { } } func delete(account: AppAccount) { + availableAccounts.removeAll(where: { $0.id == account.id }) account.delete() - AppAccount.deleteAll() - currentAccount = AppAccount(server: IceCubesApp.defaultServer, oauthToken: nil) + if currentAccount.id == account.id { + currentAccount = availableAccounts.first ?? AppAccount(server: IceCubesApp.defaultServer, oauthToken: nil) + } } } diff --git a/IceCubesApp/App/Tabs/AccountTab.swift b/IceCubesApp/App/Tabs/AccountTab.swift index 3ea338b8..642b9b83 100644 --- a/IceCubesApp/App/Tabs/AccountTab.swift +++ b/IceCubesApp/App/Tabs/AccountTab.swift @@ -20,6 +20,7 @@ struct AccountTab: View { .toolbar { statusEditorToolbarItem(routeurPath: routeurPath) } + .id(account.id) } else { AccountDetailView(account: .placeholder()) .redacted(reason: .placeholder) diff --git a/IceCubesApp/App/Tabs/ExploreTab.swift b/IceCubesApp/App/Tabs/ExploreTab.swift index f7ac1ae8..0a443722 100644 --- a/IceCubesApp/App/Tabs/ExploreTab.swift +++ b/IceCubesApp/App/Tabs/ExploreTab.swift @@ -7,6 +7,7 @@ import Env import Network struct ExploreTab: View { + @EnvironmentObject private var currentAccount: CurrentAccount @EnvironmentObject private var client: Client @StateObject private var routeurPath = RouterPath() @Binding var popToRootTab: Tab diff --git a/IceCubesApp/App/Tabs/NotificationTab.swift b/IceCubesApp/App/Tabs/NotificationTab.swift index 40f8c804..de2b272f 100644 --- a/IceCubesApp/App/Tabs/NotificationTab.swift +++ b/IceCubesApp/App/Tabs/NotificationTab.swift @@ -7,6 +7,7 @@ import Notifications struct NotificationsTab: View { @EnvironmentObject private var client: Client @EnvironmentObject private var watcher: StreamWatcher + @EnvironmentObject private var currentAccount: CurrentAccount @StateObject private var routeurPath = RouterPath() @Binding var popToRootTab: Tab @@ -18,6 +19,7 @@ struct NotificationsTab: View { .toolbar { statusEditorToolbarItem(routeurPath: routeurPath) } + .id(currentAccount.account?.id) } .onAppear { routeurPath.client = client diff --git a/IceCubesApp/App/Tabs/Settings/SettingsTab.swift b/IceCubesApp/App/Tabs/Settings/SettingsTab.swift index 2812340a..9751bea1 100644 --- a/IceCubesApp/App/Tabs/Settings/SettingsTab.swift +++ b/IceCubesApp/App/Tabs/Settings/SettingsTab.swift @@ -8,7 +8,6 @@ import DesignSystem struct SettingsTabs: View { @EnvironmentObject private var client: Client - @EnvironmentObject private var currentAccount: CurrentAccount @EnvironmentObject private var currentInstance: CurrentInstance @EnvironmentObject private var appAccountsManager: AppAccountsManager @EnvironmentObject private var theme: Theme @@ -30,7 +29,6 @@ struct SettingsTabs: View { } .task { if appAccountsManager.currentAccount.oauthToken != nil { - await currentAccount.fetchCurrentAccount() await currentInstance.fetchCurrentInstance() } } @@ -38,19 +36,21 @@ struct SettingsTabs: View { private var accountsSection: some View { Section("Account") { - if let accountData = currentAccount.account { + ForEach(appAccountsManager.availableAccounts) { account in HStack { - AvatarView(url: accountData.avatar) - VStack(alignment: .leading) { - Text(appAccountsManager.currentAccount.server) - .font(.headline) - Text(accountData.displayName) - Text(accountData.username) - .font(.footnote) - .foregroundColor(.gray) + AppAccountView(viewModel: .init(appAccount: account)) + } + .onTapGesture { + withAnimation { + appAccountsManager.currentAccount = account } } - signOutButton + } + .onDelete { indexSet in + if let index = indexSet.first { + let account = appAccountsManager.availableAccounts[index] + appAccountsManager.delete(account: account) + } } addAccountButton } diff --git a/IceCubesApp/App/Tabs/TimelineTab.swift b/IceCubesApp/App/Tabs/TimelineTab.swift index 167c44e0..cb690a50 100644 --- a/IceCubesApp/App/Tabs/TimelineTab.swift +++ b/IceCubesApp/App/Tabs/TimelineTab.swift @@ -5,6 +5,7 @@ import Network import Combine struct TimelineTab: View { + @EnvironmentObject private var currentAccount: CurrentAccount @EnvironmentObject private var client: Client @StateObject private var routeurPath = RouterPath() @Binding var popToRootTab: Tab @@ -23,6 +24,7 @@ struct TimelineTab: View { } } } + .id(currentAccount.account?.id) } .onAppear { routeurPath.client = client diff --git a/Packages/Account/Sources/Account/AccountDetailViewModel.swift b/Packages/Account/Sources/Account/AccountDetailViewModel.swift index 0e44cf99..d497c17f 100644 --- a/Packages/Account/Sources/Account/AccountDetailViewModel.swift +++ b/Packages/Account/Sources/Account/AccountDetailViewModel.swift @@ -85,6 +85,7 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher { async let featuredTags: [FeaturedTag] = client.get(endpoint: Accounts.featuredTags(id: accountId)) async let familliarFollowers: [FamilliarAccounts] = client.get(endpoint: Accounts.familiarFollowers(withAccount: accountId)) let loadedAccount = try await account + self.account = loadedAccount self.featuredTags = try await featuredTags self.featuredTags.sort { $0.statusesCountInt > $1.statusesCountInt } self.fields = loadedAccount.fields @@ -96,10 +97,13 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher { self.relationship = relationships.first self.familliarFollowers = try await familliarFollowers.first?.accounts ?? [] } - self.account = loadedAccount accountState = .data(account: loadedAccount) } catch { - accountState = .error(error: error) + if let account { + accountState = .data(account: account) + } else { + accountState = .error(error: error) + } } } diff --git a/Packages/Explore/Sources/Explore/ExploreView.swift b/Packages/Explore/Sources/Explore/ExploreView.swift index cc3aaaf6..42dd04f2 100644 --- a/Packages/Explore/Sources/Explore/ExploreView.swift +++ b/Packages/Explore/Sources/Explore/ExploreView.swift @@ -35,7 +35,6 @@ public struct ExploreView: View { } .task { viewModel.client = client - guard !viewModel.isLoaded else { return } await viewModel.fetchTrending() } .refreshable { diff --git a/Packages/Explore/Sources/Explore/ExploreViewModel.swift b/Packages/Explore/Sources/Explore/ExploreViewModel.swift index a7d6569f..2e6b5bf2 100644 --- a/Packages/Explore/Sources/Explore/ExploreViewModel.swift +++ b/Packages/Explore/Sources/Explore/ExploreViewModel.swift @@ -4,7 +4,18 @@ import Network @MainActor class ExploreViewModel: ObservableObject { - var client: Client? + var client: Client? { + didSet { + if oldValue != client { + isLoaded = false + results = [:] + trendingTags = [] + trendingLinks = [] + trendingStatuses = [] + suggestedAccounts = [] + } + } + } enum Token: String, Identifiable { case user = "@user" @@ -56,8 +67,6 @@ class ExploreViewModel: ObservableObject { func fetchTrending() async { guard let client else { return } do { - isLoaded = false - async let suggestedAccounts: [Account] = client.get(endpoint: Accounts.suggestions) async let trendingTags: [Tag] = client.get(endpoint: Trends.tags) async let trendingStatuses: [Status] = client.get(endpoint: Trends.statuses) @@ -71,7 +80,9 @@ class ExploreViewModel: ObservableObject { self.suggestedAccountsRelationShips = try await client.get(endpoint: Accounts.relationships(ids: self.suggestedAccounts.map{ $0.id })) isLoaded = true - } catch { } + } catch { + isLoaded = true + } } func search() { diff --git a/Packages/Notifications/Sources/Notifications/NotificationRowView.swift b/Packages/Notifications/Sources/Notifications/NotificationRowView.swift index 9610dd9e..5b135ff8 100644 --- a/Packages/Notifications/Sources/Notifications/NotificationRowView.swift +++ b/Packages/Notifications/Sources/Notifications/NotificationRowView.swift @@ -108,7 +108,7 @@ extension Models.Notification.NotificationType { case .status: return "posted a status" case .mention: - return "mentionned you" + return "mentioned you" case .reblog: return "boosted" case .follow: diff --git a/Packages/Notifications/Sources/Notifications/NotificationsViewModel.swift b/Packages/Notifications/Sources/Notifications/NotificationsViewModel.swift index f71fe7ff..1ae175f2 100644 --- a/Packages/Notifications/Sources/Notifications/NotificationsViewModel.swift +++ b/Packages/Notifications/Sources/Notifications/NotificationsViewModel.swift @@ -19,7 +19,13 @@ class NotificationsViewModel: ObservableObject { case mentions = "Mentions" } - var client: Client? + var client: Client? { + didSet { + if oldValue != client { + notifications = [] + } + } + } @Published var state: State = .loading @Published var tab: Tab = .all { didSet { diff --git a/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift b/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift index dbabdee4..2552da53 100644 --- a/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift +++ b/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift @@ -6,7 +6,13 @@ import Env @MainActor class TimelineViewModel: ObservableObject, StatusesFetcher { - var client: Client? + var client: Client? { + didSet { + if oldValue != client { + statuses = [] + } + } + } // Internal source of truth for a timeline. private var statuses: [Status] = [] @@ -66,14 +72,14 @@ class TimelineViewModel: ObservableObject, StatusesFetcher { pendingStatuses = [] statusesState = .loading statuses = try await client.get(endpoint: timeline.endpoint(sinceId: nil, maxId: nil, minId: nil)) - statusesState = .display(statuses: statuses, nextPageState: .hasNextPage) + statusesState = .display(statuses: statuses, nextPageState: statuses.count < 20 ? .none : .hasNextPage) } else if let first = statuses.first { var newStatuses: [Status] = await fetchNewPages(minId: first.id, maxPages: 10) if userIntent || !pendingStatusesEnabled { pendingStatuses = [] statuses.insert(contentsOf: newStatuses, at: 0) withAnimation { - statusesState = .display(statuses: statuses, nextPageState: .hasNextPage) + statusesState = .display(statuses: statuses, nextPageState: statuses.count < 20 ? .none : .hasNextPage) } } else { newStatuses = newStatuses.filter { status in