summaryrefslogtreecommitdiff
path: root/Hutch/Views
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-07-15 22:51:41 -0500
committerChristian Cleberg <[email protected]>2026-07-15 22:51:41 -0500
commit91023a6572354af7770309715d1d77e978298b0d (patch)
treec15d307e2c93cd34599115f4e0f957e76b540324 /Hutch/Views
parent3611d941247a7163fce9c61d8f75a81ddedd90b7 (diff)
downloadhutch-91023a6572354af7770309715d1d77e978298b0d.tar.gz
hutch-91023a6572354af7770309715d1d77e978298b0d.tar.bz2
hutch-91023a6572354af7770309715d1d77e978298b0d.zip
fix: let pull to refresh actually refresh Home
New mail showed as unread inside a mailing list but not on Home. The list view reads threads with client.execute, so it always sees the server. Home reads them with executeCached under cacheFirstThenRefresh, which returns the cached value and only schedules a background fetch. loadDashboard had no way to bypass that, so pulling to refresh could not surface mail that arrived after the last fetch — it returned the same cache it already had and Home stayed a cycle behind. Thread forceRefresh through loadDashboard to the cached reads, and use it from the pull to refresh on Home and Work. Matches loadSystemStatus(forceRefresh:), which MoreView already refreshes this way. The staleness predates the unread baseline. It was invisible while every thread counted as unread, because Home always had a large number to show; now that Home correctly ignores mail older than sign-in, a stale cache reads as a truthful 0.
Diffstat (limited to 'Hutch/Views')
-rw-r--r--Hutch/Views/Home/HomeView.swift2
-rw-r--r--Hutch/Views/Home/HomeViewModel.swift61
-rw-r--r--Hutch/Views/Work/WorkView.swift2
3 files changed, 35 insertions, 30 deletions
diff --git a/Hutch/Views/Home/HomeView.swift b/Hutch/Views/Home/HomeView.swift
index 409d095..3e95f0b 100644
--- a/Hutch/Views/Home/HomeView.swift
+++ b/Hutch/Views/Home/HomeView.swift
@@ -74,7 +74,7 @@ struct HomeView: View {
.listStyle(.insetGrouped)
.listSectionSpacing(.compact)
.refreshable {
- await viewModel.loadDashboard()
+ await viewModel.loadDashboard(forceRefresh: true)
}
.connectivityOverlay(hasContent: hasHomeContent(viewModel)) {
await viewModel.loadDashboard()
diff --git a/Hutch/Views/Home/HomeViewModel.swift b/Hutch/Views/Home/HomeViewModel.swift
index 4436f0c..98dd968 100644
--- a/Hutch/Views/Home/HomeViewModel.swift
+++ b/Hutch/Views/Home/HomeViewModel.swift
@@ -343,7 +343,12 @@ final class HomeViewModel {
self.accountID = accountID
}
- func loadDashboard() async {
+ /// Loads the dashboard.
+ ///
+ /// `forceRefresh` bypasses the cache. Without it, a pull to refresh returns
+ /// whatever is already cached and only schedules a background fetch, so new
+ /// mail cannot show up on the first pull.
+ func loadDashboard(forceRefresh: Bool = false) async {
isLoadingProjects = true
isLoadingAssignedTickets = true
isLoadingRecentBuilds = true
@@ -354,11 +359,11 @@ final class HomeViewModel {
isShowingStaleSystemStatus = false
systemStatusErrorMessage = nil
- async let projectsTask = loadProjects()
- async let jobsTask = loadRecentJobs()
- async let assignedTicketsTask = loadAssignedTickets()
- async let inboxUnreadTask = loadInboxUnreadSnapshot()
- async let systemStatusTask = loadSystemStatusSnapshot()
+ async let projectsTask = loadProjects(forceRefresh: forceRefresh)
+ async let jobsTask = loadRecentJobs(forceRefresh: forceRefresh)
+ async let assignedTicketsTask = loadAssignedTickets(forceRefresh: forceRefresh)
+ async let inboxUnreadTask = loadInboxUnreadSnapshot(forceRefresh: forceRefresh)
+ async let systemStatusTask = loadSystemStatusSnapshot(forceRefresh: forceRefresh)
let projectsResult = await projectsTask
switch projectsResult {
@@ -676,7 +681,7 @@ final class HomeViewModel {
persistNeedsAttentionSnapshot()
}
- private func loadProjects() async -> Result<[Project], Error> {
+ private func loadProjects(forceRefresh: Bool) async -> Result<[Project], Error> {
do {
return .success(try await projectService.fetchProjects())
} catch {
@@ -684,7 +689,7 @@ final class HomeViewModel {
}
}
- private func loadRecentJobs() async -> Result<[HomeJobPayload], Error> {
+ private func loadRecentJobs(forceRefresh: Bool) async -> Result<[HomeJobPayload], Error> {
do {
let cached = try await client.executeCached(
service: .builds,
@@ -693,7 +698,7 @@ final class HomeViewModel {
cacheKey: APICacheKeys.homeJobs(actor: currentUser.canonicalName),
resourceType: .buildList,
ttl: APICacheTTLs.homeDashboard,
- policy: .cacheFirstThenRefresh
+ policy: forceRefresh ? .refreshIgnoringCache : .cacheFirstThenRefresh
)
return .success(cached.value.jobs.results)
} catch {
@@ -701,15 +706,15 @@ final class HomeViewModel {
}
}
- private func loadInboxUnreadSnapshot() async -> HomeInboxUnreadSnapshot? {
+ private func loadInboxUnreadSnapshot(forceRefresh: Bool) async -> HomeInboxUnreadSnapshot? {
do {
- return try await fetchUnreadInboxSnapshot()
+ return try await fetchUnreadInboxSnapshot(forceRefresh: forceRefresh)
} catch {
return nil
}
}
- private func loadSystemStatusSnapshot() async -> Result<CachedSystemStatusValue<SystemStatusSnapshot>, Error> {
+ private func loadSystemStatusSnapshot(forceRefresh: Bool) async -> Result<CachedSystemStatusValue<SystemStatusSnapshot>, Error> {
do {
return .success(try await systemStatusRepository.snapshotResult())
} catch {
@@ -717,8 +722,8 @@ final class HomeViewModel {
}
}
- private func fetchUnreadInboxSnapshot() async throws -> HomeInboxUnreadSnapshot {
- let mailingLists = try await fetchInboxMailingLists()
+ private func fetchUnreadInboxSnapshot(forceRefresh: Bool) async throws -> HomeInboxUnreadSnapshot {
+ let mailingLists = try await fetchInboxMailingLists(forceRefresh: forceRefresh)
guard !mailingLists.isEmpty else { return HomeInboxUnreadSnapshot(unreadCount: 0, threads: []) }
var startIndex = mailingLists.startIndex
@@ -737,7 +742,7 @@ final class HomeViewModel {
for mailingList in batch {
group.addTask {
do {
- return .success(try await self.fetchUnreadThreadSnapshot(for: mailingList))
+ return .success(try await self.fetchUnreadThreadSnapshot(for: mailingList, forceRefresh: forceRefresh))
} catch {
return .failure(error)
}
@@ -776,7 +781,7 @@ final class HomeViewModel {
)
}
- private func fetchInboxMailingLists() async throws -> [InboxMailingListReference] {
+ private func fetchInboxMailingLists(forceRefresh: Bool) async throws -> [InboxMailingListReference] {
var subscriptions: [HomeInboxSubscription] = []
var cursor: String?
@@ -794,7 +799,7 @@ final class HomeViewModel {
cacheKey: APICacheKeys.inboxSubscriptions(cursor: cursor),
resourceType: .ticketList,
ttl: APICacheTTLs.inboxSummary,
- policy: .cacheFirstThenRefresh
+ policy: forceRefresh ? .refreshIgnoringCache : .cacheFirstThenRefresh
)
let response = cached.value
@@ -809,7 +814,7 @@ final class HomeViewModel {
return subscriptions.compactMap(\.list).filter { seen.insert($0.rid).inserted }
}
- private func fetchUnreadThreadSnapshot(for mailingList: InboxMailingListReference) async throws -> HomeInboxUnreadSnapshot {
+ private func fetchUnreadThreadSnapshot(for mailingList: InboxMailingListReference, forceRefresh: Bool) async throws -> HomeInboxUnreadSnapshot {
var unreadCount = 0
var cursor: String?
var unreadThreads: [InboxThreadSummary] = []
@@ -828,7 +833,7 @@ final class HomeViewModel {
cacheKey: APICacheKeys.inboxThreads(listRid: mailingList.rid, cursor: cursor),
resourceType: .ticketList,
ttl: APICacheTTLs.inboxSummary,
- policy: .cacheFirstThenRefresh
+ policy: forceRefresh ? .refreshIgnoringCache : .cacheFirstThenRefresh
)
let response = cached.value
@@ -871,10 +876,10 @@ final class HomeViewModel {
)
}
- private func loadAssignedTickets() async -> Result<[HomeAssignedTicket], Error> {
+ private func loadAssignedTickets(forceRefresh: Bool) async -> Result<[HomeAssignedTicket], Error> {
do {
- let trackers = try await fetchAllTrackers()
- let tickets = try await fetchAssignedTickets(for: trackers)
+ let trackers = try await fetchAllTrackers(forceRefresh: forceRefresh)
+ let tickets = try await fetchAssignedTickets(for: trackers, forceRefresh: forceRefresh)
.sorted(by: Self.sortAssignedTicketsForTriage)
return .success(tickets)
} catch {
@@ -882,7 +887,7 @@ final class HomeViewModel {
}
}
- private func fetchAllTrackers() async throws -> [TrackerSummary] {
+ private func fetchAllTrackers(forceRefresh: Bool) async throws -> [TrackerSummary] {
var allTrackers: [TrackerSummary] = []
var cursor: String?
@@ -900,7 +905,7 @@ final class HomeViewModel {
cacheKey: APICacheKeys.trackers(cursor: cursor),
resourceType: .ticketList,
ttl: APICacheTTLs.ticketList,
- policy: .cacheFirstThenRefresh
+ policy: forceRefresh ? .refreshIgnoringCache : .cacheFirstThenRefresh
)
let response = cached.value
@@ -914,7 +919,7 @@ final class HomeViewModel {
return allTrackers
}
- private func fetchAssignedTickets(for trackers: [TrackerSummary]) async throws -> [HomeAssignedTicket] {
+ private func fetchAssignedTickets(for trackers: [TrackerSummary], forceRefresh: Bool) async throws -> [HomeAssignedTicket] {
guard !trackers.isEmpty else { return [] }
var assignedTickets: [HomeAssignedTicket] = []
@@ -927,7 +932,7 @@ final class HomeViewModel {
let batchTickets = try await withThrowingTaskGroup(of: [HomeAssignedTicket].self) { group in
for tracker in batch {
group.addTask {
- try await self.fetchAssignedTickets(for: tracker)
+ try await self.fetchAssignedTickets(for: tracker, forceRefresh: forceRefresh)
}
}
@@ -945,7 +950,7 @@ final class HomeViewModel {
return assignedTickets
}
- private func fetchAssignedTickets(for tracker: TrackerSummary) async throws -> [HomeAssignedTicket] {
+ private func fetchAssignedTickets(for tracker: TrackerSummary, forceRefresh: Bool) async throws -> [HomeAssignedTicket] {
let cached = try await client.executeCached(
service: .todo,
query: Self.trackerTicketsQuery,
@@ -964,7 +969,7 @@ final class HomeViewModel {
),
resourceType: .ticketList,
ttl: APICacheTTLs.ticketList,
- policy: .cacheFirstThenRefresh
+ policy: forceRefresh ? .refreshIgnoringCache : .cacheFirstThenRefresh
)
let response = cached.value
diff --git a/Hutch/Views/Work/WorkView.swift b/Hutch/Views/Work/WorkView.swift
index 6ba1d0c..23acdfb 100644
--- a/Hutch/Views/Work/WorkView.swift
+++ b/Hutch/Views/Work/WorkView.swift
@@ -72,7 +72,7 @@ struct WorkView: View {
.themedList()
.listStyle(.insetGrouped)
.refreshable {
- await viewModel.loadDashboard()
+ await viewModel.loadDashboard(forceRefresh: true)
}
.connectivityOverlay(hasContent: hasWorkContent(viewModel)) {
await viewModel.loadDashboard()