All new accounts selector

This commit is contained in:
Thomas Ricouard 2023-03-08 19:02:31 +01:00
parent 5c69aa64bc
commit 15b704c97a
5 changed files with 98 additions and 120 deletions

View file

@ -328,6 +328,12 @@ public struct AccountDetailView: View {
} label: { } label: {
Label("settings.push.navigation-title", systemImage: "bell") Label("settings.push.navigation-title", systemImage: "bell")
} }
Button {
routerPath.presentedSheet = .settings
} label: {
Label("settings.title", systemImage: "gear")
}
} }
} label: { } label: {
Image(systemName: "ellipsis.circle") Image(systemName: "ellipsis.circle")

View file

@ -5,7 +5,9 @@ import SwiftUI
public struct AppAccountView: View { public struct AppAccountView: View {
@EnvironmentObject private var routerPath: RouterPath @EnvironmentObject private var routerPath: RouterPath
@EnvironmentObject var appAccounts: AppAccountsManager @EnvironmentObject private var appAccounts: AppAccountsManager
@EnvironmentObject private var preferences: UserPreferences
@StateObject var viewModel: AppAccountViewModel @StateObject var viewModel: AppAccountViewModel
public init(viewModel: AppAccountViewModel) { public init(viewModel: AppAccountViewModel) {
@ -47,6 +49,19 @@ public struct AppAccountView: View {
Image(systemName: "checkmark.circle.fill") Image(systemName: "checkmark.circle.fill")
.foregroundStyle(.white, .green) .foregroundStyle(.white, .green)
.offset(x: 5, y: -5) .offset(x: 5, y: -5)
} else if viewModel.showBadge,
let token = viewModel.appAccount.oauthToken,
let notificationsCount = preferences.getNotificationsCount(for: token),
notificationsCount > 0{
ZStack {
Circle()
.fill(.red)
Text(notificationsCount > 99 ? "99+" : String(notificationsCount))
.foregroundColor(.white)
.font(.system(size: 9))
}
.frame(width: 20, height: 20)
.offset(x: 5, y: -5)
} }
} }
} else { } else {
@ -66,9 +81,11 @@ public struct AppAccountView: View {
.foregroundColor(.gray) .foregroundColor(.gray)
} }
} }
Spacer() if viewModel.isInNavigation {
Image(systemName: "chevron.right") Spacer()
.foregroundColor(.gray) Image(systemName: "chevron.right")
.foregroundColor(.gray)
}
} }
.contentShape(Rectangle()) .contentShape(Rectangle())
.onTapGesture { .onTapGesture {

View file

@ -12,6 +12,8 @@ public class AppAccountViewModel: ObservableObject {
var appAccount: AppAccount var appAccount: AppAccount
let client: Client let client: Client
let isCompact: Bool let isCompact: Bool
let isInNavigation: Bool
let showBadge: Bool
@Published var account: Account? { @Published var account: Account? {
didSet { didSet {
@ -21,8 +23,6 @@ public class AppAccountViewModel: ObservableObject {
} }
} }
@Published var roundedAvatar: UIImage?
var acct: String { var acct: String {
if let acct = appAccount.accountName { if let acct = appAccount.accountName {
return acct return acct
@ -31,24 +31,20 @@ public class AppAccountViewModel: ObservableObject {
} }
} }
public init(appAccount: AppAccount, isCompact: Bool = false) { public init(appAccount: AppAccount, isCompact: Bool = false, isInNavigation: Bool = true, showBadge: Bool = false) {
self.appAccount = appAccount self.appAccount = appAccount
self.isCompact = isCompact self.isCompact = isCompact
self.isInNavigation = isInNavigation
self.showBadge = showBadge
client = .init(server: appAccount.server, oauthToken: appAccount.oauthToken) client = .init(server: appAccount.server, oauthToken: appAccount.oauthToken)
} }
func fetchAccount() async { func fetchAccount() async {
do { do {
account = Self.accountsCache[appAccount.id] account = Self.accountsCache[appAccount.id]
roundedAvatar = Self.avatarsCache[appAccount.id]
account = try await client.get(endpoint: Accounts.verifyCredentials) account = try await client.get(endpoint: Accounts.verifyCredentials)
Self.accountsCache[appAccount.id] = account Self.accountsCache[appAccount.id] = account
if let account {
await refreshAvatar(account: account)
}
} catch {} } catch {}
} }
@ -60,18 +56,4 @@ public class AppAccountViewModel: ObservableObject {
} }
} catch {} } catch {}
} }
private func refreshAvatar(account: Account) async {
// Warning: Non-sendable type '(any URLSessionTaskDelegate)?' exiting main actor-isolated
// context in call to non-isolated instance method 'data(for:delegate:)' cannot cross actor
// boundary.
// This is on the defaulted-to-nil second parameter of `.data(from:delegate:)`.
// There is a Radar tracking this & others like it.
if let (data, _) = try? await URLSession.shared.data(from: account.avatar),
let image = UIImage(data: data)?.roundedImage
{
roundedAvatar = image
Self.avatarsCache[account.id] = image
}
}
} }

View file

@ -6,10 +6,12 @@ public struct AppAccountsSelectorView: View {
@EnvironmentObject private var preferences: UserPreferences @EnvironmentObject private var preferences: UserPreferences
@EnvironmentObject private var currentAccount: CurrentAccount @EnvironmentObject private var currentAccount: CurrentAccount
@EnvironmentObject private var appAccounts: AppAccountsManager @EnvironmentObject private var appAccounts: AppAccountsManager
@EnvironmentObject private var theme: Theme
@ObservedObject var routerPath: RouterPath @ObservedObject var routerPath: RouterPath
@State private var accountsViewModel: [AppAccountViewModel] = [] @State private var accountsViewModel: [AppAccountViewModel] = []
@State private var isPresented: Bool = false
private let accountCreationEnabled: Bool private let accountCreationEnabled: Bool
private let avatarSize: AvatarView.Size private let avatarSize: AvatarView.Size
@ -32,26 +34,18 @@ public struct AppAccountsSelectorView: View {
} }
public var body: some View { public var body: some View {
Group { Button {
if UIDevice.current.userInterfaceIdiom == .pad { isPresented.toggle()
labelView
.contextMenu {
menuView
}
} else {
Menu {
menuView
} label: {
labelView
}
}
}
.onTapGesture {
HapticManager.shared.fireHaptic(of: .buttonPress) HapticManager.shared.fireHaptic(of: .buttonPress)
} label: {
labelView
} }
.onAppear { .sheet(isPresented: $isPresented, content: {
refreshAccounts() accountsView.presentationDetents([.medium])
} .onAppear {
refreshAccounts()
}
})
.onChange(of: currentAccount.account?.id) { _ in .onChange(of: currentAccount.account?.id) { _ in
refreshAccounts() refreshAccounts()
} }
@ -67,7 +61,7 @@ public struct AppAccountsSelectorView: View {
.redacted(reason: .placeholder) .redacted(reason: .placeholder)
} }
}.overlay(alignment: .topTrailing) { }.overlay(alignment: .topTrailing) {
if !currentAccount.followRequests.isEmpty || showNotificationBadge { if (!currentAccount.followRequests.isEmpty || showNotificationBadge) && accountCreationEnabled {
Circle() Circle()
.fill(Color.red) .fill(Color.red)
.frame(width: 9, height: 9) .frame(width: 9, height: 9)
@ -76,77 +70,71 @@ public struct AppAccountsSelectorView: View {
.accessibilityLabel("accessibility.app-account.selector.accounts") .accessibilityLabel("accessibility.app-account.selector.accounts")
} }
@ViewBuilder private var accountsView: some View {
private var menuView: some View { NavigationStack {
ForEach(accountsViewModel.sorted { $0.acct < $1.acct }, id: \.appAccount.id) { viewModel in List {
Section(viewModel.acct) { Section {
Button { ForEach(accountsViewModel.sorted { $0.acct < $1.acct }, id: \.appAccount.id) { viewModel in
if let account = currentAccount.account, AppAccountView(viewModel: viewModel)
viewModel.account?.id == account.id
{
routerPath.navigate(to: .accountDetailWithAccount(account: account))
} else {
var transation = Transaction()
transation.disablesAnimations = true
withTransaction(transation) {
appAccounts.currentAccount = viewModel.appAccount
}
} }
}
HapticManager.shared.fireHaptic(of: .buttonPress) .listRowBackground(theme.primaryBackgroundColor)
} label: {
HStack { if accountCreationEnabled {
if let image = viewModel.roundedAvatar { Section {
Image(uiImage: image) Button {
} isPresented = false
HapticManager.shared.fireHaptic(of: .buttonPress)
let name = viewModel.account.flatMap { account in DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
account.displayName?.isEmpty != false ? "@\(account.acct)" : account.displayName routerPath.presentedSheet = .addAccount
} ?? "" }
if let token = viewModel.appAccount.oauthToken, } label: {
preferences.getNotificationsCount(for: token) > 0 Label("app-account.button.add", systemImage: "person.badge.plus")
{
Text("\(name) (\(preferences.getNotificationsCount(for: token)))")
} else {
Text("\(name)")
} }
settingsButton
}
.listRowBackground(theme.primaryBackgroundColor)
}
}
.listStyle(.insetGrouped)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
.navigationTitle("settings.section.accounts")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button {
isPresented.toggle()
} label: {
Image(systemName: "xmark.circle")
}
}
if accountCreationEnabled {
ToolbarItem(placement: .navigationBarTrailing) {
settingsButton
} }
} }
} }
} }
if accountCreationEnabled { }
Divider()
Button { private var settingsButton: some View {
HapticManager.shared.fireHaptic(of: .buttonPress) Button {
routerPath.presentedSheet = .addAccount isPresented = false
} label: { HapticManager.shared.fireHaptic(of: .buttonPress)
Label("app-account.button.add", systemImage: "person.badge.plus") DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
}
}
if UIDevice.current.userInterfaceIdiom == .phone && accountCreationEnabled {
Divider()
Button {
HapticManager.shared.fireHaptic(of: .buttonPress)
routerPath.presentedSheet = .settings routerPath.presentedSheet = .settings
} label: {
Label("tab.settings", systemImage: "gear")
} }
} label: {
Label("tab.settings", systemImage: "gear")
} }
} }
private func refreshAccounts() { private func refreshAccounts() {
if accountsViewModel.isEmpty || appAccounts.availableAccounts.count != accountsViewModel.count { accountsViewModel = []
accountsViewModel = [] for account in appAccounts.availableAccounts {
for account in appAccounts.availableAccounts { let viewModel: AppAccountViewModel = .init(appAccount: account, isInNavigation: false, showBadge: true)
let viewModel: AppAccountViewModel = .init(appAccount: account) accountsViewModel.append(viewModel)
Task {
await viewModel.fetchAccount()
if !accountsViewModel.contains(where: { $0.acct == viewModel.acct }) {
accountsViewModel.append(viewModel)
}
}
}
} }
} }
} }

View file

@ -1,15 +0,0 @@
import UIKit
public extension UIImage {
var roundedImage: UIImage? {
let rect = CGRect(origin: CGPoint(x: 0, y: 0), size: size)
UIGraphicsBeginImageContextWithOptions(size, false, 1)
defer { UIGraphicsEndImageContext() }
UIBezierPath(
roundedRect: rect,
cornerRadius: size.height
).addClip()
draw(in: rect)
return UIGraphicsGetImageFromCurrentImageContext()
}
}