diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index e55109735a..a5edb84b8f 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -5826,7 +5826,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 0; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = NKUJUXUJ3B; @@ -5853,7 +5853,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 33.0.7; + MARKETING_VERSION = 33.0.8; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "-v"; OTHER_LDFLAGS = ""; @@ -5893,7 +5893,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 0; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = NKUJUXUJ3B; @@ -5918,7 +5918,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 33.0.7; + MARKETING_VERSION = 33.0.8; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "-v"; OTHER_LDFLAGS = ""; diff --git a/iOSClient/GUI/Lucid Banner/ErrorBannerView.swift b/iOSClient/GUI/Lucid Banner/ErrorBannerView.swift index 95837f8fd8..f1a52ddf28 100644 --- a/iOSClient/GUI/Lucid Banner/ErrorBannerView.swift +++ b/iOSClient/GUI/Lucid Banner/ErrorBannerView.swift @@ -29,8 +29,11 @@ func showErrorBanner(windowScene: UIWindowScene?, } #if !EXTENSION - guard !bannerContainsError(errorCode: errorCode, afError: afError) else { - return + if let errorCode, + let controller = SceneManager.shared.getController(scene: windowScene) { + if await !ErrorBannerGate.shared.shouldShow(errorCode: errorCode, account: controller.account) { + return + } } #endif diff --git a/iOSClient/GUI/Lucid Banner/HelperBanner.swift b/iOSClient/GUI/Lucid Banner/HelperBanner.swift index 013b21d94b..2c2b290a17 100644 --- a/iOSClient/GUI/Lucid Banner/HelperBanner.swift +++ b/iOSClient/GUI/Lucid Banner/HelperBanner.swift @@ -124,41 +124,67 @@ func horizontalLayoutBanner(bounds: CGRect, } } -#if !EXTENSION +/// Prevents the same error banner from being shown repeatedly in a short time. +/// Uses a per-error (and optional account) cooldown to avoid UI spam. +/// Call `shouldShow(...)` before presenting a banner. +actor ErrorBannerGate { + static let shared = ErrorBannerGate() -// Error 401 (maintenance mode) -// Error 423 (locked) -// Error 507 (insufficient storage) -// Error -1009 (NSURLErrorNotConnectedToInternet) -// Error -1003 (NSURLError​Cannot​Find​Host) + private var lastShownByKey: [String: Date] = [:] + private let maxEntryAge: TimeInterval = 120 -func bannerContainsError(errorCode: Int?, afError: AFError? = nil) -> Bool { - guard let errorCode else { - return false - } - // List of errors not to be displayed - if errorCode == -999 || errorCode == 423 { + private init() {} + + func shouldShow(errorCode: Int, account: String? = nil) -> Bool { + cleanupOldEntries() + + let key = makeKey(errorCode: errorCode, account: account) + let now = Date() + let cooldown = cooldownInterval(for: errorCode) + + if let lastShown = lastShownByKey[key], + now.timeIntervalSince(lastShown) < cooldown { + return false + } + + lastShownByKey[key] = now return true } - if let afError, case .explicitlyCancelled = afError { - return true + + // MARK: - Private + + private func makeKey(errorCode: Int, account: String?) -> String { + "\(errorCode)|\(account ?? "-")" } - // Prevent repeated display of the same user-facing error during the current foreground session. - // If this error code has already been shown, do nothing. - // Otherwise, record it and allow the UX notification to be displayed once. - if shownErrors.contains(errorCode) { - return true - } else { - // Coalesce user-facing errors across the current foreground session. - // The same error code is shown to the user only once. - if errorCode == 401 || - errorCode == 423 || - errorCode == 507 || - errorCode == NSURLErrorNotConnectedToInternet || - errorCode == NSURLErrorCannotFindHost { - shownErrors.insert(errorCode) + + private func cooldownInterval(for errorCode: Int) -> TimeInterval { + switch errorCode { + + case NSURLErrorNotConnectedToInternet: + return 30 // No internet connection (persistent until network changes) + + case NSURLErrorCannotFindHost: + return 30 // Host/DNS not reachable (likely server down or misconfigured URL) + + case 401: + return 30 // Unauthorized (server maintenance) + + case 423: + return 20 // Resource locked (temporary server-side condition) + + case 507: + return 30 // Insufficient storage (server quota exceeded, persistent) + + default: + return 5 // Transient or unknown error + } + } + + private func cleanupOldEntries() { + let now = Date() + + lastShownByKey = lastShownByKey.filter { _, lastShown in + now.timeIntervalSince(lastShown) < maxEntryAge } - return false } } -#endif diff --git a/iOSClient/GUI/Lucid Banner/InfoBannerView.swift b/iOSClient/GUI/Lucid Banner/InfoBannerView.swift index 86bc7144dc..5566a49a90 100644 --- a/iOSClient/GUI/Lucid Banner/InfoBannerView.swift +++ b/iOSClient/GUI/Lucid Banner/InfoBannerView.swift @@ -19,8 +19,11 @@ func showInfoBanner(windowScene: UIWindowScene?, } #if !EXTENSION - guard !bannerContainsError(errorCode: errorCode) else { - return + if let errorCode, + let controller = SceneManager.shared.getController(scene: windowScene) { + if await !ErrorBannerGate.shared.shouldShow(errorCode: errorCode, account: controller.account) { + return + } } #endif diff --git a/iOSClient/GUI/Lucid Banner/ShowBanner.swift b/iOSClient/GUI/Lucid Banner/ShowBanner.swift index ce3d972e4e..8627cbc277 100644 --- a/iOSClient/GUI/Lucid Banner/ShowBanner.swift +++ b/iOSClient/GUI/Lucid Banner/ShowBanner.swift @@ -32,8 +32,11 @@ func showBanner(windowScene: UIWindowScene?, } #if !EXTENSION - guard !bannerContainsError(errorCode: errorCode) else { - return (nil, nil) + if let errorCode, + let controller = SceneManager.shared.getController(scene: windowScene) { + if await !ErrorBannerGate.shared.shouldShow(errorCode: errorCode, account: controller.account) { + return(nil, nil) + } } #endif diff --git a/iOSClient/GUI/Lucid Banner/WarningBannerView.swift b/iOSClient/GUI/Lucid Banner/WarningBannerView.swift index fbeced0226..f1ba8d00dc 100644 --- a/iOSClient/GUI/Lucid Banner/WarningBannerView.swift +++ b/iOSClient/GUI/Lucid Banner/WarningBannerView.swift @@ -21,8 +21,11 @@ func showWarningBanner(windowScene: UIWindowScene?, } #if !EXTENSION - guard !bannerContainsError(errorCode: errorCode) else { - return + if let errorCode, + let controller = SceneManager.shared.getController(scene: windowScene) { + if await !ErrorBannerGate.shared.shouldShow(errorCode: errorCode, account: controller.account) { + return + } } #endif diff --git a/iOSClient/NCAppStateManager.swift b/iOSClient/NCAppStateManager.swift index d5221cfa84..533e71b2ba 100644 --- a/iOSClient/NCAppStateManager.swift +++ b/iOSClient/NCAppStateManager.swift @@ -14,9 +14,6 @@ var isAppInBackground: Bool = true // Global flag indicating whether the app is in maintenanceMode. var maintenanceMode: Bool = false -// Global error code -var shownErrors: Set = [] - /// Singleton responsible for monitoring and managing app state transitions. /// /// This class observes system notifications related to app lifecycle events and updates global flags accordingly: @@ -53,11 +50,6 @@ final class NCAppStateManager { appDelegate?.pushSubscriptionTask?.cancel() appDelegate?.pushSubscriptionTask = nil - // - // Clear the errors - // - shownErrors.removeAll() - nkLog(debug: "Application did enter in background") } } diff --git a/iOSClient/Networking/NCNetworkingProcess.swift b/iOSClient/Networking/NCNetworkingProcess.swift index 6ebe157bfe..93ab4dd4ce 100644 --- a/iOSClient/Networking/NCNetworkingProcess.swift +++ b/iOSClient/Networking/NCNetworkingProcess.swift @@ -281,9 +281,6 @@ actor NCNetworkingProcess { // Set Live Photo await NCNetworking.shared.setLivePhoto(account: currentAccount) - // Clear the errors - shownErrors.removeAll() - if lastUsedInterval != maxInterval { await startTimer(interval: maxInterval) }