| @@ -343,7 +343,12 @@ final class HomeViewModel { |
| 343 | self.accountID = accountID |
343 | self.accountID = accountID |
| 344 | } |
344 | } |
| 345 | |
345 | |
| 346 | func loadDashboard() async { |
346 | /// Loads the dashboard. |
| |
347 | /// |
| |
348 | /// `forceRefresh` bypasses the cache. Without it, a pull to refresh returns |
| |
349 | /// whatever is already cached and only schedules a background fetch, so new |
| |
350 | /// mail cannot show up on the first pull. |
| |
351 | func loadDashboard(forceRefresh: Bool = false) async { |
| 347 | isLoadingProjects = true |
352 | isLoadingProjects = true |
| 348 | isLoadingAssignedTickets = true |
353 | isLoadingAssignedTickets = true |
| 349 | isLoadingRecentBuilds = true |
354 | isLoadingRecentBuilds = true |
| @@ -354,11 +359,11 @@ final class HomeViewModel { |
| 354 | isShowingStaleSystemStatus = false |
359 | isShowingStaleSystemStatus = false |
| 355 | systemStatusErrorMessage = nil |
360 | systemStatusErrorMessage = nil |
| 356 | |
361 | |
| 357 | async let projectsTask = loadProjects() |
362 | async let projectsTask = loadProjects(forceRefresh: forceRefresh) |
| 358 | async let jobsTask = loadRecentJobs() |
363 | async let jobsTask = loadRecentJobs(forceRefresh: forceRefresh) |
| 359 | async let assignedTicketsTask = loadAssignedTickets() |
364 | async let assignedTicketsTask = loadAssignedTickets(forceRefresh: forceRefresh) |
| 360 | async let inboxUnreadTask = loadInboxUnreadSnapshot() |
365 | async let inboxUnreadTask = loadInboxUnreadSnapshot(forceRefresh: forceRefresh) |
| 361 | async let systemStatusTask = loadSystemStatusSnapshot() |
366 | async let systemStatusTask = loadSystemStatusSnapshot(forceRefresh: forceRefresh) |
| 362 | |
367 | |
| 363 | let projectsResult = await projectsTask |
368 | let projectsResult = await projectsTask |
| 364 | switch projectsResult { |
369 | switch projectsResult { |
| @@ -676,7 +681,7 @@ final class HomeViewModel { |
| 676 | persistNeedsAttentionSnapshot() |
681 | persistNeedsAttentionSnapshot() |
| 677 | } |
682 | } |
| 678 | |
683 | |
| 679 | private func loadProjects() async -> Result<[Project], Error> { |
684 | private func loadProjects(forceRefresh: Bool) async -> Result<[Project], Error> { |
| 680 | do { |
685 | do { |
| 681 | return .success(try await projectService.fetchProjects()) |
686 | return .success(try await projectService.fetchProjects()) |
| 682 | } catch { |
687 | } catch { |
| @@ -684,7 +689,7 @@ final class HomeViewModel { |
| 684 | } |
689 | } |
| 685 | } |
690 | } |
| 686 | |
691 | |
| 687 | private func loadRecentJobs() async -> Result<[HomeJobPayload], Error> { |
692 | private func loadRecentJobs(forceRefresh: Bool) async -> Result<[HomeJobPayload], Error> { |
| 688 | do { |
693 | do { |
| 689 | let cached = try await client.executeCached( |
694 | let cached = try await client.executeCached( |
| 690 | service: .builds, |
695 | service: .builds, |
| @@ -693,7 +698,7 @@ final class HomeViewModel { |
| 693 | cacheKey: APICacheKeys.homeJobs(actor: currentUser.canonicalName), |
698 | cacheKey: APICacheKeys.homeJobs(actor: currentUser.canonicalName), |
| 694 | resourceType: .buildList, |
699 | resourceType: .buildList, |
| 695 | ttl: APICacheTTLs.homeDashboard, |
700 | ttl: APICacheTTLs.homeDashboard, |
| 696 | policy: .cacheFirstThenRefresh |
701 | policy: forceRefresh ? .refreshIgnoringCache : .cacheFirstThenRefresh |
| 697 | ) |
702 | ) |
| 698 | return .success(cached.value.jobs.results) |
703 | return .success(cached.value.jobs.results) |
| 699 | } catch { |
704 | } catch { |
| @@ -701,15 +706,15 @@ final class HomeViewModel { |
| 701 | } |
706 | } |
| 702 | } |
707 | } |
| 703 | |
708 | |
| 704 | private func loadInboxUnreadSnapshot() async -> HomeInboxUnreadSnapshot? { |
709 | private func loadInboxUnreadSnapshot(forceRefresh: Bool) async -> HomeInboxUnreadSnapshot? { |
| 705 | do { |
710 | do { |
| 706 | return try await fetchUnreadInboxSnapshot() |
711 | return try await fetchUnreadInboxSnapshot(forceRefresh: forceRefresh) |
| 707 | } catch { |
712 | } catch { |
| 708 | return nil |
713 | return nil |
| 709 | } |
714 | } |
| 710 | } |
715 | } |
| 711 | |
716 | |
| 712 | private func loadSystemStatusSnapshot() async -> Result<CachedSystemStatusValue<SystemStatusSnapshot>, Error> { |
717 | private func loadSystemStatusSnapshot(forceRefresh: Bool) async -> Result<CachedSystemStatusValue<SystemStatusSnapshot>, Error> { |
| 713 | do { |
718 | do { |
| 714 | return .success(try await systemStatusRepository.snapshotResult()) |
719 | return .success(try await systemStatusRepository.snapshotResult()) |
| 715 | } catch { |
720 | } catch { |
| @@ -717,8 +722,8 @@ final class HomeViewModel { |
| 717 | } |
722 | } |
| 718 | } |
723 | } |
| 719 | |
724 | |
| 720 | private func fetchUnreadInboxSnapshot() async throws -> HomeInboxUnreadSnapshot { |
725 | private func fetchUnreadInboxSnapshot(forceRefresh: Bool) async throws -> HomeInboxUnreadSnapshot { |
| 721 | let mailingLists = try await fetchInboxMailingLists() |
726 | let mailingLists = try await fetchInboxMailingLists(forceRefresh: forceRefresh) |
| 722 | guard !mailingLists.isEmpty else { return HomeInboxUnreadSnapshot(unreadCount: 0, threads: []) } |
727 | guard !mailingLists.isEmpty else { return HomeInboxUnreadSnapshot(unreadCount: 0, threads: []) } |
| 723 | |
728 | |
| 724 | var startIndex = mailingLists.startIndex |
729 | var startIndex = mailingLists.startIndex |
| @@ -737,7 +742,7 @@ final class HomeViewModel { |
| 737 | for mailingList in batch { |
742 | for mailingList in batch { |
| 738 | group.addTask { |
743 | group.addTask { |
| 739 | do { |
744 | do { |
| 740 | return .success(try await self.fetchUnreadThreadSnapshot(for: mailingList)) |
745 | return .success(try await self.fetchUnreadThreadSnapshot(for: mailingList, forceRefresh: forceRefresh)) |
| 741 | } catch { |
746 | } catch { |
| 742 | return .failure(error) |
747 | return .failure(error) |
| 743 | } |
748 | } |
| @@ -776,7 +781,7 @@ final class HomeViewModel { |
| 776 | ) |
781 | ) |
| 777 | } |
782 | } |
| 778 | |
783 | |
| 779 | private func fetchInboxMailingLists() async throws -> [InboxMailingListReference] { |
784 | private func fetchInboxMailingLists(forceRefresh: Bool) async throws -> [InboxMailingListReference] { |
| 780 | var subscriptions: [HomeInboxSubscription] = [] |
785 | var subscriptions: [HomeInboxSubscription] = [] |
| 781 | var cursor: String? |
786 | var cursor: String? |
| 782 | |
787 | |
| @@ -794,7 +799,7 @@ final class HomeViewModel { |
| 794 | cacheKey: APICacheKeys.inboxSubscriptions(cursor: cursor), |
799 | cacheKey: APICacheKeys.inboxSubscriptions(cursor: cursor), |
| 795 | resourceType: .ticketList, |
800 | resourceType: .ticketList, |
| 796 | ttl: APICacheTTLs.inboxSummary, |
801 | ttl: APICacheTTLs.inboxSummary, |
| 797 | policy: .cacheFirstThenRefresh |
802 | policy: forceRefresh ? .refreshIgnoringCache : .cacheFirstThenRefresh |
| 798 | ) |
803 | ) |
| 799 | let response = cached.value |
804 | let response = cached.value |
| 800 | |
805 | |
| @@ -809,7 +814,7 @@ final class HomeViewModel { |
| 809 | return subscriptions.compactMap(\.list).filter { seen.insert($0.rid).inserted } |
814 | return subscriptions.compactMap(\.list).filter { seen.insert($0.rid).inserted } |
| 810 | } |
815 | } |
| 811 | |
816 | |
| 812 | private func fetchUnreadThreadSnapshot(for mailingList: InboxMailingListReference) async throws -> HomeInboxUnreadSnapshot { |
817 | private func fetchUnreadThreadSnapshot(for mailingList: InboxMailingListReference, forceRefresh: Bool) async throws -> HomeInboxUnreadSnapshot { |
| 813 | var unreadCount = 0 |
818 | var unreadCount = 0 |
| 814 | var cursor: String? |
819 | var cursor: String? |
| 815 | var unreadThreads: [InboxThreadSummary] = [] |
820 | var unreadThreads: [InboxThreadSummary] = [] |
| @@ -828,7 +833,7 @@ final class HomeViewModel { |
| 828 | cacheKey: APICacheKeys.inboxThreads(listRid: mailingList.rid, cursor: cursor), |
833 | cacheKey: APICacheKeys.inboxThreads(listRid: mailingList.rid, cursor: cursor), |
| 829 | resourceType: .ticketList, |
834 | resourceType: .ticketList, |
| 830 | ttl: APICacheTTLs.inboxSummary, |
835 | ttl: APICacheTTLs.inboxSummary, |
| 831 | policy: .cacheFirstThenRefresh |
836 | policy: forceRefresh ? .refreshIgnoringCache : .cacheFirstThenRefresh |
| 832 | ) |
837 | ) |
| 833 | let response = cached.value |
838 | let response = cached.value |
| 834 | |
839 | |
| @@ -871,10 +876,10 @@ final class HomeViewModel { |
| 871 | ) |
876 | ) |
| 872 | } |
877 | } |
| 873 | |
878 | |
| 874 | private func loadAssignedTickets() async -> Result<[HomeAssignedTicket], Error> { |
879 | private func loadAssignedTickets(forceRefresh: Bool) async -> Result<[HomeAssignedTicket], Error> { |
| 875 | do { |
880 | do { |
| 876 | let trackers = try await fetchAllTrackers() |
881 | let trackers = try await fetchAllTrackers(forceRefresh: forceRefresh) |
| 877 | let tickets = try await fetchAssignedTickets(for: trackers) |
882 | let tickets = try await fetchAssignedTickets(for: trackers, forceRefresh: forceRefresh) |
| 878 | .sorted(by: Self.sortAssignedTicketsForTriage) |
883 | .sorted(by: Self.sortAssignedTicketsForTriage) |
| 879 | return .success(tickets) |
884 | return .success(tickets) |
| 880 | } catch { |
885 | } catch { |
| @@ -882,7 +887,7 @@ final class HomeViewModel { |
| 882 | } |
887 | } |
| 883 | } |
888 | } |
| 884 | |
889 | |
| 885 | private func fetchAllTrackers() async throws -> [TrackerSummary] { |
890 | private func fetchAllTrackers(forceRefresh: Bool) async throws -> [TrackerSummary] { |
| 886 | var allTrackers: [TrackerSummary] = [] |
891 | var allTrackers: [TrackerSummary] = [] |
| 887 | var cursor: String? |
892 | var cursor: String? |
| 888 | |
893 | |
| @@ -900,7 +905,7 @@ final class HomeViewModel { |
| 900 | cacheKey: APICacheKeys.trackers(cursor: cursor), |
905 | cacheKey: APICacheKeys.trackers(cursor: cursor), |
| 901 | resourceType: .ticketList, |
906 | resourceType: .ticketList, |
| 902 | ttl: APICacheTTLs.ticketList, |
907 | ttl: APICacheTTLs.ticketList, |
| 903 | policy: .cacheFirstThenRefresh |
908 | policy: forceRefresh ? .refreshIgnoringCache : .cacheFirstThenRefresh |
| 904 | ) |
909 | ) |
| 905 | let response = cached.value |
910 | let response = cached.value |
| 906 | |
911 | |
| @@ -914,7 +919,7 @@ final class HomeViewModel { |
| 914 | return allTrackers |
919 | return allTrackers |
| 915 | } |
920 | } |
| 916 | |
921 | |
| 917 | private func fetchAssignedTickets(for trackers: [TrackerSummary]) async throws -> [HomeAssignedTicket] { |
922 | private func fetchAssignedTickets(for trackers: [TrackerSummary], forceRefresh: Bool) async throws -> [HomeAssignedTicket] { |
| 918 | guard !trackers.isEmpty else { return [] } |
923 | guard !trackers.isEmpty else { return [] } |
| 919 | |
924 | |
| 920 | var assignedTickets: [HomeAssignedTicket] = [] |
925 | var assignedTickets: [HomeAssignedTicket] = [] |
| @@ -927,7 +932,7 @@ final class HomeViewModel { |
| 927 | let batchTickets = try await withThrowingTaskGroup(of: [HomeAssignedTicket].self) { group in |
932 | let batchTickets = try await withThrowingTaskGroup(of: [HomeAssignedTicket].self) { group in |
| 928 | for tracker in batch { |
933 | for tracker in batch { |
| 929 | group.addTask { |
934 | group.addTask { |
| 930 | try await self.fetchAssignedTickets(for: tracker) |
935 | try await self.fetchAssignedTickets(for: tracker, forceRefresh: forceRefresh) |
| 931 | } |
936 | } |
| 932 | } |
937 | } |
| 933 | |
938 | |
| @@ -945,7 +950,7 @@ final class HomeViewModel { |
| 945 | return assignedTickets |
950 | return assignedTickets |
| 946 | } |
951 | } |
| 947 | |
952 | |
| 948 | private func fetchAssignedTickets(for tracker: TrackerSummary) async throws -> [HomeAssignedTicket] { |
953 | private func fetchAssignedTickets(for tracker: TrackerSummary, forceRefresh: Bool) async throws -> [HomeAssignedTicket] { |
| 949 | let cached = try await client.executeCached( |
954 | let cached = try await client.executeCached( |
| 950 | service: .todo, |
955 | service: .todo, |
| 951 | query: Self.trackerTicketsQuery, |
956 | query: Self.trackerTicketsQuery, |
| @@ -964,7 +969,7 @@ final class HomeViewModel { |
| 964 | ), |
969 | ), |
| 965 | resourceType: .ticketList, |
970 | resourceType: .ticketList, |
| 966 | ttl: APICacheTTLs.ticketList, |
971 | ttl: APICacheTTLs.ticketList, |
| 967 | policy: .cacheFirstThenRefresh |
972 | policy: forceRefresh ? .refreshIgnoringCache : .cacheFirstThenRefresh |
| 968 | ) |
973 | ) |
| 969 | let response = cached.value |
974 | let response = cached.value |
| 970 | |
975 | |