mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-11-22 16:31:00 +00:00
Multi accounts
This commit is contained in:
parent
dd5a6a8b45
commit
3a076492a1
16 changed files with 146 additions and 30 deletions
|
@ -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 */,
|
||||
|
|
|
@ -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)"
|
||||
|
|
37
IceCubesApp/App/AppAccounts/AppAccountView.swift
Normal file
37
IceCubesApp/App/AppAccounts/AppAccountView.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
24
IceCubesApp/App/AppAccounts/AppAccountViewModel.swift
Normal file
24
IceCubesApp/App/AppAccounts/AppAccountViewModel.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ struct AccountTab: View {
|
|||
.toolbar {
|
||||
statusEditorToolbarItem(routeurPath: routeurPath)
|
||||
}
|
||||
.id(account.id)
|
||||
} else {
|
||||
AccountDetailView(account: .placeholder())
|
||||
.redacted(reason: .placeholder)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -35,7 +35,6 @@ public struct ExploreView: View {
|
|||
}
|
||||
.task {
|
||||
viewModel.client = client
|
||||
guard !viewModel.isLoaded else { return }
|
||||
await viewModel.fetchTrending()
|
||||
}
|
||||
.refreshable {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue