krz/hutch
an ios client for sourcehut
clone: git clone https://gitbay.org/krz/hutch.git
109b0ea7e859de2d8b5a4dc22bf0472fd3b08eaa
verified · cmc
author: Christian Cleberg <hello@cleberg.net> · 2026-04-01T17:15:23Z
Hutch/Views/Home/HomeView.swift | 28 ++++++++++++-------------- Hutch/Views/Inbox/InboxView.swift | 36 +++++++++++++++++++++++++++++----- Hutch/Views/Inbox/InboxViewModel.swift | 16 +++++++++++++++ 3 files changed, 60 insertions(+), 20 deletions(-) @@ -26,11 +26,18 @@ struct HomeView: View { } } .task { - if viewModel == nil, let currentUser = appState.currentUser { - let vm = HomeViewModel(currentUser: currentUser, client: appState.client) - viewModel = vm - await vm.loadDashboard() + guard let currentUser = appState.currentUser else { return } + + let vm: HomeViewModel + if let viewModel { + vm = viewModel + } else { + let newViewModel = HomeViewModel(currentUser: currentUser, client: appState.client) + viewModel = newViewModel + vm = newViewModel } + + await vm.loadDashboard() } } @@ -214,17 +221,8 @@ private struct HomeInboxToolbarIcon: View { let hasUnreadThreads: Bool var body: some View { - ZStack(alignment: .topTrailing) { - Image(systemName: hasUnreadThreads ? "tray.fill" : "tray") - - if hasUnreadThreads { - Circle() - .fill(.blue) - .frame(width: 9, height: 9) - .offset(x: 4, y: -2) - } - } - .accessibilityLabel(hasUnreadThreads ? "Inbox, unread messages" : "Inbox") + Image(systemName: hasUnreadThreads ? "tray.fill" : "tray") + .accessibilityLabel(hasUnreadThreads ? "Inbox, unread messages" : "Inbox") } } @@ -2,6 +2,7 @@ import SwiftUI struct InboxView: View { @Environment(AppState.self) private var appState + @Environment(\.scenePhase) private var scenePhase @State private var viewModel: InboxViewModel? @State private var selectedThreadID: InboxThreadSummary.ID? @State private var selectedThreadSnapshot: InboxThreadSummary? @@ -17,10 +18,21 @@ struct InboxView: View { } .navigationTitle("Inbox") .task { - if viewModel == nil { - let vm = InboxViewModel(client: appState.client) - viewModel = vm - await vm.loadThreads() + let vm: InboxViewModel + if let viewModel { + vm = viewModel + } else { + let newViewModel = InboxViewModel(client: appState.client) + viewModel = newViewModel + vm = newViewModel + } + + await vm.loadThreads() + } + .onChange(of: scenePhase) { _, newPhase in + guard newPhase == .active, let viewModel, !isShowingThreadDetail else { return } + Task { + await viewModel.loadThreads() } } } @@ -51,6 +63,20 @@ struct InboxView: View { prompt: "Search inbox" ) .listStyle(.plain) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + if viewModel.hasUnreadThreads { + Button("Mark All Read") { + withAnimation(.easeInOut(duration: 0.2)) { + viewModel.markAllThreadsRead() + } + Task { + await viewModel.loadThreads() + } + } + } + } + } .overlay { if viewModel.isLoading, viewModel.threads.isEmpty { SRHTLoadingStateView(message: "Loading inbox…") @@ -66,7 +92,7 @@ struct InboxView: View { ContentUnavailableView( "Inbox Zero", systemImage: "tray", - description: Text("Unread threads will appear here.") + description: Text("You're up to date.") ) } } @@ -150,6 +150,18 @@ final class InboxViewModel { NeedsAttentionSnapshotStore.adjustUnreadInboxThreads(by: -1) } + func markAllThreadsRead() { + guard !threads.isEmpty else { return } + + let viewedAt = Date() + for thread in threads where thread.isUnread { + InboxReadStateStore.markViewed(max(viewedAt, thread.lastActivityAt), for: thread.id) + } + + threads.removeAll { $0.isUnread } + NeedsAttentionSnapshotStore.update(unreadInboxThreads: threads.count) + } + func markThreadUnread(_ thread: InboxThreadSummary) { InboxReadStateStore.markUnread(for: thread.id) updateThread(thread, isUnread: true) @@ -178,6 +190,10 @@ final class InboxViewModel { } } + var hasUnreadThreads: Bool { + threads.contains(where: \.isUnread) + } + private func fetchSubscriptions() async throws -> [InboxActivitySubscription] { var subscriptions: [InboxActivitySubscription] = [] var cursor: String?