diff options
| author | Christian Cleberg <[email protected]> | 2026-07-15 22:51:41 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-07-15 22:51:41 -0500 |
| commit | 91023a6572354af7770309715d1d77e978298b0d (patch) | |
| tree | c15d307e2c93cd34599115f4e0f957e76b540324 /Hutch/Views | |
| parent | 3611d941247a7163fce9c61d8f75a81ddedd90b7 (diff) | |
| download | hutch-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.swift | 2 | ||||
| -rw-r--r-- | Hutch/Views/Home/HomeViewModel.swift | 61 | ||||
| -rw-r--r-- | Hutch/Views/Work/WorkView.swift | 2 |
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() |
