diff --git a/IceCubesApp/App/Tabs/Settings/SettingsTab.swift b/IceCubesApp/App/Tabs/Settings/SettingsTab.swift index 03b0784a..32b87085 100644 --- a/IceCubesApp/App/Tabs/Settings/SettingsTab.swift +++ b/IceCubesApp/App/Tabs/Settings/SettingsTab.swift @@ -21,6 +21,7 @@ struct SettingsTabs: View { @StateObject private var routerPath = RouterPath() @State private var addAccountSheetPresented = false + @State private var isEditingAccount = false @Binding var popToRootTab: Tab @@ -69,27 +70,47 @@ struct SettingsTabs: View { private var accountsSection: some View { Section("settings.section.accounts") { ForEach(appAccountsManager.availableAccounts) { account in - AppAccountView(viewModel: .init(appAccount: account)) + HStack { + if isEditingAccount { + Button { + Task { + await logoutAccount(account: account) + } + } label: { + Image(systemName: "trash") + .renderingMode(.template) + .tint(.red) + } + } + AppAccountView(viewModel: .init(appAccount: account)) + } } .onDelete { indexSet in if let index = indexSet.first { let account = appAccountsManager.availableAccounts[index] - if let token = account.oauthToken, - let sub = pushNotifications.subscriptions.first(where: { $0.account.token == token }) - { - Task { - let client = Client(server: account.server, oauthToken: token) - await TimelineCache.shared.clearCache(for: client) - await sub.deleteSubscription() - appAccountsManager.delete(account: account) - } + Task { + await logoutAccount(account: account) } } } + if !appAccountsManager.availableAccounts.isEmpty { + editAccountButton + } addAccountButton } .listRowBackground(theme.primaryBackgroundColor) } + + private func logoutAccount(account: AppAccount) async { + if let token = account.oauthToken, + let sub = pushNotifications.subscriptions.first(where: { $0.account.token == token }) + { + let client = Client(server: account.server, oauthToken: token) + await TimelineCache.shared.clearCache(for: client) + await sub.deleteSubscription() + appAccountsManager.delete(account: account) + } + } @ViewBuilder private var generalSection: some View { @@ -214,6 +235,20 @@ struct SettingsTabs: View { AddAccountView() } } + + private var editAccountButton: some View { + Button(role: isEditingAccount ? .none : .destructive) { + withAnimation { + isEditingAccount.toggle() + } + } label: { + if isEditingAccount { + Text("action.done") + } else { + Text("account.action.logout") + } + } + } private var remoteLocalTimelinesView: some View { Form { diff --git a/Packages/AppAccount/Sources/AppAccount/AppAccountViewModel.swift b/Packages/AppAccount/Sources/AppAccount/AppAccountViewModel.swift index 326c682b..6d5c6f5f 100644 --- a/Packages/AppAccount/Sources/AppAccount/AppAccountViewModel.swift +++ b/Packages/AppAccount/Sources/AppAccount/AppAccountViewModel.swift @@ -6,12 +6,22 @@ import SwiftUI @MainActor public class AppAccountViewModel: ObservableObject { private static var avatarsCache: [String: UIImage] = [:] + private static var accountsCache: [String: Account] = [:] var appAccount: AppAccount let client: Client let isCompact: Bool - @Published var account: Account? + @Published var account: Account? { + didSet { + if let account { + refreshAcct(account: account) + Task { + await refreshAvatar(account: account) + } + } + } + } @Published var roundedAvatar: UIImage? var acct: String { @@ -30,23 +40,31 @@ public class AppAccountViewModel: ObservableObject { func fetchAccount() async { do { + account = Self.accountsCache[appAccount.id] + roundedAvatar = Self.avatarsCache[appAccount.id] + account = try await client.get(endpoint: Accounts.verifyCredentials) - if appAccount.accountName == nil, let account { + + Self.accountsCache[appAccount.id] = account + + } catch {} + } + + private func refreshAcct(account: Account) { + do { + if appAccount.accountName == nil { appAccount.accountName = "\(account.acct)@\(appAccount.server)" try appAccount.save() } - - if let account { - if let image = Self.avatarsCache[account.id] { - roundedAvatar = image - } else if let (data, _) = try? await URLSession.shared.data(from: account.avatar), - let image = UIImage(data: data)?.roundedImage - { - roundedAvatar = image - Self.avatarsCache[account.id] = image - } - } - - } catch {} + } catch { } + } + + private func refreshAvatar(account: Account) async { + if roundedAvatar == nil, + let (data, _) = try? await URLSession.shared.data(from: account.avatar), + let image = UIImage(data: data)?.roundedImage { + roundedAvatar = image + Self.avatarsCache[account.id] = image + } } } diff --git a/Packages/AppAccount/Sources/AppAccount/AppAccountsSelectorView.swift b/Packages/AppAccount/Sources/AppAccount/AppAccountsSelectorView.swift index 740eab78..4bcdeb04 100644 --- a/Packages/AppAccount/Sources/AppAccount/AppAccountsSelectorView.swift +++ b/Packages/AppAccount/Sources/AppAccount/AppAccountsSelectorView.swift @@ -54,7 +54,8 @@ public struct AppAccountsSelectorView: View { if let avatar = currentAccount.account?.avatar, !currentAccount.isLoadingAccount { AvatarView(url: avatar, size: avatarSize) } else { - ProgressView() + AvatarView(url: nil, size: avatarSize) + .redacted(reason: .placeholder) } }.overlay(alignment: .topTrailing) { if !currentAccount.followRequests.isEmpty { diff --git a/Packages/DesignSystem/Sources/DesignSystem/Views/AvatarView.swift b/Packages/DesignSystem/Sources/DesignSystem/Views/AvatarView.swift index 34034706..e212fd3b 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/Views/AvatarView.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/Views/AvatarView.swift @@ -39,10 +39,10 @@ public struct AvatarView: View { } } - public let url: URL + public let url: URL? public let size: Size - public init(url: URL, size: Size = .status) { + public init(url: URL?, size: Size = .status) { self.url = url self.size = size } diff --git a/Packages/Env/Sources/Env/CurrentAccount.swift b/Packages/Env/Sources/Env/CurrentAccount.swift index ea9985af..935e55db 100644 --- a/Packages/Env/Sources/Env/CurrentAccount.swift +++ b/Packages/Env/Sources/Env/CurrentAccount.swift @@ -4,6 +4,8 @@ import Network @MainActor public class CurrentAccount: ObservableObject { + static private var accountsCache: [String: Account] = [:] + @Published public private(set) var account: Account? @Published public private(set) var lists: [List] = [] @Published public private(set) var tags: [Tag] = [] @@ -57,9 +59,13 @@ public class CurrentAccount: ObservableObject { account = nil return } - isLoadingAccount = true + account = Self.accountsCache[client.id] + if account == nil { + isLoadingAccount = true + } account = try? await client.get(endpoint: Accounts.verifyCredentials) isLoadingAccount = false + Self.accountsCache[client.id] = account } public func fetchLists() async {