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 | 5 | @Environment(\.scenePhase) private var scenePhase |
| 6 | 6 | @AppStorage(AppStorageKeys.homeFailedBuildLookbackDays, store: .standard) |
| 7 | 7 | private var failedBuildLookbackDays = HomeViewModel.defaultFailedBuildLookbackDays |
| 8 | @AppStorage(AppStorageKeys.swipeActionsEnabled, store: .standard) |
| 9 | private var swipeActionsEnabled = true |
| 8 | 10 | @State private var viewModel: HomeViewModel? |
| 9 | 11 | @State private var recentItems: [RecentActivityEntry] = [] |
| 10 | 12 | @State private var isOpeningRecentItem = false |
| @@ -118,7 +120,7 @@ struct HomeView: View { |
| 118 | 120 | @ViewBuilder |
| 119 | 121 | private var recentSection: some View { |
| 120 | 122 | if !recentItems.isEmpty { |
| 121 | | Section("Recent") { |
| 123 | Section { |
| 122 | 124 | ForEach(recentItems.prefix(3)) { item in |
| 123 | 125 | Button { |
| 124 | 126 | openRecentItem(item) |
| @@ -128,12 +130,41 @@ struct HomeView: View { |
| 128 | 130 | .buttonStyle(.plain) |
| 129 | 131 | .disabled(isOpeningRecentItem) |
| 130 | 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 | 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 | 168 | private func buildsSection(_ viewModel: HomeViewModel) -> some View { |
| 138 | 169 | Section("Builds") { |
| 139 | 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 | 142 | private static func record(_ entry: RecentActivityEntry, defaults: UserDefaults) { |
| 133 | 143 | var entries = load(defaults: defaults) |
| 134 | 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 | } |