krz/hutch
an ios client for sourcehut
clone: git clone https://gitbay.org/krz/hutch.git
a0ed11a3c4ddb2f532fa70884dc2829a23a5eec8
verified · cmc
author: Christian Cleberg <hello@cleberg.net> · 2026-08-07T06:44:17Z
Hutch/Views/Home/HomeView.swift | 33 +++++++++++++++++++++++++- Hutch/Views/Home/RecentActivityStore.swift | 10 ++++++++ HutchTests/RecentActivityStoreTests.swift | 38 ++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 1 deletion(-) @@ -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 { @@ -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 } new file mode 100644 @@ -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) + } +}