diff options
| -rw-r--r-- | Hutch.xcodeproj/project.pbxproj | 16 | ||||
| -rw-r--r-- | Hutch/Views/Builds/BuildListView.swift | 4 | ||||
| -rw-r--r-- | Hutch/Views/Home/HomeView.swift | 2 | ||||
| -rw-r--r-- | Hutch/Views/Home/HomeViewModel.swift | 8 | ||||
| -rw-r--r-- | Hutch/Views/Repositories/RepositoryListView.swift | 5 | ||||
| -rw-r--r-- | Hutch/Views/Repositories/RepositoryListViewModel.swift | 15 | ||||
| -rw-r--r-- | Hutch/Views/Work/WorkView.swift | 2 |
7 files changed, 34 insertions, 18 deletions
diff --git a/Hutch.xcodeproj/project.pbxproj b/Hutch.xcodeproj/project.pbxproj index f1e1869..f2a08a8 100644 --- a/Hutch.xcodeproj/project.pbxproj +++ b/Hutch.xcodeproj/project.pbxproj @@ -515,7 +515,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = Hutch/Hutch.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 60; + CURRENT_PROJECT_VERSION = 61; DEVELOPMENT_TEAM = ZCNAX3VL9D; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -532,7 +532,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.21.1; + MARKETING_VERSION = 3.0.0; PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = YES; @@ -552,7 +552,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = Hutch/Hutch.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 60; + CURRENT_PROJECT_VERSION = 61; DEVELOPMENT_TEAM = ZCNAX3VL9D; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -569,7 +569,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.21.1; + MARKETING_VERSION = 3.0.0; PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = YES; @@ -632,7 +632,7 @@ APPLICATION_EXTENSION_API_ONLY = YES; CODE_SIGN_ENTITLEMENTS = HutchWidgetExtension/HutchWidgetExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 60; + CURRENT_PROJECT_VERSION = 61; DEVELOPMENT_TEAM = ZCNAX3VL9D; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = HutchWidgetExtension/Info.plist; @@ -642,7 +642,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.21.1; + MARKETING_VERSION = 3.0.0; PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -661,7 +661,7 @@ APPLICATION_EXTENSION_API_ONLY = YES; CODE_SIGN_ENTITLEMENTS = HutchWidgetExtension/HutchWidgetExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 60; + CURRENT_PROJECT_VERSION = 61; DEVELOPMENT_TEAM = ZCNAX3VL9D; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = HutchWidgetExtension/Info.plist; @@ -671,7 +671,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.21.1; + MARKETING_VERSION = 3.0.0; PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; diff --git a/Hutch/Views/Builds/BuildListView.swift b/Hutch/Views/Builds/BuildListView.swift index aa98066..6f3f934 100644 --- a/Hutch/Views/Builds/BuildListView.swift +++ b/Hutch/Views/Builds/BuildListView.swift @@ -108,8 +108,10 @@ struct BuildListView: View { vm.repoFilter = savedRepoFilter viewModel = vm await vm.loadJobs() - vm.startAutoRefresh(interval: autoRefreshInterval) } + // Restart auto-refresh every time the view (re)appears, since + // onDisappear stops it when navigating away. + viewModel?.startAutoRefresh(interval: autoRefreshInterval) } .onDisappear { viewModel?.stopAutoRefresh() diff --git a/Hutch/Views/Home/HomeView.swift b/Hutch/Views/Home/HomeView.swift index 1a68498..105b62b 100644 --- a/Hutch/Views/Home/HomeView.swift +++ b/Hutch/Views/Home/HomeView.swift @@ -48,7 +48,7 @@ struct HomeView: View { loadRecentActivity() } .onChange(of: scenePhase) { _, newPhase in - guard newPhase == .active, let viewModel else { return } + guard newPhase == .active, let viewModel, viewModel.needsRefresh() else { return } Task { await viewModel.loadDashboard() loadRecentActivity() diff --git a/Hutch/Views/Home/HomeViewModel.swift b/Hutch/Views/Home/HomeViewModel.swift index f0c14dd..ec54759 100644 --- a/Hutch/Views/Home/HomeViewModel.swift +++ b/Hutch/Views/Home/HomeViewModel.swift @@ -188,6 +188,7 @@ final class HomeViewModel { private(set) var projectsError: String? private(set) var assignedTicketsError: String? private(set) var recentBuildsError: String? + private(set) var lastRefreshed: Date? private let currentUser: User private let client: SRHTClient @@ -405,10 +406,17 @@ final class HomeViewModel { systemStatusErrorMessage = error.userFacingMessage } isLoadingSystemStatus = false + lastRefreshed = Date() persistNeedsAttentionSnapshot() persistSystemStatusWidgetSnapshot() } + /// Returns true if sufficient time has elapsed since the last dashboard refresh. + func needsRefresh(after interval: TimeInterval = 60) -> Bool { + guard let lastRefreshed else { return true } + return Date().timeIntervalSince(lastRefreshed) > interval + } + var hasDashboardContent: Bool { !pinnedProjects.isEmpty || !assignedTickets.isEmpty || !recentBuilds.isEmpty || !unreadInboxThreads.isEmpty || systemStatusSnapshot != nil } diff --git a/Hutch/Views/Repositories/RepositoryListView.swift b/Hutch/Views/Repositories/RepositoryListView.swift index 68dd0b0..0258a58 100644 --- a/Hutch/Views/Repositories/RepositoryListView.swift +++ b/Hutch/Views/Repositories/RepositoryListView.swift @@ -88,9 +88,6 @@ struct RepositoryListView: View { ) } .alignmentGuide(.listRowSeparatorLeading) { _ in 0 } - .task { - await viewModel.loadMoreIfNeeded(currentItem: repo) - } } if viewModel.isLoadingMore { @@ -156,7 +153,7 @@ struct RepositoryListView: View { } .srhtErrorBanner(error: $vm.error) .refreshable { - await viewModel.loadRepositories() + await viewModel.loadRepositories(forceRefresh: true) } .task { await viewModel.loadRepositories() diff --git a/Hutch/Views/Repositories/RepositoryListViewModel.swift b/Hutch/Views/Repositories/RepositoryListViewModel.swift index 0cfc8c3..1e830bb 100644 --- a/Hutch/Views/Repositories/RepositoryListViewModel.swift +++ b/Hutch/Views/Repositories/RepositoryListViewModel.swift @@ -46,6 +46,7 @@ final class RepositoryListViewModel { private let client: SRHTClient private let defaults: UserDefaults private var buildStatusTask: Task<Void, Never>? + private var lastBuildStatusRefresh: Date? private static let gitCacheKey = "git.repositories" private static let hgCacheKey = "hg.repositories" @@ -147,7 +148,8 @@ final class RepositoryListViewModel { /// Fetch the first page of repositories. Shows cached data instantly if available, /// then refreshes from the network in the background. /// - Parameter search: Optional search string. Pass `nil` to use the current `searchText`. - func loadRepositories(search: String? = nil) async { + /// - Parameter forceRefresh: When true, bypass the build-status TTL (e.g. pull-to-refresh). + func loadRepositories(search: String? = nil, forceRefresh: Bool = false) async { let query = (search ?? searchText).trimmingCharacters(in: .whitespacesAndNewlines) let isSearch = !query.isEmpty @@ -193,7 +195,7 @@ final class RepositoryListViewModel { } repositories = filteredResults.sorted(by: repositorySortOrder) - scheduleBuildStatusRefresh() + scheduleBuildStatusRefresh(force: forceRefresh) } catch { // Only show error if we have no cached data to fall back on if repositories.isEmpty { @@ -583,7 +585,13 @@ final class RepositoryListViewModel { } } - private func scheduleBuildStatusRefresh() { + private func scheduleBuildStatusRefresh(force: Bool = false) { + // Skip if we already refreshed recently (120-second TTL). Pull-to-refresh + // passes force: true to bypass this check. + if !force, let last = lastBuildStatusRefresh, + Date().timeIntervalSince(last) < 120 { + return + } let repositoriesSnapshot = repositories buildStatusTask?.cancel() buildStatusTask = Task { [weak self] in @@ -635,6 +643,7 @@ final class RepositoryListViewModel { await MainActor.run { guard repositories == self.repositories else { return } latestBuildStatuses = finalStatuses + lastBuildStatusRefresh = Date() } } catch { // Build status is auxiliary data for the list. Leave the default gray state on failure. diff --git a/Hutch/Views/Work/WorkView.swift b/Hutch/Views/Work/WorkView.swift index 1e684d4..a7fbd1c 100644 --- a/Hutch/Views/Work/WorkView.swift +++ b/Hutch/Views/Work/WorkView.swift @@ -30,7 +30,7 @@ struct WorkView: View { await ensureViewModel(currentUser: currentUser).loadDashboard() } .onChange(of: scenePhase) { _, newPhase in - guard newPhase == .active, let viewModel else { return } + guard newPhase == .active, let viewModel, viewModel.needsRefresh() else { return } Task { await viewModel.loadDashboard() } |
