summaryrefslogtreecommitdiff
path: root/Hutch/Views
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-04-12 23:19:20 -0500
committerChristian Cleberg <[email protected]>2026-04-12 23:19:20 -0500
commitee9f2904aa319231b7047fca3cb069f0c07019cd (patch)
tree69a18234512c63f38f0d477e0023969698ec3c9e /Hutch/Views
parent72f860e0e171f6aac4e7d896e7a571147dbcb5c5 (diff)
downloadhutch-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.swift4
-rw-r--r--Hutch/Views/Home/HomeView.swift8
-rw-r--r--Hutch/Views/Home/HomeViewModel.swift29
-rw-r--r--Hutch/Views/Inbox/InboxView.swift6
-rw-r--r--Hutch/Views/Inbox/InboxViewModel.swift22
-rw-r--r--Hutch/Views/Lookup/LookupView.swift14
-rw-r--r--Hutch/Views/Lookup/UserProfileView.swift2
-rw-r--r--Hutch/Views/More/AccountSwitcherView.swift49
-rw-r--r--Hutch/Views/More/ProfileView.swift2
-rw-r--r--Hutch/Views/Pastes/PasteListView.swift2
-rw-r--r--Hutch/Views/Projects/ProjectDetailView.swift4
-rw-r--r--Hutch/Views/Projects/ProjectMailingListView.swift35
-rw-r--r--Hutch/Views/Repositories/RepositoryListView.swift2
-rw-r--r--Hutch/Views/Settings/SettingsView.swift30
-rw-r--r--Hutch/Views/Tickets/TicketListView.swift5
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)