Multi accounts

This commit is contained in:
Thomas Ricouard 2022-12-30 08:36:22 +01:00
parent dd5a6a8b45
commit 3a076492a1
16 changed files with 146 additions and 30 deletions

View file

@ -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 = "<group>"; };
9F2B92F9295DA7D700DE16D0 /* AddAccountsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAccountsView.swift; sourceTree = "<group>"; };
9F2B92FB295DA94500DE16D0 /* InstanceInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceInfoView.swift; sourceTree = "<group>"; };
9F2B92FE295EB87100DE16D0 /* AppAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppAccountView.swift; sourceTree = "<group>"; };
9F2B9300295EB8A100DE16D0 /* AppAccountViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppAccountViewModel.swift; sourceTree = "<group>"; };
9F35DB42294F9A2900B3281A /* Status */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Status; path = Packages/Status; sourceTree = "<group>"; };
9F35DB45294FA04C00B3281A /* DesignSystem */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = DesignSystem; path = Packages/DesignSystem; sourceTree = "<group>"; };
9F35DB4629506F6600B3281A /* NotificationTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationTab.swift; sourceTree = "<group>"; };
@ -120,6 +124,8 @@
children = (
9FAE4AD029379AD600772766 /* AppAccount.swift */,
9FAE4AD22937A0C600772766 /* AppAccountsManager.swift */,
9F2B92FE295EB87100DE16D0 /* AppAccountView.swift */,
9F2B9300295EB8A100DE16D0 /* AppAccountViewModel.swift */,
);
path = AppAccounts;
sourceTree = "<group>";
@ -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 */,

View file

@ -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)"

View file

@ -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()
}
}
}
}

View file

@ -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)
}
}
}

View file

@ -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)
}
}
}

View file

@ -20,6 +20,7 @@ struct AccountTab: View {
.toolbar {
statusEditorToolbarItem(routeurPath: routeurPath)
}
.id(account.id)
} else {
AccountDetailView(account: .placeholder())
.redacted(reason: .placeholder)

View file

@ -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

View file

@ -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

View file

@ -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
}

View file

@ -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

View file

@ -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)
}
}
}

View file

@ -35,7 +35,6 @@ public struct ExploreView: View {
}
.task {
viewModel.client = client
guard !viewModel.isLoaded else { return }
await viewModel.fetchTrending()
}
.refreshable {

View file

@ -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() {

View file

@ -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:

View file

@ -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 {

View file

@ -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