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 {
5 @Environment(\.scenePhase) private var scenePhase 5 @Environment(\.scenePhase) private var scenePhase
6 @AppStorage(AppStorageKeys.homeFailedBuildLookbackDays, store: .standard) 6 @AppStorage(AppStorageKeys.homeFailedBuildLookbackDays, store: .standard)
7 private var failedBuildLookbackDays = HomeViewModel.defaultFailedBuildLookbackDays 7 private var failedBuildLookbackDays = HomeViewModel.defaultFailedBuildLookbackDays
8 @AppStorage(AppStorageKeys.swipeActionsEnabled, store: .standard)
9 private var swipeActionsEnabled = true
8 @State private var viewModel: HomeViewModel? 10 @State private var viewModel: HomeViewModel?
9 @State private var recentItems: [RecentActivityEntry] = [] 11 @State private var recentItems: [RecentActivityEntry] = []
10 @State private var isOpeningRecentItem = false 12 @State private var isOpeningRecentItem = false
@@ -118,7 +120,7 @@ struct HomeView: View {
118 @ViewBuilder 120 @ViewBuilder
119 private var recentSection: some View { 121 private var recentSection: some View {
120 if !recentItems.isEmpty { 122 if !recentItems.isEmpty {
121 Section("Recent") { 123 Section {
122 ForEach(recentItems.prefix(3)) { item in 124 ForEach(recentItems.prefix(3)) { item in
123 Button { 125 Button {
124 openRecentItem(item) 126 openRecentItem(item)
@@ -128,12 +130,41 @@ struct HomeView: View {
128 .buttonStyle(.plain) 130 .buttonStyle(.plain)
129 .disabled(isOpeningRecentItem) 131 .disabled(isOpeningRecentItem)
130 .listRowSeparator(.hidden) 132 .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 }
131 } 142 }
132 .themedRow() 143 .themedRow()
144 } header: {
145 HStack {
146 Text("Recent")
147 Spacer()
148 Button("Clear") {
149 clearRecentActivity()
150 }
151 .font(.caption)
152 .textCase(nil)
153 }
133 } 154 }
134 } 155 }
135 } 156 }
136 157
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
137 private func buildsSection(_ viewModel: HomeViewModel) -> some View { 168 private func buildsSection(_ viewModel: HomeViewModel) -> some View {
138 Section("Builds") { 169 Section("Builds") {
139 Button { 170 Button {
Hutch/Views/Home/RecentActivityStore.swift +10
@@ -129,6 +129,16 @@ enum RecentActivityStore {
129 ) 129 )
130 } 130 }
131 131
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
132 private static func record(_ entry: RecentActivityEntry, defaults: UserDefaults) { 142 private static func record(_ entry: RecentActivityEntry, defaults: UserDefaults) {
133 var entries = load(defaults: defaults) 143 var entries = load(defaults: defaults)
134 entries.removeAll { $0.id == entry.id } 144 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}