diff --git a/GiteaPR.xcodeproj/project.pbxproj b/GiteaPR.xcodeproj/project.pbxproj index 89a19a6..0cb58c7 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 = 25; + CURRENT_PROJECT_VERSION = 26; DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_CFBundleDisplayName = "GiteaPR"; @@ -197,7 +197,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 3.4; + MARKETING_VERSION = 3.5; 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 = 25; + CURRENT_PROJECT_VERSION = 26; DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_CFBundleDisplayName = "GiteaPR"; @@ -221,7 +221,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 3.4; + MARKETING_VERSION = 3.5; PRODUCT_BUNDLE_IDENTIFIER = com.bueso.giteapr; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/GiteaPR/Detail/PRDetailViewModel.swift b/GiteaPR/Detail/PRDetailViewModel.swift index a0c640c..07f77f6 100644 --- a/GiteaPR/Detail/PRDetailViewModel.swift +++ b/GiteaPR/Detail/PRDetailViewModel.swift @@ -36,6 +36,18 @@ final class PRDetailViewModel: ObservableObject { } } + /// A task cancellation is not a real failure — it happens when a refresh + /// (pull-to-refresh / .task / .onAppear) supersedes an in-flight request, + /// and surfaces as URLError.cancelled ("cancelled"). These must not be + /// shown as errors. + private func isCancellation(_ error: Error) -> Bool { + if let urlError = error as? URLError { + return urlError.code == .cancelled + } + let ns = error as NSError + return ns.domain == NSURLErrorDomain && ns.code == NSURLErrorCancelled + } + func load() async { guard let client = SettingsStore.shared.client() else { return } isLoading = true @@ -48,6 +60,7 @@ final class PRDetailViewModel: ObservableObject { do { pr = try await client.pullRequest(owner: owner, repo: repo.name, number: prNumber) } catch { + if isCancellation(error) { return } // pull-to-refresh superseded; not a real error LogStore.shared.log(.error, "PR \(prNumber): load failed: \(error.localizedDescription)") errorMessage = error.localizedDescription return @@ -79,8 +92,11 @@ final class PRDetailViewModel: ObservableObject { } } catch { // CI is best-effort; don't fail the whole detail view, but log it so - // a token-scope 403 on the Actions endpoint is visible. - LogStore.shared.log(.warn, "PR \(prNumber): actions/runs failed: \(error.localizedDescription)") + // a token-scope 403 on the Actions endpoint is visible. Ignore + // cancellations (refresh superseding an in-flight request). + if !isCancellation(error) { + LogStore.shared.log(.warn, "PR \(prNumber): actions/runs failed: \(error.localizedDescription)") + } } }