summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-08-07 01:44:17 -0500
committerChristian Cleberg <[email protected]>2026-08-07 01:44:17 -0500
commita0ed11a3c4ddb2f532fa70884dc2829a23a5eec8 (patch)
tree6124f8e184a5ba3b648f173af597928caa5fcd84
parent5e4e259d8bfd9b062aa39af793ec3a59b70ad7f0 (diff)
downloadhutch-a0ed11a3c4ddb2f532fa70884dc2829a23a5eec8.tar.gz
hutch-a0ed11a3c4ddb2f532fa70884dc2829a23a5eec8.tar.bz2
hutch-a0ed11a3c4ddb2f532fa70884dc2829a23a5eec8.zip
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.
-rw-r--r--Hutch/Views/Home/HomeView.swift33
-rw-r--r--Hutch/Views/Home/RecentActivityStore.swift10
-rw-r--r--HutchTests/RecentActivityStoreTests.swift38
3 files changed, 80 insertions, 1 deletions
diff --git a/Hutch/Views/Home/HomeView.swift b/Hutch/Views/Home/HomeView.swift
index f0ec9ff..0e3e214 100644
--- a/Hutch/Views/Home/HomeView.swift
+++ b/Hutch/Views/Home/HomeView.swift
@@ -5,6 +5,8 @@ struct HomeView: View {
@Environment(\.scenePhase) private var scenePhase
@AppStorage(AppStorageKeys.homeFailedBuildLookbackDays, store: .standard)
private var failedBuildLookbackDays = HomeViewModel.defaultFailedBuildLookbackDays
+ @AppStorage(AppStorageKeys.swipeActionsEnabled, store: .standard)
+ private var swipeActionsEnabled = true
@State private var viewModel: HomeViewModel?
@State private var recentItems: [RecentActivityEntry] = []
@State private var isOpeningRecentItem = false
@@ -118,7 +120,7 @@ struct HomeView: View {
@ViewBuilder
private var recentSection: some View {
if !recentItems.isEmpty {
- Section("Recent") {
+ Section {
ForEach(recentItems.prefix(3)) { item in
Button {
openRecentItem(item)
@@ -128,12 +130,41 @@ struct HomeView: View {
.buttonStyle(.plain)
.disabled(isOpeningRecentItem)
.listRowSeparator(.hidden)
+ .swipeActions(edge: .trailing, allowsFullSwipe: true) {
+ if swipeActionsEnabled {
+ Button(role: .destructive) {
+ removeRecentItem(item)
+ } label: {
+ Label("Remove", systemImage: "trash")
+ }
+ }
+ }
}
.themedRow()
+ } header: {
+ HStack {
+ Text("Recent")
+ Spacer()
+ Button("Clear") {
+ clearRecentActivity()
+ }
+ .font(.caption)
+ .textCase(nil)
+ }
}
}
}
+ private func removeRecentItem(_ item: RecentActivityEntry) {
+ RecentActivityStore.remove(id: item.id, defaults: appState.accountDefaults)
+ loadRecentActivity()
+ }
+
+ private func clearRecentActivity() {
+ RecentActivityStore.clear(defaults: appState.accountDefaults)
+ loadRecentActivity()
+ }
+
private func buildsSection(_ viewModel: HomeViewModel) -> some View {
Section("Builds") {
Button {
diff --git a/Hutch/Views/Home/RecentActivityStore.swift b/Hutch/Views/Home/RecentActivityStore.swift
index 464a2c1..6bc5720 100644
--- a/Hutch/Views/Home/RecentActivityStore.swift
+++ b/Hutch/Views/Home/RecentActivityStore.swift
@@ -129,6 +129,16 @@ enum RecentActivityStore {
)
}
+ static func remove(id: String, defaults: UserDefaults) {
+ var entries = load(defaults: defaults)
+ entries.removeAll { $0.id == id }
+ save(entries, defaults: defaults)
+ }
+
+ static func clear(defaults: UserDefaults) {
+ defaults.removeObject(forKey: AppStorageKeys.recentActivity)
+ }
+
private static func record(_ entry: RecentActivityEntry, defaults: UserDefaults) {
var entries = load(defaults: defaults)
entries.removeAll { $0.id == entry.id }
diff --git a/HutchTests/RecentActivityStoreTests.swift b/HutchTests/RecentActivityStoreTests.swift
new file mode 100644
index 0000000..e6fcc04
--- /dev/null
+++ b/HutchTests/RecentActivityStoreTests.swift
@@ -0,0 +1,38 @@
+import Foundation
+import Testing
+@testable import Hutch
+
+struct RecentActivityStoreTests {
+ @Test
+ func removeDropsMatchingEntryKeepingOthers() {
+ let defaults = UserDefaults(suiteName: #function)!
+ defaults.removePersistentDomain(forName: #function)
+
+ RecentActivityStore.recordBuild(jobId: 1, title: "Build 1", defaults: defaults)
+ RecentActivityStore.recordBuild(jobId: 2, title: "Build 2", defaults: defaults)
+
+ RecentActivityStore.remove(id: "build:1", defaults: defaults)
+
+ let remaining = RecentActivityStore.load(defaults: defaults)
+ #expect(remaining.map(\.id) == ["build:2"])
+ }
+
+ @Test
+ func clearRemovesAllEntries() {
+ let defaults = UserDefaults(suiteName: #function)!
+ defaults.removePersistentDomain(forName: #function)
+
+ RecentActivityStore.recordBuild(jobId: 1, title: "Build 1", defaults: defaults)
+ RecentActivityStore.recordTicket(
+ ownerUsername: "~alice",
+ trackerName: "hutch",
+ ticketId: 42,
+ title: "A ticket",
+ defaults: defaults
+ )
+
+ RecentActivityStore.clear(defaults: defaults)
+
+ #expect(RecentActivityStore.load(defaults: defaults).isEmpty)
+ }
+}