diff options
| author | Christian Cleberg <[email protected]> | 2026-04-13 13:09:57 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-04-13 13:09:57 -0500 |
| commit | f5f5757d0e1b78470429ae7cb9f742c95662849b (patch) | |
| tree | 758f67965a985f4f1f52a456d9b672f8256851de /Hutch/Views/Home/RecentActivityStore.swift | |
| parent | 0e461a382257979037e8927e5f6b468635b0a7fe (diff) | |
| download | hutch-3.0.0.tar.gz hutch-3.0.0.tar.bz2 hutch-3.0.0.zip | |
release: v3.0.0 — navigation overhaul, Work view, and multi-pin supportv3.0.0
Reworks the app's core navigation and home screen for v3.0.0:
- Replace Inbox with Work view: unified dashboard showing unread threads
and assigned tickets, with All/Unread/Assigned scope picker
- Overhaul Home screen: redesigned with pinned items grid (trackers,
repos, mailing lists, users), recent activity section, and system
status banner; backed by HomePinStore supporting all resource types
- Simplify navigation bars across list screens: default toolbar shows
only the primary action (+) and an overflow menu (…); pin, share, and
select moved into the overflow menu; selection mode gets its own nav
bar with Cancel / "N Selected" / All
- Consolidate repo detail toolbars: pin and share moved into the
existing actions menu for both Git and Mercurial detail views
- Add recent activity tracking via RecentActivityStore
- Add work and inbox deep link aliases (hutch://work, hutch://inbox)
Implements: https://todo.sr.ht/~ccleberg/hutch/55
Diffstat (limited to 'Hutch/Views/Home/RecentActivityStore.swift')
| -rw-r--r-- | Hutch/Views/Home/RecentActivityStore.swift | 148 |
1 files changed, 148 insertions, 0 deletions
diff --git a/Hutch/Views/Home/RecentActivityStore.swift b/Hutch/Views/Home/RecentActivityStore.swift new file mode 100644 index 0000000..464a2c1 --- /dev/null +++ b/Hutch/Views/Home/RecentActivityStore.swift @@ -0,0 +1,148 @@ +import Foundation + +enum RecentActivityKind: String, Codable, Sendable { + case repository + case ticket + case build +} + +struct RecentActivityEntry: Codable, Hashable, Identifiable, Sendable { + let kind: RecentActivityKind + let title: String + let viewedAt: Date + let repositoryOwner: String? + let repositoryName: String? + let repositoryService: SRHTService? + let ticketOwnerUsername: String? + let ticketTrackerName: String? + let ticketId: Int? + let buildJobId: Int? + + var id: String { + switch kind { + case .repository: + let service = repositoryService?.rawValue ?? SRHTService.git.rawValue + return "repo:\(service):\(repositoryOwner ?? "")/\(repositoryName ?? "")" + case .ticket: + return "ticket:\(ticketOwnerUsername ?? "")/\(ticketTrackerName ?? "")#\(ticketId ?? 0)" + case .build: + return "build:\(buildJobId ?? 0)" + } + } + + var detailText: String { + switch kind { + case .repository: + let serviceName = repositoryService?.displayName ?? "Repository" + return "\(serviceName) • Viewed \(viewedAt.relativeDescription)" + case .ticket: + return "Ticket • Viewed \(viewedAt.relativeDescription)" + case .build: + return "Build • Viewed \(viewedAt.relativeDescription)" + } + } +} + +enum RecentActivityStore { + private static let maximumEntries = 5 + + static func load(defaults: UserDefaults) -> [RecentActivityEntry] { + guard let data = defaults.data(forKey: AppStorageKeys.recentActivity) else { + return [] + } + + do { + return try JSONDecoder().decode([RecentActivityEntry].self, from: data) + } catch { + defaults.removeObject(forKey: AppStorageKeys.recentActivity) + return [] + } + } + + static func recordRepository( + _ repository: RepositorySummary, + defaults: UserDefaults, + now: Date = .now + ) { + record( + RecentActivityEntry( + kind: .repository, + title: "\(repository.owner.canonicalName)/\(repository.name)", + viewedAt: now, + repositoryOwner: repository.owner.canonicalName.srhtUsername, + repositoryName: repository.name, + repositoryService: repository.service, + ticketOwnerUsername: nil, + ticketTrackerName: nil, + ticketId: nil, + buildJobId: nil + ), + defaults: defaults + ) + } + + static func recordTicket( + ownerUsername: String, + trackerName: String, + ticketId: Int, + title: String, + defaults: UserDefaults, + now: Date = .now + ) { + record( + RecentActivityEntry( + kind: .ticket, + title: "#\(ticketId) \(title)", + viewedAt: now, + repositoryOwner: nil, + repositoryName: nil, + repositoryService: nil, + ticketOwnerUsername: ownerUsername, + ticketTrackerName: trackerName, + ticketId: ticketId, + buildJobId: nil + ), + defaults: defaults + ) + } + + static func recordBuild( + jobId: Int, + title: String, + defaults: UserDefaults, + now: Date = .now + ) { + record( + RecentActivityEntry( + kind: .build, + title: title, + viewedAt: now, + repositoryOwner: nil, + repositoryName: nil, + repositoryService: nil, + ticketOwnerUsername: nil, + ticketTrackerName: nil, + ticketId: nil, + buildJobId: jobId + ), + defaults: defaults + ) + } + + private static func record(_ entry: RecentActivityEntry, defaults: UserDefaults) { + var entries = load(defaults: defaults) + entries.removeAll { $0.id == entry.id } + entries.insert(entry, at: 0) + + if entries.count > maximumEntries { + entries = Array(entries.prefix(maximumEntries)) + } + + save(entries, defaults: defaults) + } + + private static func save(_ entries: [RecentActivityEntry], defaults: UserDefaults) { + guard let data = try? JSONEncoder().encode(entries) else { return } + defaults.set(data, forKey: AppStorageKeys.recentActivity) + } +} |
