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 { |
| 26 | 26 | } |
| 27 | 27 | } |
| 28 | 28 | .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 |
| 33 | 38 | } |
| 39 | |
| 40 | await vm.loadDashboard() |
| 34 | 41 | } |
| 35 | 42 | } |
| 36 | 43 | |
| @@ -214,17 +221,8 @@ private struct HomeInboxToolbarIcon: View { |
| 214 | 221 | let hasUnreadThreads: Bool |
| 215 | 222 | |
| 216 | 223 | 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") |
| 228 | 226 | } |
| 229 | 227 | } |
| 230 | 228 | |
Hutch/Views/Inbox/InboxView.swift
+31 −5
| @@ -2,6 +2,7 @@ import SwiftUI |
| 2 | 2 | |
| 3 | 3 | struct InboxView: View { |
| 4 | 4 | @Environment(AppState.self) private var appState |
| 5 | @Environment(\.scenePhase) private var scenePhase |
| 5 | 6 | @State private var viewModel: InboxViewModel? |
| 6 | 7 | @State private var selectedThreadID: InboxThreadSummary.ID? |
| 7 | 8 | @State private var selectedThreadSnapshot: InboxThreadSummary? |
| @@ -17,10 +18,21 @@ struct InboxView: View { |
| 17 | 18 | } |
| 18 | 19 | .navigationTitle("Inbox") |
| 19 | 20 | .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() |
| 24 | 36 | } |
| 25 | 37 | } |
| 26 | 38 | } |
| @@ -51,6 +63,20 @@ struct InboxView: View { |
| 51 | 63 | prompt: "Search inbox" |
| 52 | 64 | ) |
| 53 | 65 | .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 | } |
| 54 | 80 | .overlay { |
| 55 | 81 | if viewModel.isLoading, viewModel.threads.isEmpty { |
| 56 | 82 | SRHTLoadingStateView(message: "Loading inbox…") |
| @@ -66,7 +92,7 @@ struct InboxView: View { |
| 66 | 92 | ContentUnavailableView( |
| 67 | 93 | "Inbox Zero", |
| 68 | 94 | systemImage: "tray", |
| 69 | | description: Text("Unread threads will appear here.") |
| 95 | description: Text("You're up to date.") |
| 70 | 96 | ) |
| 71 | 97 | } |
| 72 | 98 | } |
Hutch/Views/Inbox/InboxViewModel.swift
+16
| @@ -150,6 +150,18 @@ final class InboxViewModel { |
| 150 | 150 | NeedsAttentionSnapshotStore.adjustUnreadInboxThreads(by: -1) |
| 151 | 151 | } |
| 152 | 152 | |
| 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 | |
| 153 | 165 | func markThreadUnread(_ thread: InboxThreadSummary) { |
| 154 | 166 | InboxReadStateStore.markUnread(for: thread.id) |
| 155 | 167 | updateThread(thread, isUnread: true) |
| @@ -178,6 +190,10 @@ final class InboxViewModel { |
| 178 | 190 | } |
| 179 | 191 | } |
| 180 | 192 | |
| 193 | var hasUnreadThreads: Bool { |
| 194 | threads.contains(where: \.isUnread) |
| 195 | } |
| 196 | |
| 181 | 197 | private func fetchSubscriptions() async throws -> [InboxActivitySubscription] { |
| 182 | 198 | var subscriptions: [InboxActivitySubscription] = [] |
| 183 | 199 | var cursor: String? |