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 @@ |
| |
1 | import Foundation |
| |
2 | import Testing |
| |
3 | @testable import Hutch |
| |
4 | |
| |
5 | struct 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 | } |