diff options
| author | Christian Cleberg <[email protected]> | 2026-04-12 23:19:20 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-04-12 23:19:20 -0500 |
| commit | ee9f2904aa319231b7047fca3cb069f0c07019cd (patch) | |
| tree | 69a18234512c63f38f0d477e0023969698ec3c9e /Hutch/Views | |
| parent | 72f860e0e171f6aac4e7d896e7a571147dbcb5c5 (diff) | |
| download | hutch-ee9f2904aa319231b7047fca3cb069f0c07019cd.tar.gz hutch-ee9f2904aa319231b7047fca3cb069f0c07019cd.tar.bz2 hutch-ee9f2904aa319231b7047fca3cb069f0c07019cd.zip | |
feat: add multi-account support
Implements: https://todo.sr.ht/~ccleberg/hutch/49
Implements: https://todo.sr.ht/~ccleberg/hutch/50
Implements: https://todo.sr.ht/~ccleberg/hutch/51
Diffstat (limited to 'Hutch/Views')
| -rw-r--r-- | Hutch/Views/Builds/BuildListView.swift | 4 | ||||
| -rw-r--r-- | Hutch/Views/Home/HomeView.swift | 8 | ||||
| -rw-r--r-- | Hutch/Views/Home/HomeViewModel.swift | 29 | ||||
| -rw-r--r-- | Hutch/Views/Inbox/InboxView.swift | 6 | ||||
| -rw-r--r-- | Hutch/Views/Inbox/InboxViewModel.swift | 22 | ||||
| -rw-r--r-- | Hutch/Views/Lookup/LookupView.swift | 14 | ||||
| -rw-r--r-- | Hutch/Views/Lookup/UserProfileView.swift | 2 | ||||
| -rw-r--r-- | Hutch/Views/More/AccountSwitcherView.swift | 49 | ||||
| -rw-r--r-- | Hutch/Views/More/ProfileView.swift | 2 | ||||
| -rw-r--r-- | Hutch/Views/Pastes/PasteListView.swift | 2 | ||||
| -rw-r--r-- | Hutch/Views/Projects/ProjectDetailView.swift | 4 | ||||
| -rw-r--r-- | Hutch/Views/Projects/ProjectMailingListView.swift | 35 | ||||
| -rw-r--r-- | Hutch/Views/Repositories/RepositoryListView.swift | 2 | ||||
| -rw-r--r-- | Hutch/Views/Settings/SettingsView.swift | 30 | ||||
| -rw-r--r-- | Hutch/Views/Tickets/TicketListView.swift | 5 |
15 files changed, 150 insertions, 64 deletions
diff --git a/Hutch/Views/Builds/BuildListView.swift b/Hutch/Views/Builds/BuildListView.swift index 3bc3d99..d99d53a 100644 --- a/Hutch/Views/Builds/BuildListView.swift +++ b/Hutch/Views/Builds/BuildListView.swift @@ -1,7 +1,7 @@ import SwiftUI struct BuildListView: View { - @AppStorage(AppStorageKeys.swipeActionsEnabled) private var swipeActionsEnabled = true + @AppStorage(AppStorageKeys.swipeActionsEnabled, store: .standard) private var swipeActionsEnabled = true @AppStorage(AppStorageKeys.buildsAutoRefreshInterval) private var autoRefreshRawValue = 0 @AppStorage(AppStorageKeys.buildsRepoFilter) private var savedRepoFilter = "" @Environment(AppState.self) private var appState @@ -104,7 +104,7 @@ struct BuildListView: View { } .task { if viewModel == nil { - let vm = BuildListViewModel(client: appState.client) + let vm = BuildListViewModel(client: appState.client, defaults: appState.accountDefaults) vm.repoFilter = savedRepoFilter viewModel = vm await vm.loadJobs() diff --git a/Hutch/Views/Home/HomeView.swift b/Hutch/Views/Home/HomeView.swift index 426d351..abde827 100644 --- a/Hutch/Views/Home/HomeView.swift +++ b/Hutch/Views/Home/HomeView.swift @@ -1,7 +1,7 @@ import SwiftUI struct HomeView: View { - @AppStorage(AppStorageKeys.swipeActionsEnabled) private var swipeActionsEnabled = true + @AppStorage(AppStorageKeys.swipeActionsEnabled, store: .standard) private var swipeActionsEnabled = true @AppStorage(AppStorageKeys.homeProjectsExpanded) private var projectsExpanded = true @AppStorage(AppStorageKeys.homeAssignedTicketsExpanded) private var assignedTicketsExpanded = true @AppStorage(AppStorageKeys.homeBuildsExpanded) private var buildsExpanded = true @@ -39,7 +39,9 @@ struct HomeView: View { let newViewModel = HomeViewModel( currentUser: currentUser, client: appState.client, - systemStatusRepository: appState.systemStatusRepository + systemStatusRepository: appState.systemStatusRepository, + defaults: appState.accountDefaults, + accountID: appState.activeAccountID ) viewModel = newViewModel vm = newViewModel @@ -834,7 +836,7 @@ private struct HomeAttentionLinkRow<Destination: View>: View { private struct HomeAssignedTicketsListView: View { let viewModel: HomeViewModel - @AppStorage(AppStorageKeys.swipeActionsEnabled) private var swipeActionsEnabled = true + @AppStorage(AppStorageKeys.swipeActionsEnabled, store: .standard) private var swipeActionsEnabled = true var body: some View { List { diff --git a/Hutch/Views/Home/HomeViewModel.swift b/Hutch/Views/Home/HomeViewModel.swift index 682c94d..a28f731 100644 --- a/Hutch/Views/Home/HomeViewModel.swift +++ b/Hutch/Views/Home/HomeViewModel.swift @@ -321,11 +321,22 @@ final class HomeViewModel { } """ - init(currentUser: User, client: SRHTClient, systemStatusRepository: SystemStatusRepository) { + private let defaults: UserDefaults + private let accountID: String + + init( + currentUser: User, + client: SRHTClient, + systemStatusRepository: SystemStatusRepository, + defaults: UserDefaults, + accountID: String + ) { self.currentUser = currentUser self.client = client self.systemStatusRepository = systemStatusRepository self.projectService = ProjectService(client: client) + self.defaults = defaults + self.accountID = accountID } func loadDashboard() async { @@ -403,7 +414,7 @@ final class HomeViewModel { } var pinnedProjects: [Project] { - let pinnedIDs = ProjectPinStore.loadPinnedProjectIDs(for: currentUserKey) + let pinnedIDs = ProjectPinStore.loadPinnedProjectIDs(for: currentUserKey, defaults: defaults) guard !pinnedIDs.isEmpty else { return [] } let projectsByID = Dictionary(uniqueKeysWithValues: projects.map { ($0.id, $0) }) @@ -411,7 +422,7 @@ final class HomeViewModel { } var hasPinnedProjects: Bool { - !ProjectPinStore.loadPinnedProjectIDs(for: currentUserKey).isEmpty + !ProjectPinStore.loadPinnedProjectIDs(for: currentUserKey, defaults: defaults).isEmpty } var failedBuildCount: Int { @@ -592,7 +603,7 @@ final class HomeViewModel { } func markInboxThreadRead(_ thread: InboxThreadSummary) { - InboxReadStateStore.markViewed(max(Date(), thread.lastActivityAt), for: thread.id) + InboxReadStateStore.markViewed(max(Date(), thread.lastActivityAt), for: thread.id, defaults: defaults) unreadInboxThreads.removeAll { $0.id == thread.id } unreadInboxThreadCount = max((unreadInboxThreadCount ?? 1) - 1, 0) hasUnreadInboxThreads = (unreadInboxThreadCount ?? 0) > 0 @@ -600,7 +611,7 @@ final class HomeViewModel { } func markInboxThreadUnread(_ thread: InboxThreadSummary) { - InboxReadStateStore.markUnread(for: thread.id) + InboxReadStateStore.markUnread(for: thread.id, defaults: defaults) if unreadInboxThreads.contains(where: { $0.id == thread.id }) == false { unreadInboxThreads.append( InboxThreadSummary( @@ -786,7 +797,8 @@ final class HomeViewModel { containsPatch: thread.root.patch != nil || thread.subject.localizedCaseInsensitiveContains("[patch"), isUnread: InboxReadStateStore.isUnread( threadID: "\(mailingList.rid)#\(InboxThreadSummary.normalizationKey(for: thread.subject))", - lastActivityAt: thread.updated + lastActivityAt: thread.updated, + defaults: defaults ) ) return summary.isUnread ? summary : nil @@ -944,7 +956,8 @@ final class HomeViewModel { }.count : nil, updatedAt: .now - ) + ), + accountID: accountID ) } @@ -966,7 +979,7 @@ final class HomeViewModel { bannerSummary: snapshot.bannerSummary, updatedAt: .now ) - SystemStatusWidgetSnapshotStore.save(widgetSnapshot) + SystemStatusWidgetSnapshotStore.save(widgetSnapshot, accountID: accountID) } nonisolated static func buildItems(from jobs: [HomeJobPayload]) -> [HomeBuildItem] { diff --git a/Hutch/Views/Inbox/InboxView.swift b/Hutch/Views/Inbox/InboxView.swift index f8753d7..c5299ec 100644 --- a/Hutch/Views/Inbox/InboxView.swift +++ b/Hutch/Views/Inbox/InboxView.swift @@ -22,7 +22,11 @@ struct InboxView: View { if let viewModel { vm = viewModel } else { - let newViewModel = InboxViewModel(client: appState.client) + let newViewModel = InboxViewModel( + client: appState.client, + defaults: appState.accountDefaults, + accountID: appState.activeAccountID + ) viewModel = newViewModel vm = newViewModel } diff --git a/Hutch/Views/Inbox/InboxViewModel.swift b/Hutch/Views/Inbox/InboxViewModel.swift index a01f836..8fff2dc 100644 --- a/Hutch/Views/Inbox/InboxViewModel.swift +++ b/Hutch/Views/Inbox/InboxViewModel.swift @@ -72,6 +72,8 @@ final class InboxViewModel { var searchText = "" private let client: SRHTClient + private let defaults: UserDefaults + private let accountID: String private let listThreadFetchLimit = 10 private let listFetchConcurrencyLimit = 4 @@ -121,8 +123,10 @@ final class InboxViewModel { } """ - init(client: SRHTClient) { + init(client: SRHTClient, defaults: UserDefaults, accountID: String) { self.client = client + self.defaults = defaults + self.accountID = accountID } func loadThreads() async { @@ -143,7 +147,7 @@ final class InboxViewModel { } return lhs.lastActivityAt > rhs.lastActivityAt } - NeedsAttentionSnapshotStore.update(unreadInboxThreads: threads.count) + NeedsAttentionSnapshotStore.update(unreadInboxThreads: threads.count, accountID: accountID) } catch { inboxListLogger.error("Inbox request failed") self.error = "Failed to load inbox" @@ -152,9 +156,9 @@ final class InboxViewModel { func markThreadRead(_ thread: InboxThreadSummary) { let viewedAt = max(Date(), thread.lastActivityAt) - InboxReadStateStore.markViewed(viewedAt, for: thread.id) + InboxReadStateStore.markViewed(viewedAt, for: thread.id, defaults: defaults) threads.removeAll { $0.id == thread.id } - NeedsAttentionSnapshotStore.adjustUnreadInboxThreads(by: -1) + NeedsAttentionSnapshotStore.adjustUnreadInboxThreads(by: -1, accountID: accountID) } func markAllThreadsRead() { @@ -162,17 +166,17 @@ final class InboxViewModel { let viewedAt = Date() for thread in threads where thread.isUnread { - InboxReadStateStore.markViewed(max(viewedAt, thread.lastActivityAt), for: thread.id) + InboxReadStateStore.markViewed(max(viewedAt, thread.lastActivityAt), for: thread.id, defaults: defaults) } threads.removeAll { $0.isUnread } - NeedsAttentionSnapshotStore.update(unreadInboxThreads: threads.count) + NeedsAttentionSnapshotStore.update(unreadInboxThreads: threads.count, accountID: accountID) } func markThreadUnread(_ thread: InboxThreadSummary) { - InboxReadStateStore.markUnread(for: thread.id) + InboxReadStateStore.markUnread(for: thread.id, defaults: defaults) updateThread(thread, isUnread: true) - NeedsAttentionSnapshotStore.adjustUnreadInboxThreads(by: 1) + NeedsAttentionSnapshotStore.adjustUnreadInboxThreads(by: 1, accountID: accountID) } func toggleThreadReadState(_ thread: InboxThreadSummary) { @@ -291,7 +295,7 @@ final class InboxViewModel { return response.list.threads.results.prefix(listThreadFetchLimit).map { thread in let groupingKey = "\(mailingList.rid)#\(thread.subject.replacingOccurrences(of: #"\s+"#, with: " ", options: .regularExpression).trimmingCharacters(in: .whitespacesAndNewlines).replacingOccurrences(of: #"^(?:(?:re|fwd?)\s*:\s*)+"#, with: "", options: [.regularExpression, .caseInsensitive]).lowercased())" - let isUnread = InboxReadStateStore.isUnread(threadID: groupingKey, lastActivityAt: thread.updated) + let isUnread = InboxReadStateStore.isUnread(threadID: groupingKey, lastActivityAt: thread.updated, defaults: defaults) return InboxThreadSummary( rootEmailID: thread.root.id, rootMessageID: thread.root.messageID, diff --git a/Hutch/Views/Lookup/LookupView.swift b/Hutch/Views/Lookup/LookupView.swift index 6cefcf1..e53dd33 100644 --- a/Hutch/Views/Lookup/LookupView.swift +++ b/Hutch/Views/Lookup/LookupView.swift @@ -339,7 +339,7 @@ struct LookupView: View { .navigationTitle("Look Up") .task { if viewModel == nil { - viewModel = LookupViewModel(client: appState.client, appState: appState) + viewModel = LookupViewModel(client: appState.client, appState: appState, defaults: appState.accountDefaults) } } } @@ -440,16 +440,16 @@ struct LookupView: View { ThreadDetailView( thread: thread, onViewed: { - InboxReadStateStore.markViewed(max(Date(), thread.lastActivityAt), for: thread.id) - NeedsAttentionSnapshotStore.adjustUnreadInboxThreads(by: -1) + InboxReadStateStore.markViewed(max(Date(), thread.lastActivityAt), for: thread.id, defaults: appState.accountDefaults) + NeedsAttentionSnapshotStore.adjustUnreadInboxThreads(by: -1, accountID: appState.activeAccountID) }, onMarkRead: { - InboxReadStateStore.markViewed(max(Date(), thread.lastActivityAt), for: thread.id) - NeedsAttentionSnapshotStore.adjustUnreadInboxThreads(by: -1) + InboxReadStateStore.markViewed(max(Date(), thread.lastActivityAt), for: thread.id, defaults: appState.accountDefaults) + NeedsAttentionSnapshotStore.adjustUnreadInboxThreads(by: -1, accountID: appState.activeAccountID) }, onMarkUnread: { - InboxReadStateStore.markUnread(for: thread.id) - NeedsAttentionSnapshotStore.adjustUnreadInboxThreads(by: 1) + InboxReadStateStore.markUnread(for: thread.id, defaults: appState.accountDefaults) + NeedsAttentionSnapshotStore.adjustUnreadInboxThreads(by: 1, accountID: appState.activeAccountID) } ) case .manPageBrowser: diff --git a/Hutch/Views/Lookup/UserProfileView.swift b/Hutch/Views/Lookup/UserProfileView.swift index 068e05f..84375de 100644 --- a/Hutch/Views/Lookup/UserProfileView.swift +++ b/Hutch/Views/Lookup/UserProfileView.swift @@ -2,7 +2,7 @@ import SwiftUI struct UserProfileView: View { @Environment(AppState.self) private var appState - @AppStorage(AppStorageKeys.contributionGraphsEnabled) private var contributionGraphsEnabled = true + @AppStorage(AppStorageKeys.contributionGraphsEnabled, store: .standard) private var contributionGraphsEnabled = true let user: User @State private var profileViewModel: UserProfileViewModel? diff --git a/Hutch/Views/More/AccountSwitcherView.swift b/Hutch/Views/More/AccountSwitcherView.swift index aefcc82..5fde9c5 100644 --- a/Hutch/Views/More/AccountSwitcherView.swift +++ b/Hutch/Views/More/AccountSwitcherView.swift @@ -7,21 +7,35 @@ struct AccountSwitcherView: View { @State private var showAddAccount = false @State private var isSwitching = false @State private var switchError: String? + @State private var pendingRemoval: AccountEntry? var body: some View { NavigationStack { List { Section { ForEach(appState.accounts) { account in + let isActive = account.id == appState.activeAccountID Button { - guard account.id != appState.activeAccountID else { return } + guard !isActive else { return } switchTo(account) } label: { - HStack { - Text("~\(account.username)") - .foregroundStyle(.primary) + HStack(spacing: 12) { + if isActive { + Image(systemName: "person.crop.circle.fill") + .foregroundStyle(.tint) + } else { + Image(systemName: "person.crop.circle") + .foregroundStyle(.secondary) + } + VStack(alignment: .leading, spacing: 2) { + Text("~\(account.username)") + .foregroundStyle(.primary) + Text(isActive ? "Active Account" : "Tap to switch") + .font(.caption) + .foregroundStyle(.secondary) + } Spacer() - if account.id == appState.activeAccountID { + if isActive { Image(systemName: "checkmark") .foregroundStyle(.tint) } @@ -31,8 +45,7 @@ struct AccountSwitcherView: View { } .onDelete { indexSet in for index in indexSet { - let account = appState.accounts[index] - Task { await appState.removeAccount(id: account.id) } + pendingRemoval = appState.accounts[index] } } } @@ -71,6 +84,28 @@ struct AccountSwitcherView: View { } message: { Text(switchError ?? "") } + .alert( + "Remove Account?", + isPresented: Binding( + get: { pendingRemoval != nil }, + set: { isPresented in + if !isPresented { + pendingRemoval = nil + } + } + ) + ) { + Button("Cancel", role: .cancel) {} + Button("Remove", role: .destructive) { + guard let pendingRemoval else { return } + Task { await appState.removeAccount(id: pendingRemoval.id) } + self.pendingRemoval = nil + } + } message: { + if let pendingRemoval { + Text("~\(pendingRemoval.username) and its isolated local cache will be removed from this device.") + } + } .sheet(isPresented: $showAddAccount) { AddAccountView() } diff --git a/Hutch/Views/More/ProfileView.swift b/Hutch/Views/More/ProfileView.swift index 673b298..db3cb53 100644 --- a/Hutch/Views/More/ProfileView.swift +++ b/Hutch/Views/More/ProfileView.swift @@ -7,7 +7,7 @@ private let profileBioMarkdownOptions = AttributedString.MarkdownParsingOptions( struct ProfileView: View { @Environment(AppState.self) private var appState - @AppStorage(AppStorageKeys.contributionGraphsEnabled) private var contributionGraphsEnabled = true + @AppStorage(AppStorageKeys.contributionGraphsEnabled, store: .standard) private var contributionGraphsEnabled = true @State private var viewModel: SettingsViewModel? @State private var contributionViewModel: UserProfileViewModel? @State private var pendingDestructiveAction: ProfileDestructiveAction? diff --git a/Hutch/Views/Pastes/PasteListView.swift b/Hutch/Views/Pastes/PasteListView.swift index ffd49ab..0fdddd7 100644 --- a/Hutch/Views/Pastes/PasteListView.swift +++ b/Hutch/Views/Pastes/PasteListView.swift @@ -1,7 +1,7 @@ import SwiftUI struct PasteListView: View { - @AppStorage(AppStorageKeys.swipeActionsEnabled) private var swipeActionsEnabled = true + @AppStorage(AppStorageKeys.swipeActionsEnabled, store: .standard) private var swipeActionsEnabled = true @Environment(AppState.self) private var appState @State private var viewModel: PasteListViewModel? @State private var showCreatePasteSheet = false diff --git a/Hutch/Views/Projects/ProjectDetailView.swift b/Hutch/Views/Projects/ProjectDetailView.swift index 11758ae..dc8b24d 100644 --- a/Hutch/Views/Projects/ProjectDetailView.swift +++ b/Hutch/Views/Projects/ProjectDetailView.swift @@ -22,7 +22,7 @@ struct ProjectDetailView: View { private var isPinnedToHome: Bool { _ = pinChangeCount guard let currentUserKey else { return false } - return ProjectPinStore.isPinned(projectID: displayedProject.id, for: currentUserKey) + return ProjectPinStore.isPinned(projectID: displayedProject.id, for: currentUserKey, defaults: appState.accountDefaults) } var body: some View { @@ -243,7 +243,7 @@ struct ProjectDetailView: View { private func togglePinnedState() { guard let currentUserKey else { return } - ProjectPinStore.togglePin(projectID: displayedProject.id, for: currentUserKey) + ProjectPinStore.togglePin(projectID: displayedProject.id, for: currentUserKey, defaults: appState.accountDefaults) pinChangeCount += 1 } diff --git a/Hutch/Views/Projects/ProjectMailingListView.swift b/Hutch/Views/Projects/ProjectMailingListView.swift index 832c889..478e471 100644 --- a/Hutch/Views/Projects/ProjectMailingListView.swift +++ b/Hutch/Views/Projects/ProjectMailingListView.swift @@ -36,6 +36,8 @@ final class MailingListDetailViewModel { private let mailingList: InboxMailingListReference private let client: SRHTClient + private let defaults: UserDefaults + private let accountID: String private static let listThreadsQuery = """ query projectMailingListThreads($rid: ID!) { @@ -57,9 +59,11 @@ final class MailingListDetailViewModel { } """ - init(mailingList: InboxMailingListReference, client: SRHTClient) { + init(mailingList: InboxMailingListReference, client: SRHTClient, defaults: UserDefaults, accountID: String) { self.mailingList = mailingList self.client = client + self.defaults = defaults + self.accountID = accountID } var filteredThreads: [InboxThreadSummary] { @@ -90,15 +94,15 @@ final class MailingListDetailViewModel { func markThreadRead(_ thread: InboxThreadSummary) { let viewedAt = max(Date(), thread.lastActivityAt) - InboxReadStateStore.markViewed(viewedAt, for: thread.id) + InboxReadStateStore.markViewed(viewedAt, for: thread.id, defaults: defaults) updateThread(thread, isUnread: false) - NeedsAttentionSnapshotStore.adjustUnreadInboxThreads(by: -1) + NeedsAttentionSnapshotStore.adjustUnreadInboxThreads(by: -1, accountID: accountID) } func markThreadUnread(_ thread: InboxThreadSummary) { - InboxReadStateStore.markUnread(for: thread.id) + InboxReadStateStore.markUnread(for: thread.id, defaults: defaults) updateThread(thread, isUnread: true) - NeedsAttentionSnapshotStore.adjustUnreadInboxThreads(by: 1) + NeedsAttentionSnapshotStore.adjustUnreadInboxThreads(by: 1, accountID: accountID) } private func makeSummary(from thread: ProjectMailingListThreadPayload) -> InboxThreadSummary { @@ -124,7 +128,7 @@ final class MailingListDetailViewModel { messageCount: thread.replies + 1, repo: nil, containsPatch: thread.root.patch != nil || thread.subject.localizedCaseInsensitiveContains("[patch"), - isUnread: InboxReadStateStore.isUnread(threadID: threadID, lastActivityAt: thread.updated) + isUnread: InboxReadStateStore.isUnread(threadID: threadID, lastActivityAt: thread.updated, defaults: defaults) ) } @@ -238,7 +242,12 @@ struct MailingListDetailView: View { .navigationBarTitleDisplayMode(.inline) .task { if viewModel == nil { - let viewModel = MailingListDetailViewModel(mailingList: mailingList, client: appState.client) + let viewModel = MailingListDetailViewModel( + mailingList: mailingList, + client: appState.client, + defaults: appState.accountDefaults, + accountID: appState.activeAccountID + ) self.viewModel = viewModel await viewModel.loadThreads() } @@ -261,16 +270,16 @@ struct MailingListDetailView: View { ThreadDetailView( thread: thread, onViewed: { - InboxReadStateStore.markViewed(max(Date(), thread.lastActivityAt), for: thread.id) - NeedsAttentionSnapshotStore.adjustUnreadInboxThreads(by: -1) + InboxReadStateStore.markViewed(max(Date(), thread.lastActivityAt), for: thread.id, defaults: appState.accountDefaults) + NeedsAttentionSnapshotStore.adjustUnreadInboxThreads(by: -1, accountID: appState.activeAccountID) }, onMarkRead: { - InboxReadStateStore.markViewed(max(Date(), thread.lastActivityAt), for: thread.id) - NeedsAttentionSnapshotStore.adjustUnreadInboxThreads(by: -1) + InboxReadStateStore.markViewed(max(Date(), thread.lastActivityAt), for: thread.id, defaults: appState.accountDefaults) + NeedsAttentionSnapshotStore.adjustUnreadInboxThreads(by: -1, accountID: appState.activeAccountID) }, onMarkUnread: { - InboxReadStateStore.markUnread(for: thread.id) - NeedsAttentionSnapshotStore.adjustUnreadInboxThreads(by: 1) + InboxReadStateStore.markUnread(for: thread.id, defaults: appState.accountDefaults) + NeedsAttentionSnapshotStore.adjustUnreadInboxThreads(by: 1, accountID: appState.activeAccountID) } ) } label: { diff --git a/Hutch/Views/Repositories/RepositoryListView.swift b/Hutch/Views/Repositories/RepositoryListView.swift index f5ca716..68dd0b0 100644 --- a/Hutch/Views/Repositories/RepositoryListView.swift +++ b/Hutch/Views/Repositories/RepositoryListView.swift @@ -59,7 +59,7 @@ struct RepositoryListView: View { } .task { if viewModel == nil { - viewModel = RepositoryListViewModel(client: appState.client) + viewModel = RepositoryListViewModel(client: appState.client, defaults: appState.accountDefaults) } } } diff --git a/Hutch/Views/Settings/SettingsView.swift b/Hutch/Views/Settings/SettingsView.swift index 5abac90..b6ff8dd 100644 --- a/Hutch/Views/Settings/SettingsView.swift +++ b/Hutch/Views/Settings/SettingsView.swift @@ -2,10 +2,10 @@ import SwiftUI struct SettingsView: View { @Environment(AppState.self) private var appState - @AppStorage(AppStorageKeys.appTheme) private var appTheme: AppTheme = .system - @AppStorage(AppStorageKeys.displayDensity) private var displayDensity: DisplayDensity = .standard - @AppStorage(AppStorageKeys.swipeActionsEnabled) private var swipeActionsEnabled = true - @AppStorage(AppStorageKeys.contributionGraphsEnabled) private var contributionGraphsEnabled = true + @AppStorage(AppStorageKeys.appTheme, store: .standard) private var appTheme: AppTheme = .system + @AppStorage(AppStorageKeys.displayDensity, store: .standard) private var displayDensity: DisplayDensity = .standard + @AppStorage(AppStorageKeys.swipeActionsEnabled, store: .standard) private var swipeActionsEnabled = true + @AppStorage(AppStorageKeys.contributionGraphsEnabled, store: .standard) private var contributionGraphsEnabled = true @State private var pendingDestructiveAction: SettingsDestructiveAction? var body: some View { @@ -89,7 +89,25 @@ struct SettingsView: View { HStack { Image(systemName: "key.fill") .foregroundStyle(.secondary) - Text("Personal access token in use") + VStack(alignment: .leading, spacing: 2) { + Text(appState.currentUser?.canonicalName ?? "No active account") + Text("\(appState.accounts.count) saved account\(appState.accounts.count == 1 ? "" : "s")") + .font(.caption) + .foregroundStyle(.secondary) + } + } + .alignmentGuide(.listRowSeparatorLeading) { _ in 0 } + + NavigationLink { + AccountSwitcherView() + } label: { + Label("Manage Accounts", systemImage: "person.2") + } + + HStack { + Image(systemName: "lock.shield") + .foregroundStyle(.secondary) + Text("Tokens are stored separately per account in the iOS keychain") .font(.subheadline) .foregroundStyle(.secondary) } @@ -105,7 +123,7 @@ struct SettingsView: View { } header: { Text("Authentication") } footer: { - Text("Hutch stores your SourceHut token in the iOS keychain. Reset App Data removes saved token data, local settings, cached responses, cookies, and embedded web data on this device.") + Text("Account switching keeps local caches and saved state isolated per account. Sign Out removes all saved accounts from this device. Reset App Data also clears local settings, cached responses, cookies, and embedded web data.") } } diff --git a/Hutch/Views/Tickets/TicketListView.swift b/Hutch/Views/Tickets/TicketListView.swift index d2548d7..ccd06fe 100644 --- a/Hutch/Views/Tickets/TicketListView.swift +++ b/Hutch/Views/Tickets/TicketListView.swift @@ -4,7 +4,7 @@ struct TicketListView: View { let onTrackerUpdated: (TrackerSummary) -> Void let onTrackerDeleted: (TrackerSummary) -> Void - @AppStorage(AppStorageKeys.swipeActionsEnabled) private var swipeActionsEnabled = true + @AppStorage(AppStorageKeys.swipeActionsEnabled, store: .standard) private var swipeActionsEnabled = true @Environment(AppState.self) private var appState @Environment(\.dismiss) private var dismiss @State private var tracker: TrackerSummary @@ -232,7 +232,8 @@ struct TicketListView: View { trackerName: tracker.name, trackerId: tracker.id, trackerRid: tracker.rid, - client: appState.client + client: appState.client, + defaults: appState.accountDefaults ) viewModel = vm trackerManagementViewModel = TrackerManagementViewModel(tracker: tracker, client: appState.client) |
