summaryrefslogtreecommitdiff
path: root/Hutch/Views/Home/RecentActivityStore.swift
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-04-13 13:09:57 -0500
committerChristian Cleberg <[email protected]>2026-04-13 13:09:57 -0500
commitf5f5757d0e1b78470429ae7cb9f742c95662849b (patch)
tree758f67965a985f4f1f52a456d9b672f8256851de /Hutch/Views/Home/RecentActivityStore.swift
parent0e461a382257979037e8927e5f6b468635b0a7fe (diff)
downloadhutch-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.swift148
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)
+ }
+}