Commit d151e20a46

d151e20a46f18addc04dcc976dd41307266d7e6d

parent: 9d4296fb07

Unsigned

cmc <hello@cleberg.net> · 2026-04-01 17:15 UTC

fix: fixed inbox's inconsistent UX and adds a Mark All Read button

Fixes: https://todo.sr.ht/~ccleberg/Hutch/10

Layout: unified · split

Hutch/Views/Home/HomeView.swift +13 −15
@@ -26,11 +26,18 @@ struct HomeView: View {
2626 }
2727 }
2828 .task {
29 if viewModel == nil, let currentUser = appState.currentUser {
30 let vm = HomeViewModel(currentUser: currentUser, client: appState.client)
31 viewModel = vm
32 await vm.loadDashboard()
29 guard let currentUser = appState.currentUser else { return }
30
31 let vm: HomeViewModel
32 if let viewModel {
33 vm = viewModel
34 } else {
35 let newViewModel = HomeViewModel(currentUser: currentUser, client: appState.client)
36 viewModel = newViewModel
37 vm = newViewModel
3338 }
39
40 await vm.loadDashboard()
3441 }
3542 }
3643
@@ -214,17 +221,8 @@ private struct HomeInboxToolbarIcon: View {
214221 let hasUnreadThreads: Bool
215222
216223 var body: some View {
217 ZStack(alignment: .topTrailing) {
218 Image(systemName: hasUnreadThreads ? "tray.fill" : "tray")
219
220 if hasUnreadThreads {
221 Circle()
222 .fill(.blue)
223 .frame(width: 9, height: 9)
224 .offset(x: 4, y: -2)
225 }
226 }
227 .accessibilityLabel(hasUnreadThreads ? "Inbox, unread messages" : "Inbox")
224 Image(systemName: hasUnreadThreads ? "tray.fill" : "tray")
225 .accessibilityLabel(hasUnreadThreads ? "Inbox, unread messages" : "Inbox")
228226 }
229227}
230228
Hutch/Views/Inbox/InboxView.swift +31 −5
@@ -2,6 +2,7 @@ import SwiftUI
22
33struct InboxView: View {
44 @Environment(AppState.self) private var appState
5 @Environment(\.scenePhase) private var scenePhase
56 @State private var viewModel: InboxViewModel?
67 @State private var selectedThreadID: InboxThreadSummary.ID?
78 @State private var selectedThreadSnapshot: InboxThreadSummary?
@@ -17,10 +18,21 @@ struct InboxView: View {
1718 }
1819 .navigationTitle("Inbox")
1920 .task {
20 if viewModel == nil {
21 let vm = InboxViewModel(client: appState.client)
22 viewModel = vm
23 await vm.loadThreads()
21 let vm: InboxViewModel
22 if let viewModel {
23 vm = viewModel
24 } else {
25 let newViewModel = InboxViewModel(client: appState.client)
26 viewModel = newViewModel
27 vm = newViewModel
28 }
29
30 await vm.loadThreads()
31 }
32 .onChange(of: scenePhase) { _, newPhase in
33 guard newPhase == .active, let viewModel, !isShowingThreadDetail else { return }
34 Task {
35 await viewModel.loadThreads()
2436 }
2537 }
2638 }
@@ -51,6 +63,20 @@ struct InboxView: View {
5163 prompt: "Search inbox"
5264 )
5365 .listStyle(.plain)
66 .toolbar {
67 ToolbarItem(placement: .topBarTrailing) {
68 if viewModel.hasUnreadThreads {
69 Button("Mark All Read") {
70 withAnimation(.easeInOut(duration: 0.2)) {
71 viewModel.markAllThreadsRead()
72 }
73 Task {
74 await viewModel.loadThreads()
75 }
76 }
77 }
78 }
79 }
5480 .overlay {
5581 if viewModel.isLoading, viewModel.threads.isEmpty {
5682 SRHTLoadingStateView(message: "Loading inbox…")
@@ -66,7 +92,7 @@ struct InboxView: View {
6692 ContentUnavailableView(
6793 "Inbox Zero",
6894 systemImage: "tray",
69 description: Text("Unread threads will appear here.")
95 description: Text("You're up to date.")
7096 )
7197 }
7298 }
Hutch/Views/Inbox/InboxViewModel.swift +16
@@ -150,6 +150,18 @@ final class InboxViewModel {
150150 NeedsAttentionSnapshotStore.adjustUnreadInboxThreads(by: -1)
151151 }
152152
153 func markAllThreadsRead() {
154 guard !threads.isEmpty else { return }
155
156 let viewedAt = Date()
157 for thread in threads where thread.isUnread {
158 InboxReadStateStore.markViewed(max(viewedAt, thread.lastActivityAt), for: thread.id)
159 }
160
161 threads.removeAll { $0.isUnread }
162 NeedsAttentionSnapshotStore.update(unreadInboxThreads: threads.count)
163 }
164
153165 func markThreadUnread(_ thread: InboxThreadSummary) {
154166 InboxReadStateStore.markUnread(for: thread.id)
155167 updateThread(thread, isUnread: true)
@@ -178,6 +190,10 @@ final class InboxViewModel {
178190 }
179191 }
180192
193 var hasUnreadThreads: Bool {
194 threads.contains(where: \.isUnread)
195 }
196
181197 private func fetchSubscriptions() async throws -> [InboxActivitySubscription] {
182198 var subscriptions: [InboxActivitySubscription] = []
183199 var cursor: String?