Commit 56a1acaeb5

56a1acaeb5be905e989885f3dd4c1067c3b35472

parent: e02d4c17d1

Unsigned

cmc <hello@cleberg.net> · 2026-08-07 06:44 UTC

Let users clear recent activity on Home (#11)

Add a Clear button in the Home Recent section header to drop all
entries, plus swipe-to-delete on each row to forget a single item
(gated on the existing swipe-actions setting). Back it with
RecentActivityStore.clear and remove(id:), with unit tests.

Layout: unified · split

Hutch/Views/Home/HomeView.swift +32 −1
@@ -5,6 +5,8 @@ struct HomeView: View {
55 @Environment(\.scenePhase) private var scenePhase
66 @AppStorage(AppStorageKeys.homeFailedBuildLookbackDays, store: .standard)
77 private var failedBuildLookbackDays = HomeViewModel.defaultFailedBuildLookbackDays
8 @AppStorage(AppStorageKeys.swipeActionsEnabled, store: .standard)
9 private var swipeActionsEnabled = true
810 @State private var viewModel: HomeViewModel?
911 @State private var recentItems: [RecentActivityEntry] = []
1012 @State private var isOpeningRecentItem = false
@@ -118,7 +120,7 @@ struct HomeView: View {
118120 @ViewBuilder
119121 private var recentSection: some View {
120122 if !recentItems.isEmpty {
121 Section("Recent") {
123 Section {
122124 ForEach(recentItems.prefix(3)) { item in
123125 Button {
124126 openRecentItem(item)
@@ -128,12 +130,41 @@ struct HomeView: View {
128130 .buttonStyle(.plain)
129131 .disabled(isOpeningRecentItem)
130132 .listRowSeparator(.hidden)
133 .swipeActions(edge: .trailing, allowsFullSwipe: true) {
134 if swipeActionsEnabled {
135 Button(role: .destructive) {
136 removeRecentItem(item)
137 } label: {
138 Label("Remove", systemImage: "trash")
139 }
140 }
141 }
131142 }
132143 .themedRow()
144 } header: {
145 HStack {
146 Text("Recent")
147 Spacer()
148 Button("Clear") {
149 clearRecentActivity()
150 }
151 .font(.caption)
152 .textCase(nil)
153 }
133154 }
134155 }
135156 }
136157
158 private func removeRecentItem(_ item: RecentActivityEntry) {
159 RecentActivityStore.remove(id: item.id, defaults: appState.accountDefaults)
160 loadRecentActivity()
161 }
162
163 private func clearRecentActivity() {
164 RecentActivityStore.clear(defaults: appState.accountDefaults)
165 loadRecentActivity()
166 }
167
137168 private func buildsSection(_ viewModel: HomeViewModel) -> some View {
138169 Section("Builds") {
139170 Button {
Hutch/Views/Home/RecentActivityStore.swift +10
@@ -129,6 +129,16 @@ enum RecentActivityStore {
129129 )
130130 }
131131
132 static func remove(id: String, defaults: UserDefaults) {
133 var entries = load(defaults: defaults)
134 entries.removeAll { $0.id == id }
135 save(entries, defaults: defaults)
136 }
137
138 static func clear(defaults: UserDefaults) {
139 defaults.removeObject(forKey: AppStorageKeys.recentActivity)
140 }
141
132142 private static func record(_ entry: RecentActivityEntry, defaults: UserDefaults) {
133143 var entries = load(defaults: defaults)
134144 entries.removeAll { $0.id == entry.id }
HutchTests/RecentActivityStoreTests.swift added +38
@@ -0,0 +1,38 @@
1import Foundation
2import Testing
3@testable import Hutch
4
5struct RecentActivityStoreTests {
6 @Test
7 func removeDropsMatchingEntryKeepingOthers() {
8 let defaults = UserDefaults(suiteName: #function)!
9 defaults.removePersistentDomain(forName: #function)
10
11 RecentActivityStore.recordBuild(jobId: 1, title: "Build 1", defaults: defaults)
12 RecentActivityStore.recordBuild(jobId: 2, title: "Build 2", defaults: defaults)
13
14 RecentActivityStore.remove(id: "build:1", defaults: defaults)
15
16 let remaining = RecentActivityStore.load(defaults: defaults)
17 #expect(remaining.map(\.id) == ["build:2"])
18 }
19
20 @Test
21 func clearRemovesAllEntries() {
22 let defaults = UserDefaults(suiteName: #function)!
23 defaults.removePersistentDomain(forName: #function)
24
25 RecentActivityStore.recordBuild(jobId: 1, title: "Build 1", defaults: defaults)
26 RecentActivityStore.recordTicket(
27 ownerUsername: "~alice",
28 trackerName: "hutch",
29 ticketId: 42,
30 title: "A ticket",
31 defaults: defaults
32 )
33
34 RecentActivityStore.clear(defaults: defaults)
35
36 #expect(RecentActivityStore.load(defaults: defaults).isEmpty)
37 }
38}