From 880cff9e68d26a6661b64aba3123b824f837d320 Mon Sep 17 00:00:00 2001 From: Hermes Date: Thu, 13 Aug 2026 06:43:46 +0000 Subject: [PATCH] Remove in-list spinner + add monitored-repos setting; bump to 2.3 - Remove the in-list loading wheel overlay; the landing page just shows the cached list or the 'No open PRs' screen. - Add a 'Monitored repos' setting (Settings > Inbox) that limits the inbox to specific repos, cutting the number of API calls dramatically. Empty = monitor all accessible repos. Bump to 2.3. --- GiteaPR.xcodeproj/project.pbxproj | 8 +-- GiteaPR/Inbox/InboxViewModel.swift | 17 +++-- GiteaPR/Settings/MonitoredReposView.swift | 84 +++++++++++++++++++++++ GiteaPR/Settings/SettingsStore.swift | 7 ++ GiteaPR/Settings/SettingsView.swift | 13 ++++ GiteaPR/Views/InboxView.swift | 8 --- 6 files changed, 120 insertions(+), 17 deletions(-) create mode 100644 GiteaPR/Settings/MonitoredReposView.swift diff --git a/GiteaPR.xcodeproj/project.pbxproj b/GiteaPR.xcodeproj/project.pbxproj index 3e2f932..74b2579 100644 --- a/GiteaPR.xcodeproj/project.pbxproj +++ b/GiteaPR.xcodeproj/project.pbxproj @@ -187,7 +187,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 13; + CURRENT_PROJECT_VERSION = 14; DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_CFBundleDisplayName = "GiteaPR"; @@ -197,7 +197,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.2; + MARKETING_VERSION = 2.3; PRODUCT_BUNDLE_IDENTIFIER = com.bueso.giteapr; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -211,7 +211,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 13; + CURRENT_PROJECT_VERSION = 14; DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_CFBundleDisplayName = "GiteaPR"; @@ -221,7 +221,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.2; + MARKETING_VERSION = 2.3; PRODUCT_BUNDLE_IDENTIFIER = com.bueso.giteapr; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/GiteaPR/Inbox/InboxViewModel.swift b/GiteaPR/Inbox/InboxViewModel.swift index d80cb10..543f8f9 100644 --- a/GiteaPR/Inbox/InboxViewModel.swift +++ b/GiteaPR/Inbox/InboxViewModel.swift @@ -47,14 +47,21 @@ final class InboxViewModel: ObservableObject { do { let repos = try await client.repos() - LogStore.shared.log(.info, "Inbox: \(repos.count) repos") - // Fetch open PRs across all repos concurrently. A single repo that - // fails (e.g. 404) must NOT wipe the whole inbox — catch per-repo - // errors, log them, and keep whatever succeeded. + // If the user has configured monitored repos, only fetch PRs for + // those — this cuts the number of API calls dramatically (the + // inbox was fetching every accessible repo). + let monitored = SettingsStore.shared.monitoredRepos + let targets = monitored.isEmpty + ? repos + : repos.filter { monitored.contains($0.fullName) } + LogStore.shared.log(.info, "Inbox: \(targets.count)/\(repos.count) repos") + // Fetch open PRs across the target repos concurrently. A single + // repo that fails (e.g. 404) must NOT wipe the whole inbox — catch + // per-repo errors, log them, and keep whatever succeeded. var rows: [PRRow] = [] var failures: [String] = [] await withTaskGroup(of: Result<[PRRow], Error>.self) { group in - for repo in repos { + for repo in targets { group.addTask { do { let prs = try await client.pullRequests(owner: repo.owner.login, diff --git a/GiteaPR/Settings/MonitoredReposView.swift b/GiteaPR/Settings/MonitoredReposView.swift new file mode 100644 index 0000000..f00f569 --- /dev/null +++ b/GiteaPR/Settings/MonitoredReposView.swift @@ -0,0 +1,84 @@ +import SwiftUI + +/// Lets the user pick which repos the inbox monitors. Fetches the accessible +/// repos from the API and shows a toggle per repo. Empty selection = monitor +/// all. +struct MonitoredReposView: View { + @Binding var selection: [String] + @State private var repos: [Repository] = [] + @State private var isLoading = true + @State private var errorMessage: String? + + var body: some View { + Group { + if isLoading { + ProgressView("Loading repos…") + } else if let errorMessage { + ContentUnavailableView("Couldn't load repos", + systemImage: "exclamationmark.triangle", + description: Text(errorMessage)) + } else { + List { + Section { + Button { + selection = [] + } label: { + HStack { + Text("Monitor all repos") + Spacer() + if selection.isEmpty { + Image(systemName: "checkmark") + .foregroundColor(.accentColor) + } + } + } + } + Section("Monitored") { + ForEach(repos) { repo in + Button { + toggle(repo.fullName) + } label: { + HStack { + Text(repo.fullName) + .foregroundColor(.primary) + Spacer() + if selection.contains(repo.fullName) { + Image(systemName: "checkmark") + .foregroundColor(.accentColor) + } + } + } + } + } + } + } + } + .navigationTitle("Monitored repos") + .navigationBarTitleDisplayMode(.inline) + .task { await load() } + } + + private func load() async { + isLoading = true + errorMessage = nil + defer { isLoading = false } + guard let client = SettingsStore.shared.client() else { + errorMessage = "Not configured." + return + } + do { + repos = try await client.repos() + .sorted { $0.fullName < $1.fullName } + } catch { + errorMessage = error.localizedDescription + } + } + + private func toggle(_ fullName: String) { + if selection.contains(fullName) { + selection.removeAll { $0 == fullName } + } else { + selection.append(fullName) + } + } +} diff --git a/GiteaPR/Settings/SettingsStore.swift b/GiteaPR/Settings/SettingsStore.swift index 2d20746..0a3f997 100644 --- a/GiteaPR/Settings/SettingsStore.swift +++ b/GiteaPR/Settings/SettingsStore.swift @@ -27,6 +27,12 @@ final class SettingsStore: ObservableObject { didSet { UserDefaults.standard.set(deleteBranchAfterMerge, forKey: "deleteBranchAfterMerge") } } + /// Repos to monitor for the inbox. Empty = monitor all accessible repos. + /// Stored as full names (e.g. "luis/config-nixos"). + @Published var monitoredRepos: [String] { + didSet { UserDefaults.standard.set(monitoredRepos, + forKey: "monitoredRepos") } + } private init() { instanceURL = KeychainHelper.get("instanceURL") ?? "https://gitea.bueso.eu" @@ -34,6 +40,7 @@ final class SettingsStore: ObservableObject { let raw = UserDefaults.standard.string(forKey: "mergeGateMode") mergeGateMode = MergeGateMode(rawValue: raw ?? "") ?? .warn deleteBranchAfterMerge = UserDefaults.standard.bool(forKey: "deleteBranchAfterMerge") + monitoredRepos = UserDefaults.standard.stringArray(forKey: "monitoredRepos") ?? [] } var isConfigured: Bool { diff --git a/GiteaPR/Settings/SettingsView.swift b/GiteaPR/Settings/SettingsView.swift index 396b479..1c3db75 100644 --- a/GiteaPR/Settings/SettingsView.swift +++ b/GiteaPR/Settings/SettingsView.swift @@ -6,6 +6,7 @@ struct SettingsView: View { @State private var token: String @State private var mergeGateMode: MergeGateMode @State private var deleteBranchAfterMerge: Bool + @State private var monitoredRepos: [String] @State private var testResult: TestResult? @State private var testing = false @State private var showLogoutConfirm = false @@ -20,6 +21,7 @@ struct SettingsView: View { _token = State(initialValue: SettingsStore.shared.token) _mergeGateMode = State(initialValue: SettingsStore.shared.mergeGateMode) _deleteBranchAfterMerge = State(initialValue: SettingsStore.shared.deleteBranchAfterMerge) + _monitoredRepos = State(initialValue: SettingsStore.shared.monitoredRepos) } var body: some View { @@ -54,6 +56,16 @@ struct SettingsView: View { Toggle("Delete branch after merge", isOn: $deleteBranchAfterMerge) } + Section { + NavigationLink("Monitored repos") { + MonitoredReposView(selection: $monitoredRepos) + } + } header: { + Text("Inbox") + } footer: { + Text("Limit the landing page to specific repos to speed it up. Leave empty to monitor all accessible repos.") + } + Section("Diagnostics") { NavigationLink("View logs") { LogsView() @@ -134,6 +146,7 @@ struct SettingsView: View { SettingsStore.shared.token = token SettingsStore.shared.mergeGateMode = mergeGateMode SettingsStore.shared.deleteBranchAfterMerge = deleteBranchAfterMerge + SettingsStore.shared.monitoredRepos = monitoredRepos dismiss() } } diff --git a/GiteaPR/Views/InboxView.swift b/GiteaPR/Views/InboxView.swift index 02e1892..49f3ebb 100644 --- a/GiteaPR/Views/InboxView.swift +++ b/GiteaPR/Views/InboxView.swift @@ -52,14 +52,6 @@ struct InboxView: View { didInitialLoad = true await vm.load() } - .overlay { - // Only show a spinner when there's nothing cached to display - // yet; with a cache the list renders instantly and refreshes - // in the background. - if vm.isLoading && vm.displayedRows.isEmpty { - ProgressView() - } - } .alert("Error", isPresented: .init( get: { vm.errorMessage != nil }, set: { if !$0 { vm.errorMessage = nil } })) {