diff --git a/File Provider Extension/FileProviderExtension+Actions.swift b/File Provider Extension/FileProviderExtension+Actions.swift index aabb2a5cf8..164d1c3add 100644 --- a/File Provider Extension/FileProviderExtension+Actions.swift +++ b/File Provider Extension/FileProviderExtension+Actions.swift @@ -139,7 +139,7 @@ extension FileProviderExtension { } else { - completionHandler(nil, NSFileProviderError(.noSuchItem, userInfo: [NSLocalizedDescriptionKey: resultsMove.error.errorDescription, NSLocalizedFailureReasonErrorKey: ""])) + completionHandler(nil, NSFileProviderError(.noSuchItem, userInfo: [NSLocalizedDescriptionKey: resultsMove.error.localizedGenericMessage, NSLocalizedFailureReasonErrorKey: ""])) return } } @@ -176,7 +176,7 @@ extension FileProviderExtension { } else if resultsMove.error.errorCode == NCGlobal.shared.errorBadRequest { - completionHandler(nil, NSFileProviderError(.noSuchItem, userInfo: [NSLocalizedDescriptionKey: resultsMove.error.errorDescription, NSLocalizedFailureReasonErrorKey: ""])) + completionHandler(nil, NSFileProviderError(.noSuchItem, userInfo: [NSLocalizedDescriptionKey: resultsMove.error.localizedGenericMessage, NSLocalizedFailureReasonErrorKey: ""])) return } else { diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 657a6f9b67..5a4fe4a667 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -12,8 +12,10 @@ 2C1D5D7923E2DE9100334ABB /* NCBrand.swift in Sources */ = {isa = PBXBuildFile; fileRef = F76B3CCD1EAE01BD00921AC9 /* NCBrand.swift */; }; 2C33C48223E2C475005F963B /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C33C48123E2C475005F963B /* NotificationService.swift */; }; 2C33C48623E2C475005F963B /* Notification Service Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 2C33C47F23E2C475005F963B /* Notification Service Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 2EF7D3FD0885CE182717578E /* NKError+UserFriendlyMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79DFE0BDF3898A66910E67CA /* NKError+UserFriendlyMessage.swift */; }; 370D26AF248A3D7A00121797 /* NCCellMain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 370D26AE248A3D7A00121797 /* NCCellMain.swift */; }; 63563FE53573412998852DD3 /* NCAlbumsPersistenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 439616AAA50F4360A761E5EF /* NCAlbumsPersistenceTests.swift */; }; + 822D68B06B101563A759AD59 /* NKError+UserFriendlyMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79DFE0BDF3898A66910E67CA /* NKError+UserFriendlyMessage.swift */; }; A12720261001000000000001 /* NCBackgroundUploadExtensionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F793E7C230417C7100F011FE /* NCBackgroundUploadExtensionManager.swift */; }; A1E2EE010000000000000002 /* NCEndToEndKeySet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1E2EE010000000000000001 /* NCEndToEndKeySet.swift */; }; A1E2EE010000000000000003 /* NCEndToEndKeySet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1E2EE010000000000000001 /* NCEndToEndKeySet.swift */; }; @@ -28,6 +30,7 @@ A1E2EE020000000000000005 /* NCEndToEndKeySetResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1E2EE020000000000000002 /* NCEndToEndKeySetResolver.swift */; }; A1E2EE020000000000000006 /* NCEndToEndKeySetResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1E2EE020000000000000002 /* NCEndToEndKeySetResolver.swift */; }; A1E2EE020000000000000008 /* NCEndToEndKeySetResolverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1E2EE020000000000000007 /* NCEndToEndKeySetResolverTests.swift */; }; + A358B569BF5FDABA6D9012D1 /* NCErrorContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0CD9A00D74B01055F238E9 /* NCErrorContext.swift */; }; A5A87F9E4B0E4441A6A4BC20 /* NCContextMenuProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB7697C94BA14450A0867940 /* NCContextMenuProfile.swift */; }; AA3C85E82D36B08C00F74F12 /* UITestBackend.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3C85E72D36B08C00F74F12 /* UITestBackend.swift */; }; AA3C85EB2D36BBFB00F74F12 /* OCSResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3C85EA2D36BBF400F74F12 /* OCSResponse.swift */; }; @@ -112,6 +115,7 @@ AFCE353727E4ED7B00FEA6C2 /* NCShareCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFCE353627E4ED7B00FEA6C2 /* NCShareCells.swift */; }; AFCE353927E5DE0500FEA6C2 /* Shareable.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFCE353827E5DE0400FEA6C2 /* Shareable.swift */; }; B601AA012F10000100C0D3E5 /* NCLogFilesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B601AA002F10000100C0D3E5 /* NCLogFilesView.swift */; }; + BAF64949B487A080A98F83F2 /* NKErrorUserFriendlyMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 320C20803A58DAF7575440A8 /* NKErrorUserFriendlyMessageTests.swift */; }; C0DEA070306D000100000005 /* NCLocalDatabase+AutoUpload.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DEA070306D000100000004 /* NCLocalDatabase+AutoUpload.swift */; }; C0DEA070306D000100000006 /* NCLocalDatabase+AutoUpload.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DEA070306D000100000004 /* NCLocalDatabase+AutoUpload.swift */; }; C0DEA070306D000100000008 /* NCAutoUploadAssetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DEA070306D000100000007 /* NCAutoUploadAssetTests.swift */; }; @@ -119,6 +123,7 @@ C0DEDA7E30C1000100A00002 /* NCAutoUploadDateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DEDA7E30C1000100A00001 /* NCAutoUploadDateTests.swift */; }; CB3666201AF7550816B5CD6A /* NCContextMenuComment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8932E90EC4278026D86CCCC9 /* NCContextMenuComment.swift */; }; D5B6AA7827200C7200D49C24 /* NCActivityTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5B6AA7727200C7200D49C24 /* NCActivityTableViewCell.swift */; }; + EB511D22564B7B9A605E70AE /* NCErrorContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0CD9A00D74B01055F238E9 /* NCErrorContext.swift */; }; F0A1B2C430B5000100D4E5F6 /* NCMediaViewerFloatingTitleViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A1B2C330B5000100D4E5F6 /* NCMediaViewerFloatingTitleViewTests.swift */; }; F0A1B2C630B6000100D4E5F6 /* NCImageZoomViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A1B2C530B6000100D4E5F6 /* NCImageZoomViewTests.swift */; }; F31165022F9674A1009A1E37 /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = F31165012F9674A1009A1E37 /* AppIcon.icon */; }; @@ -1334,9 +1339,11 @@ 2C33C47F23E2C475005F963B /* Notification Service Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Notification Service Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; 2C33C48123E2C475005F963B /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; 2C33C48A23E2CC26005F963B /* Notification_Service_Extension-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Notification_Service_Extension-Bridging-Header.h"; sourceTree = ""; }; + 320C20803A58DAF7575440A8 /* NKErrorUserFriendlyMessageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NKErrorUserFriendlyMessageTests.swift; sourceTree = ""; }; 370D26AE248A3D7A00121797 /* NCCellMain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCCellMain.swift; sourceTree = ""; }; 439616AAA50F4360A761E5EF /* NCAlbumsPersistenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAlbumsPersistenceTests.swift; sourceTree = ""; }; 4B74B947E07444CC9CFE0A6C /* NCDocumentEditorSupportTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCDocumentEditorSupportTests.swift; sourceTree = ""; }; + 79DFE0BDF3898A66910E67CA /* NKError+UserFriendlyMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NKError+UserFriendlyMessage.swift"; sourceTree = ""; }; 8932E90EC4278026D86CCCC9 /* NCContextMenuComment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCContextMenuComment.swift; sourceTree = ""; }; A1E2EE010000000000000001 /* NCEndToEndKeySet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCEndToEndKeySet.swift; sourceTree = ""; }; A1E2EE010000000000000009 /* NCEndToEndKeySetArchiveTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCEndToEndKeySetArchiveTests.swift; sourceTree = ""; }; @@ -1480,6 +1487,7 @@ C0DEA070306D000100000007 /* NCAutoUploadAssetTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAutoUploadAssetTests.swift; sourceTree = ""; }; C0DECA012F65000100C0D001 /* NCCameraRollTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCCameraRollTests.swift; sourceTree = ""; }; C0DEDA7E30C1000100A00001 /* NCAutoUploadDateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAutoUploadDateTests.swift; sourceTree = ""; }; + CE0CD9A00D74B01055F238E9 /* NCErrorContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCErrorContext.swift; sourceTree = ""; }; D5B6AA7727200C7200D49C24 /* NCActivityTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCActivityTableViewCell.swift; sourceTree = ""; }; F0A1B2C330B5000100D4E5F6 /* NCMediaViewerFloatingTitleViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCMediaViewerFloatingTitleViewTests.swift; sourceTree = ""; }; F0A1B2C530B6000100D4E5F6 /* NCImageZoomViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCImageZoomViewTests.swift; sourceTree = ""; }; @@ -2388,6 +2396,7 @@ children = ( AC13ADAF2026101300000001 /* NCMediaAdaptiveLayoutViewTests.swift */, C0DEDA7E30C1000100A00001 /* NCAutoUploadDateTests.swift */, + 320C20803A58DAF7575440A8 /* NKErrorUserFriendlyMessageTests.swift */, C0DEA070306D000100000007 /* NCAutoUploadAssetTests.swift */, F76BED7B306D0000002E1442 /* NCPhotoLibraryAssetTests.swift */, A1E2EE010000000000000009 /* NCEndToEndKeySetArchiveTests.swift */, @@ -2814,6 +2823,8 @@ F785129A2D79899E0087DDD0 /* NCNetworking+TermsOfService.swift */, F71916102E2901E800E13E96 /* NCNetworking+Upload.swift */, F7327E2F2B73A86700A462C7 /* NCNetworking+WebDAV.swift */, + 79DFE0BDF3898A66910E67CA /* NKError+UserFriendlyMessage.swift */, + CE0CD9A00D74B01055F238E9 /* NCErrorContext.swift */, F70D8D8024A4A9BF000A5756 /* NCNetworkingProcess.swift */, F755BD9A20594AC7008C5FBB /* NCService.swift */, ); @@ -4851,6 +4862,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + BAF64949B487A080A98F83F2 /* NKErrorUserFriendlyMessageTests.swift in Sources */, AC13ADAF2026101300000002 /* NCMediaAdaptiveLayoutViewTests.swift in Sources */, C0DEDA7E30C1000100A00002 /* NCAutoUploadDateTests.swift in Sources */, C0DEA070306D000100000008 /* NCAutoUploadAssetTests.swift in Sources */, @@ -4944,6 +4956,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 2EF7D3FD0885CE182717578E /* NKError+UserFriendlyMessage.swift in Sources */, + EB511D22564B7B9A605E70AE /* NCErrorContext.swift in Sources */, F7864ACF2A78FE73004870E0 /* NCManageDatabase+LocalFile.swift in Sources */, F71F6D0A2B6A6A5E00F1EB15 /* ThreadSafeArray.swift in Sources */, F7245925289BB59100474787 /* ThreadSafeDictionary.swift in Sources */, @@ -5206,6 +5220,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 822D68B06B101563A759AD59 /* NKError+UserFriendlyMessage.swift in Sources */, + A358B569BF5FDABA6D9012D1 /* NCErrorContext.swift in Sources */, F76BED77306D0000002E1442 /* NCLocalDatabase.swift in Sources */, F76BED78306D0000002E1442 /* NCLocalDatabaseMigrator.swift in Sources */, F76BED7C306D0000002E1442 /* NCLocalDatabase+PhotoLibraryAsset.swift in Sources */, @@ -7279,8 +7295,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/nextcloud/NextcloudKit"; requirement = { - kind = exactVersion; - version = 7.6.2; + branch = "friendly-errors"; + kind = branch; }; }; F788ECC5263AAAF900ADC67F /* XCRemoteSwiftPackageReference "MarkdownKit" */ = { diff --git a/Nextcloud.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Nextcloud.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 87db5d5f5e..db676d9f57 100644 --- a/Nextcloud.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Nextcloud.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -204,8 +204,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/nextcloud/NextcloudKit", "state" : { - "revision" : "705e69cf0526cac0bdeaf21699c41c895cae42a0", - "version" : "7.6.2" + "branch" : "friendly-errors", + "revision" : "4ac49ab5fa4e5f3b112afff7658713d52dbaf593" } }, { diff --git a/Share/NCShareExtension+Files.swift b/Share/NCShareExtension+Files.swift index 2d49ffcbc6..ab7db35aea 100644 --- a/Share/NCShareExtension+Files.swift +++ b/Share/NCShareExtension+Files.swift @@ -60,8 +60,8 @@ extension NCShareExtension { if resultsReadFolder.error == .success { self.metadataFolder = resultsReadFolder.metadataFolder await self.reloadData() - } else { - self.showAlert(description: resultsReadFolder.error.errorDescription) + } else if let message = resultsReadFolder.error.userFriendlyMessage() { + self.showAlert(description: message) } } } diff --git a/Share/NCShareExtension.swift b/Share/NCShareExtension.swift index 65cf8eb0b7..f22b4564d1 100644 --- a/Share/NCShareExtension.swift +++ b/Share/NCShareExtension.swift @@ -344,13 +344,13 @@ class NCShareExtension: UIViewController { guard let capabilities = NCNetworking.shared.capabilities[session.account] else { return } - let alertController = UIAlertController.createFolderWith(serverUrl: serverUrl, session: session, capabilities: capabilities) { error in + let alertController = UIAlertController.createFolderWith(serverUrl: serverUrl, session: session, capabilities: capabilities) { error, folderName in if error == .success { Task { await self.loadFolder() } - } else { - self.showAlert(title: "_error_createsubfolders_upload_", description: error.errorDescription) + } else if let message = error.userFriendlyMessage(for: .createFolder(folderName: folderName)) { + self.showAlert(title: "_error_createsubfolders_upload_", description: message) } } self.present(alertController, animated: true) @@ -444,6 +444,7 @@ extension NCShareExtension { await NCNetworkingE2EE.beginNewServerKeyValidationCycle() var error: NKError? + var errorContext: NCErrorContext = .generic guard let window = self.view.window else { return } @@ -468,6 +469,7 @@ extension NCShareExtension { error = await self.upload(metadata: metadata) if error != .success { + errorContext = .upload(fileName: metadata.fileNameView) break } } @@ -475,7 +477,7 @@ extension NCShareExtension { if error == .success { banner?.update(payload: LucidBannerPayload.Update(stage: .success, horizontalLayout: .centered(width: 100)), for: self.token) } else { - banner?.update(payload: LucidBannerPayload.Update(subtitle: error?.errorDescription, stage: .error), for: self.token) + banner?.update(payload: LucidBannerPayload.Update(subtitle: error?.userFriendlyMessage(for: errorContext) ?? "", stage: .error), for: self.token) } Task { @MainActor in diff --git a/Tests/NextcloudUnitTests/NKErrorUserFriendlyMessageTests.swift b/Tests/NextcloudUnitTests/NKErrorUserFriendlyMessageTests.swift new file mode 100644 index 0000000000..8a20bee894 --- /dev/null +++ b/Tests/NextcloudUnitTests/NKErrorUserFriendlyMessageTests.swift @@ -0,0 +1,135 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Milen Pivchev +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation +import NextcloudKit +import Testing +@testable import Nextcloud + +@Suite("Error messages shown to people") +struct NKErrorUserFriendlyMessageTests { + static let shownKinds = NKError.Kind.allCases.filter { $0 != .cancelled } + + private func localized(_ key: String) -> String { + NSLocalizedString(key, comment: "") + } + + @Test("Nothing is shown for success or cancellation") + func silentErrors() { + #expect(NKError.success.userFriendlyMessage() == nil) + #expect(NKError.cancelled.userFriendlyMessage(for: .upload(fileName: "a.jpg")) == nil) + #expect(NKError(errorCode: NCGlobal.shared.errorTaskCancelled, errorDescription: "Task cancelled").userFriendlyMessage() == nil) + } + + @Test("Keeps text the app wrote itself") + func appMessages() { + #expect(NKError(errorCode: NCGlobal.shared.errorOfflineNotAllowed, errorDescription: "_offline_not_allowed_").userFriendlyMessage() == localized("_offline_not_allowed_")) + #expect(NKError(errorCode: NCGlobal.shared.errorE2EELock, errorDescription: "Already translated").userFriendlyMessage() == "Already translated") + } + + @Test("Replaces technical server text") + func technicalServerText() throws { + let error = NKError(errorCode: 409, errorDescription: "Parent node does not exist") + let generic = try #require(error.userFriendlyMessage()) + + #expect(error.userFriendlyMessage(for: .createFolder(folderName: "a/b")) == localized("_error_create_folder_no_parent_")) + #expect(generic != error.errorDescription) + #expect(!generic.contains("409")) + } + + @Test("Uses the generic message unless the operation needs its own") + func operationMessages() { + let forbidden = NKError(errorCode: 403, errorDescription: "403: Forbidden") + let generic = forbidden.userFriendlyMessage() + + #expect(generic != nil) + #expect(forbidden.userFriendlyMessage(for: .delete(fileName: "a.jpg")) == generic) + #expect(forbidden.userFriendlyMessage(for: .rename) == generic) + #expect(forbidden.userFriendlyMessage(for: .upload(fileName: "a.jpg")) == generic) + } + + @Test("Explains document and login failures") + func documentAndLoginMessages() { + let forbidden = NKError(errorCode: 403, errorDescription: "403: Forbidden") + let notFound = NKError(errorCode: 404, errorDescription: "404: Not found") + let unauthorized = NKError(errorCode: 401, errorDescription: "401: Unauthorized") + let offline = NKError(errorCode: NSURLErrorNotConnectedToInternet, errorDescription: "The Internet connection appears to be offline.") + + #expect(forbidden.userFriendlyMessage(for: .createDocument(fileName: "a.md")) == localized("_error_create_document_")) + #expect(forbidden.userFriendlyMessage(for: .openDocument(fileName: "a.md")) == localized("_error_open_document_")) + #expect(notFound.userFriendlyMessage(for: .openDocument(fileName: "a.md")) == notFound.userFriendlyMessage()) + #expect(unauthorized.userFriendlyMessage(for: .login) == localized("_error_login_credentials_")) + #expect(forbidden.userFriendlyMessage(for: .login) == localized("_error_login_credentials_")) + #expect(offline.userFriendlyMessage(for: .login) == offline.userFriendlyMessage()) + #expect(NKError(error: CancellationError()).userFriendlyMessage() == nil) + } + + @Test("Recognizes uploads rejected by the virus scanner") + func virusDetected() { + let error = NKError(errorCode: 415, errorDescription: "Virus Eicar-Signature is detected in the file. Upload cannot be completed.") + + #expect(error.userFriendlyMessage(for: .upload(fileName: "eicar.com")) == localized("_virus_detect_")) + } + + @Test("Words different kinds of failure differently") + func kindMessages() { + let timeout = NKError(errorCode: NSURLErrorTimedOut, errorDescription: "The request timed out.").userFriendlyMessage() + let quota = NKError(errorCode: 507, errorDescription: "507: Storage quota is reached").userFriendlyMessage() + let unknown = NKError(errorCode: 418, errorDescription: "418: I'm a teapot").userFriendlyMessage() + + #expect(Set([timeout, quota, unknown]).count == 3) + #expect(quota != "507: Storage quota is reached") + } + + @Test("Every kind has a translated message", arguments: NKErrorUserFriendlyMessageTests.shownKinds) + func everyKind(kind: NKError.Kind) { + let message = NKError(errorCode: -1, errorDescription: "Technical", kind: kind).userFriendlyMessage() + + #expect(message != nil) + #expect(message != "Technical") + } + + @Test("Maps transfer events and saved statuses to operations") + func contexts() { + let global = NCGlobal.shared + + #expect(NCErrorContext(networkingStatus: global.networkingStatusUploaded, fileName: "a.jpg") == .upload(fileName: "a.jpg")) + #expect(NCErrorContext(networkingStatus: global.networkingStatusCopyMove, fileName: "a.jpg") == .moveOrCopy(fileName: "a.jpg")) + #expect(NCErrorContext(networkingStatus: global.networkingStatusReloadAvatar, fileName: "") == .generic) + #expect(NCErrorContext(metadataStatus: global.metadataStatusWaitCreateFolder, fileName: "Photos") == .createFolder(folderName: "Photos")) + #expect(NCErrorContext(metadataStatus: global.metadataStatusDownloadError, fileName: "a.jpg") == .download(fileName: "a.jpg")) + } + + @Test("Titles say what failed") + func failureTitles() throws { + let contexts: [NCErrorContext] = [.upload(fileName: "f"), .download(fileName: "f"), .delete(fileName: "f"), .rename, .moveOrCopy(fileName: "f"), .createFolder(folderName: "f"), .favorite(fileName: "f"), .createDocument(fileName: "f"), .openDocument(fileName: "f")] + + #expect(NCErrorContext.generic.failureTitle == nil) + #expect(NCErrorContext.login.failureTitle == nil) + #expect(NCErrorContext.upload(fileName: "IMG_0042.jpg").failureTitle?.contains("IMG_0042.jpg") == true) + + for context in contexts { + let title = try #require(context.failureTitle) + + #expect(!title.hasPrefix("_")) + } + } + + @Test("Words saved transfer errors") + func savedErrors() { + let metadata = tableMetadata() + metadata.fileNameView = "a.jpg" + metadata.status = NCGlobal.shared.metadataStatusUploadError + metadata.errorCode = 507 + metadata.sessionError = "507: Storage quota is reached" + + #expect(metadata.sessionErrorMessage == NKError(errorCode: 507, errorDescription: "").userFriendlyMessage()) + + metadata.errorCode = NSURLErrorCancelled + metadata.sessionError = "Transfer was cancelled." + + #expect(metadata.sessionErrorMessage != "Transfer was cancelled.") + #expect(!metadata.sessionErrorMessage.isEmpty) + } +} diff --git a/Widget/Dashboard/DashboardData.swift b/Widget/Dashboard/DashboardData.swift index e76deef9d4..0a02f54f00 100644 --- a/Widget/Dashboard/DashboardData.swift +++ b/Widget/Dashboard/DashboardData.swift @@ -205,7 +205,7 @@ func getDashboardDataEntry(configuration: DashboardIntent?, isPreview: Bool, dis let footerText = "Dashboard " + NSLocalizedString("_of_", comment: "") + " " + activeTableAccount.displayName + alias if resultsDashboardWidget.error != .success { - return(DashboardDataEntry(date: Date(), datas: datasPlaceholder, dashboard: tableDashboard, buttons: buttons, isPlaceholder: true, isEmpty: false, titleImage: titleImage, title: title, footerImage: "xmark.icloud", footerText: resultsDashboardWidget.error.errorDescription, account: activeTableAccount.account)) + return(DashboardDataEntry(date: Date(), datas: datasPlaceholder, dashboard: tableDashboard, buttons: buttons, isPlaceholder: true, isEmpty: false, titleImage: titleImage, title: title, footerImage: "xmark.icloud", footerText: resultsDashboardWidget.error.localizedGenericMessage, account: activeTableAccount.account)) } else { return(DashboardDataEntry(date: Date(), datas: datas, dashboard: tableDashboard, buttons: buttons, isPlaceholder: false, isEmpty: datas.isEmpty, titleImage: titleImage, title: title, footerImage: "checkmark.icloud", footerText: footerText, account: activeTableAccount.account)) } diff --git a/Widget/Files/FilesData.swift b/Widget/Files/FilesData.swift index ce54bde908..654b22d22f 100644 --- a/Widget/Files/FilesData.swift +++ b/Widget/Files/FilesData.swift @@ -238,7 +238,7 @@ func getFilesDataEntry(configuration: AccountIntent?, isPreview: Bool, displaySi let footerText = "Files " + NSLocalizedString("_of_", comment: "") + " " + activeTableAccount.displayName + alias if results.error != .success { - return(FilesDataEntry(date: Date(), datas: datasPlaceholder, isPlaceholder: true, isEmpty: false, userId: activeTableAccount.userId, url: activeTableAccount.urlBase, account: activeTableAccount.account, tile: title, footerImage: "xmark.icloud", footerText: results.error.errorDescription)) + return(FilesDataEntry(date: Date(), datas: datasPlaceholder, isPlaceholder: true, isEmpty: false, userId: activeTableAccount.userId, url: activeTableAccount.urlBase, account: activeTableAccount.account, tile: title, footerImage: "xmark.icloud", footerText: results.error.localizedGenericMessage)) } else { return(FilesDataEntry(date: Date(), datas: datas, isPlaceholder: false, isEmpty: datas.isEmpty, userId: activeTableAccount.userId, url: activeTableAccount.urlBase, account: activeTableAccount.account, tile: title, footerImage: "checkmark.icloud", footerText: footerText)) } diff --git a/iOSClient/Account/NCAccount.swift b/iOSClient/Account/NCAccount.swift index ff80eb3315..ceb2a3f63b 100644 --- a/iOSClient/Account/NCAccount.swift +++ b/iOSClient/Account/NCAccount.swift @@ -38,7 +38,12 @@ class NCAccount: NSObject { guard resultsGetUserProfile.error == .success, let userProfile = resultsGetUserProfile.userProfile else { NextcloudKit.shared.nkCommonInstance.nksessions.remove(account: account) - let alertController = UIAlertController(title: NSLocalizedString("_error_", comment: ""), message: resultsGetUserProfile.error.errorDescription, preferredStyle: .alert) + + let error = resultsGetUserProfile.error == .success ? NKError.invalidData : resultsGetUserProfile.error + + nkLog(error: "Getting the user profile failed: \(error.errorCode) \(error.errorDescription)") + + let alertController = UIAlertController(title: NSLocalizedString("_error_", comment: ""), message: error.userFriendlyMessage(for: .login) ?? error.localizedGenericMessage, preferredStyle: .alert) alertController.addAction(UIAlertAction(title: NSLocalizedString("_ok_", comment: ""), style: .default, handler: { _ in })) viewController.present(alertController, animated: true) diff --git a/iOSClient/Activity/NCActivity.swift b/iOSClient/Activity/NCActivity.swift index 9290195d07..dfb5aa76b1 100644 --- a/iOSClient/Activity/NCActivity.swift +++ b/iOSClient/Activity/NCActivity.swift @@ -86,8 +86,7 @@ class NCActivity: UIViewController, NCSharePagingContent { } else { Task { await showErrorBanner(windowScene: self.windowScene, - text: error.errorDescription, - errorCode: error.errorCode) + error: error) } } } @@ -447,8 +446,7 @@ extension NCActivity { } else if error.errorCode != NCGlobal.shared.errorResourceNotFound { Task { await showErrorBanner(windowScene: self.windowScene, - text: error.errorDescription, - errorCode: error.errorCode) + error: error) } } diff --git a/iOSClient/Albums/Presentation/Details/AlbumDetailsViewModel.swift b/iOSClient/Albums/Presentation/Details/AlbumDetailsViewModel.swift index 0fb87c90b9..06a848a851 100644 --- a/iOSClient/Albums/Presentation/Details/AlbumDetailsViewModel.swift +++ b/iOSClient/Albums/Presentation/Details/AlbumDetailsViewModel.swift @@ -170,8 +170,7 @@ class AlbumDetailsViewModel: ObservableObject { AlbumsManager.shared.syncAlbums(for: account) self?.navigator.pop() case .failure(let error): - // User-initiated failures must not be hidden by the network-error cooldown. - await showErrorBanner(windowScene: self?.windowScene, text: error.errorDescription) + await showErrorBanner(windowScene: self?.windowScene, error: error, context: .delete(fileName: deletedAlbum.name)) } } } @@ -185,7 +184,7 @@ class AlbumDetailsViewModel: ObservableObject { let error = await deletePhotoFromAlbum(photo, metadata: photo.metadata) guard error == .success else { - await showErrorBanner(windowScene: windowScene, text: error.errorDescription) + await showErrorBanner(windowScene: windowScene, error: error) return } @@ -207,7 +206,7 @@ class AlbumDetailsViewModel: ObservableObject { guard let photo = photos.first(where: { $0.metadata.fileId == metadata.fileId }) else { continue } let error = await deletePhotoFromAlbum(photo, metadata: metadata) guard error == .success else { - await showErrorBanner(windowScene: windowScene, text: error.errorDescription) + await showErrorBanner(windowScene: windowScene, error: error) return } photos.removeAll { $0.id == photo.id } @@ -272,8 +271,7 @@ class AlbumDetailsViewModel: ObservableObject { } catch { self.isLoadingPopupVisible = false let error = (error as? NKError) ?? NKError(error: error) - // Report every failed rename, even after another offline operation. - await showErrorBanner(windowScene: self.windowScene, text: error.errorDescription) + await showErrorBanner(windowScene: self.windowScene, error: error) } } } @@ -296,10 +294,7 @@ class AlbumDetailsViewModel: ObservableObject { for photo in selectedPhotos { guard let metadata = NCManageDatabase.shared.getMetadataFromOcId(photo) else { Task { - await showErrorBanner( - windowScene: self.windowScene, - text: NKError.invalidData.errorDescription - ) + await showErrorBanner(windowScene: self.windowScene, text: "_media_not_available_") } continue } @@ -317,7 +312,7 @@ class AlbumDetailsViewModel: ObservableObject { case .success: hadAnySuccess = true case .failure(let error): - await showErrorBanner(windowScene: self?.windowScene, text: error.errorDescription) + await showErrorBanner(windowScene: self?.windowScene, error: error) } } } diff --git a/iOSClient/Albums/Presentation/List/AlbumsListViewModel.swift b/iOSClient/Albums/Presentation/List/AlbumsListViewModel.swift index 34bef5ef45..564eea3f85 100644 --- a/iOSClient/Albums/Presentation/List/AlbumsListViewModel.swift +++ b/iOSClient/Albums/Presentation/List/AlbumsListViewModel.swift @@ -160,7 +160,7 @@ class AlbumsListViewModel: ObservableObject { } case .failure(let error): Task { - await showErrorBanner(windowScene: self?.windowScene, text: error.errorDescription) + await showErrorBanner(windowScene: self?.windowScene, error: error) } } } @@ -187,10 +187,7 @@ class AlbumsListViewModel: ObservableObject { for photo in selectedPhotos { guard let metadata = NCManageDatabase.shared.getMetadataFromOcId(photo) else { Task { - await showErrorBanner( - windowScene: self.windowScene, - text: NKError.invalidData.errorDescription - ) + await showErrorBanner(windowScene: self.windowScene, text: "_media_not_available_") } continue } @@ -202,7 +199,7 @@ class AlbumsListViewModel: ObservableObject { hadAnySuccess = true case .failure(let error): Task { - await showErrorBanner(windowScene: self.windowScene, text: error.errorDescription) + await showErrorBanner(windowScene: self.windowScene, error: error) } } group.leave() diff --git a/iOSClient/Data/NCManageDatabase+Metadata.swift b/iOSClient/Data/NCManageDatabase+Metadata.swift index 25aa8f8659..44fd79acb7 100644 --- a/iOSClient/Data/NCManageDatabase+Metadata.swift +++ b/iOSClient/Data/NCManageDatabase+Metadata.swift @@ -378,6 +378,18 @@ extension tableMetadata { } } +#if !EXTENSION || EXTENSION_SHARE +extension tableMetadata { + /// The saved `sessionError`, worded for people from its `errorCode` and the operation the item failed at. + var sessionErrorMessage: String { + let error = NKError(errorCode: errorCode, errorDescription: sessionError) + let context = NCErrorContext(metadataStatus: status, fileName: fileNameView) + + return error.userFriendlyMessage(for: context) ?? error.localizedGenericMessage + } +} +#endif + extension NCManageDatabase { #if !EXTENSION func isMetadataShareOrMounted(metadata: tableMetadata, metadataFolder: tableMetadata?) -> Bool { diff --git a/iOSClient/Extensions/UIAlertController+Extension.swift b/iOSClient/Extensions/UIAlertController+Extension.swift index 6b8a628966..06fecc4529 100644 --- a/iOSClient/Extensions/UIAlertController+Extension.swift +++ b/iOSClient/Extensions/UIAlertController+Extension.swift @@ -11,15 +11,14 @@ extension UIAlertController { /// Creates a alert controller with a textfield, asking to create a new folder /// - Parameters: /// - serverUrl: Server url of the location where the folder should be created - /// - urlBase: UrlBase object - /// - completion: If not` nil` it overrides the default behavior which shows an error + /// - completion: Receives the result and the folder name. /// - Returns: The presentable alert controller static func createFolderWith(serverUrl: String, session: NCSession.Session, markE2ee: Bool = false, sceneIdentifier: String? = nil, capabilities: NKCapabilities.Capabilities, - completion: ((_ error: NKError) -> Void)? = nil) -> UIAlertController { + completion: ((_ error: NKError, _ folderName: String) -> Void)? = nil) -> UIAlertController { let alertController = UIAlertController(title: NSLocalizedString("_create_folder_", comment: ""), message: nil, preferredStyle: .alert) let isDirectoryEncrypted = NCUtilityFileSystem().isDirectoryE2EE(serverUrl: serverUrl, urlBase: session.urlBase, userId: session.userId, account: session.account) @@ -28,7 +27,7 @@ extension UIAlertController { if markE2ee { if NCNetworking.shared.isOffline { - completion?(NKError(errorCode: NCGlobal.shared.errorOfflineNotAllowed, errorDescription: "_offline_not_allowed_")) + completion?(NKError(errorCode: NCGlobal.shared.errorOfflineNotAllowed, errorDescription: "_offline_not_allowed_"), fileNameFolder) return } Task { @@ -51,30 +50,30 @@ extension UIAlertController { banner.dismiss() } } - completion?(error) + completion?(error, fileNameFolder) } else { if let banner { banner.dismiss() } - completion?(NKError(errorCode: createFolderResults.error.errorCode, errorDescription: createFolderResults.error.errorDescription)) + completion?(createFolderResults.error, fileNameFolder) } } } else if isDirectoryEncrypted { Task { if NCNetworking.shared.isOffline { - completion?(NKError(errorCode: NCGlobal.shared.errorOfflineNotAllowed, errorDescription: "_offline_not_allowed_")) + completion?(NKError(errorCode: NCGlobal.shared.errorOfflineNotAllowed, errorDescription: "_offline_not_allowed_"), fileNameFolder) return } let error = await NCNetworkingE2EECreateFolder().createFolder(fileName: fileNameFolder, serverUrl: serverUrl, sceneIdentifier: sceneIdentifier, session: session) - completion?(error) + completion?(error, fileNameFolder) } } else { #if EXTENSION Task { let error = await NCNetworking.shared.createFolder(fileName: fileNameFolder, serverUrl: serverUrl, overwrite: false, session: session) - completion?(error) + completion?(error, fileNameFolder) } #else var metadata = tableMetadata() diff --git a/iOSClient/Files/NCFiles.swift b/iOSClient/Files/NCFiles.swift index de16570e39..e47ef575c4 100644 --- a/iOSClient/Files/NCFiles.swift +++ b/iOSClient/Files/NCFiles.swift @@ -317,7 +317,7 @@ class NCFiles: NCCollectionViewCommon { // Client Diagnostic await self.database.addDiagnosticAsync(account: account, issue: NCGlobal.shared.diagnosticIssueE2eeErrors) - await showErrorBanner(windowScene: windowScene, text: error.errorDescription, errorCode: error.errorCode) + await showErrorBanner(windowScene: windowScene, error: error) } return (metadatas, error, reloadRequired) @@ -339,9 +339,9 @@ class NCFiles: NCCollectionViewCommon { // Continue: the previous key was archived and can still offer // read-only access to storage spaces encrypted with it. } catch let error as NKError { - await showErrorBanner(windowScene: windowScene, text: error.errorDescription) + await showErrorBanner(windowScene: windowScene, error: error) } catch { - await showErrorBanner(windowScene: windowScene, text: error.localizedDescription) + await showErrorBanner(windowScene: windowScene, error: NKError(error: error)) } } @@ -363,9 +363,11 @@ class NCFiles: NCCollectionViewCommon { return storedAccess.writeAccessError } } catch { + nkLog(tag: global.logTagE2EE, emoji: .error, message: "Unable to read the stored encryption keys: \(error)") + return NKError( errorCode: global.errorInternalError, - errorDescription: error.localizedDescription + errorDescription: NSLocalizedString("_e2ee_no_metadataKey_found_", comment: "") ) } @@ -380,7 +382,7 @@ class NCFiles: NCCollectionViewCommon { guard result.error == .success, let e2eMetadata = result.e2eMetadata, let version = result.version else { - nkLog(tag: self.global.logTagE2EE, message: returnError.errorDescription) + nkLog(tag: self.global.logTagE2EE, message: result.error.errorDescription) return result.error } diff --git a/iOSClient/GUI/Lucid Banner/ErrorBannerView.swift b/iOSClient/GUI/Lucid Banner/ErrorBannerView.swift index 3af5768d52..a77d1f778a 100644 --- a/iOSClient/GUI/Lucid Banner/ErrorBannerView.swift +++ b/iOSClient/GUI/Lucid Banner/ErrorBannerView.swift @@ -5,14 +5,18 @@ import SwiftUI import LucidBanner import NextcloudKit -import Alamofire @MainActor func showErrorBanner(windowScene: UIWindowScene?, - error: NKError) async { + error: NKError, + context: NCErrorContext = .generic) async { + guard let text = error.userFriendlyMessage(for: context) else { return } + + nkLog(error: "Error shown for \(context): \(error.errorCode) \(error.errorDescription)") + await showErrorBanner(windowScene: windowScene, - title: "_error_", - text: error.errorDescription, + title: context.failureTitle ?? "_error_", + text: text, errorCode: error.errorCode) } @@ -21,8 +25,7 @@ func showErrorBanner(windowScene: UIWindowScene?, title: String = "_error_", text: String, footnote: String? = nil, - errorCode: Int? = nil, - afError: AFError? = nil) async { + errorCode: Int? = nil) async { guard let windowScene, let window = windowScene.resolvedWindow else { return diff --git a/iOSClient/Login/NCLogin.swift b/iOSClient/Login/NCLogin.swift index 79d877644f..911c2e9535 100644 --- a/iOSClient/Login/NCLogin.swift +++ b/iOSClient/Login/NCLogin.swift @@ -368,6 +368,17 @@ class NCLogin: UIViewController, UITextFieldDelegate, NCLoginQRCodeDelegate { loginProvider.startPolling(loginFlowV2Token: token, loginFlowV2Endpoint: endpoint, loginFlowV2Login: login) loginProvider.startAuthentication() self.activeLoginProvider = loginProvider + } else { + let loginFlowError = error == .success ? NKError.invalidData : error + + nkLog(error: "Login flow v2 failed: \(loginFlowError.errorCode) \(loginFlowError.errorDescription)") + + loginButton.hideSpinnerAndShowButton() + loginButton.isEnabled = true + + let alertController = UIAlertController(title: NSLocalizedString("_connection_error_", comment: ""), message: loginFlowError.localizedGenericMessage, preferredStyle: .alert) + alertController.addAction(UIAlertAction(title: NSLocalizedString("_ok_", comment: ""), style: .default, handler: { _ in })) + self.present(alertController, animated: true, completion: { }) } } case .failure(let error): @@ -413,7 +424,7 @@ class NCLogin: UIViewController, UITextFieldDelegate, NCLoginQRCodeDelegate { self.present(alertController, animated: true) #endif } else { - let alertController = UIAlertController(title: NSLocalizedString("_connection_error_", comment: ""), message: error.errorDescription, preferredStyle: .alert) + let alertController = UIAlertController(title: NSLocalizedString("_connection_error_", comment: ""), message: error.localizedGenericMessage, preferredStyle: .alert) alertController.addAction(UIAlertAction(title: NSLocalizedString("_ok_", comment: ""), style: .default, handler: { _ in })) self.present(alertController, animated: true, completion: { }) } @@ -460,7 +471,10 @@ class NCLogin: UIViewController, UITextFieldDelegate, NCLoginQRCodeDelegate { await createAccount(urlBase: server, user: user, password: token) } else { let windowScene = SceneManager.shared.getWindowScene(controller: self.controller) - await showErrorBanner(windowScene: windowScene, text: results.error.errorDescription, errorCode: results.error.errorCode) + + let error = results.error == .success ? NKError.invalidData : results.error + + await showErrorBanner(windowScene: windowScene, error: error, context: .login) dismiss(animated: true, completion: nil) } } else if value.hasPrefix(protocolLogin) { @@ -476,7 +490,10 @@ class NCLogin: UIViewController, UITextFieldDelegate, NCLoginQRCodeDelegate { await self.createAccount(urlBase: urlBase, user: user, password: password) } else { let windowScene = SceneManager.shared.getWindowScene(controller: self.controller) - await showErrorBanner(windowScene: windowScene, text: results.error.errorDescription, errorCode: results.error.errorCode) + + let error = results.error == .success ? NKError.invalidData : results.error + + await showErrorBanner(windowScene: windowScene, error: error, context: .login) dismiss(animated: true, completion: nil) } } diff --git a/iOSClient/Main/Collection Common/Cell/NCGridCell.swift b/iOSClient/Main/Collection Common/Cell/NCGridCell.swift index 030829e01b..7213f941ec 100644 --- a/iOSClient/Main/Collection Common/Cell/NCGridCell.swift +++ b/iOSClient/Main/Collection Common/Cell/NCGridCell.swift @@ -265,7 +265,7 @@ extension NCCollectionViewCommon { if !metadata.sessionError.isEmpty, metadata.status != global.metadataStatusNormal { cell.labelSubinfo.isHidden = false - cell.labelInfo.text = metadata.sessionError + cell.labelInfo.text = metadata.sessionErrorMessage } else { cell.labelSubinfo.isHidden = false cell.writeInfoDateSize(date: metadata.date, size: metadata.size) diff --git a/iOSClient/Main/Collection Common/Cell/NCListCell.swift b/iOSClient/Main/Collection Common/Cell/NCListCell.swift index 867c469eee..1e805421be 100755 --- a/iOSClient/Main/Collection Common/Cell/NCListCell.swift +++ b/iOSClient/Main/Collection Common/Cell/NCListCell.swift @@ -462,7 +462,7 @@ extension NCCollectionViewCommon { cell.labelSubinfo?.isHidden = true } else if !metadata.sessionError.isEmpty, metadata.status != global.metadataStatusNormal { cell.labelSubinfo?.isHidden = false - cell.labelInfo?.text = metadata.sessionError + cell.labelInfo?.text = metadata.sessionErrorMessage } else { cell.labelSubinfo?.isHidden = false cell.writeInfoDateSize(date: metadata.date, size: metadata.size) diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegate.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegate.swift index 8ceef5a33a..2534859df3 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegate.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegate.swift @@ -23,17 +23,10 @@ extension NCCollectionViewCommon: UICollectionViewDelegate { if error.errorCode == NSUserCancelledError { return } - await showErrorBanner( - windowScene: windowScene, - text: error.errorDescription - ) + await showErrorBanner(windowScene: windowScene, error: error) return } catch { - // fallback (non NKError) - await showErrorBanner( - windowScene: windowScene, - text: error.localizedDescription - ) + await showErrorBanner(windowScene: windowScene, error: NKError(error: error)) return } } @@ -81,7 +74,7 @@ extension NCCollectionViewCommon: UICollectionViewDelegate { if results.nkError == .success || results.nkError == .cancelled { print("ok") } else { - await showErrorBanner(windowScene: windowScene, text: results.nkError.errorDescription, errorCode: results.nkError.errorCode) + await showErrorBanner(windowScene: windowScene, error: results.nkError, context: .download(fileName: metadata.fileNameView)) } } diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+Search.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+Search.swift index 1815800cf5..bcc08c2ecb 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+Search.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+Search.swift @@ -4,7 +4,6 @@ import Foundation import NextcloudKit -import Alamofire extension NCCollectionViewCommon { @MainActor @@ -70,8 +69,7 @@ extension NCCollectionViewCommon { ) } else { await showErrorBanner(windowScene: windowScene, - text: results.error.errorDescription, - errorCode: results.error.errorCode) + error: results.error) } collectionView.reloadData() @@ -112,9 +110,7 @@ extension NCCollectionViewCommon { if results.error != .success { await showErrorBanner(windowScene: windowScene, - text: results.error.errorDescription, - errorCode: results.error.errorCode, - afError: results.error.error as? AFError) + error: results.error) } guard isSearchingMode, @@ -145,9 +141,7 @@ extension NCCollectionViewCommon { if results.error != .success { await showErrorBanner(windowScene: windowScene, - text: results.error.errorDescription, - errorCode: results.error.errorCode, - afError: results.error.error as? AFError) + error: results.error) } guard isSearchingMode, @@ -204,9 +198,7 @@ extension NCCollectionViewCommon { if results.error != .success { await showErrorBanner(windowScene: windowScene, - text: results.error.errorDescription, - errorCode: results.error.errorCode, - afError: results.error.error as? AFError) + error: results.error) } guard isSearchingMode, diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+UIEditMenuInteractionDelegate.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+UIEditMenuInteractionDelegate.swift index 279de6f3a5..23018ef96a 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+UIEditMenuInteractionDelegate.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+UIEditMenuInteractionDelegate.swift @@ -133,8 +133,8 @@ extension NCCollectionViewCommon: UIEditMenuInteractionDelegate { } else { Task { await showErrorBanner(windowScene: windowScene, - text: resultsUpload.error.errorDescription, - errorCode: resultsUpload.error.errorCode) + error: resultsUpload.error, + context: .upload(fileName: fileName)) } } } diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index 156dfecf9d..4838a85b2f 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -872,7 +872,7 @@ extension NCCollectionViewCommon: NCTransferDelegate { Task { if error != .success, error.errorCode != global.errorResourceNotFound { - await showErrorBanner(windowScene: windowScene, text: error.errorDescription, errorCode: error.errorCode) + await showErrorBanner(windowScene: windowScene, error: error, context: NCErrorContext(networkingStatus: networkingStatus, fileName: fileName)) } guard session.account == account else { diff --git a/iOSClient/Main/Create/NCCreate.swift b/iOSClient/Main/Create/NCCreate.swift index 5217caab5f..19225012ec 100644 --- a/iOSClient/Main/Create/NCCreate.swift +++ b/iOSClient/Main/Create/NCCreate.swift @@ -62,7 +62,7 @@ class NCCreate: NSObject { let editorURL = results.url, !editorURL.isEmpty else { let error: NKError = results.error == .success ? .invalidData : results.error - await showErrorBanner(windowScene: windowScene, text: error.errorDescription, errorCode: error.errorCode) + await showErrorBanner(windowScene: windowScene, error: error, context: .createDocument(fileName: fileName)) return } @@ -99,7 +99,7 @@ class NCCreate: NSObject { let editorURL = results.url, !editorURL.isEmpty else { let error: NKError = results.error == .success ? .invalidData : results.error - await showErrorBanner(windowScene: windowScene, text: error.errorDescription, errorCode: error.errorCode) + await showErrorBanner(windowScene: windowScene, error: error, context: .createDocument(fileName: fileName)) return } @@ -170,10 +170,10 @@ class NCCreate: NSObject { ) guard results.error == .success, let metadata = results.metadata else { let windowScene = SceneManager.shared.getWindowScene(controller: controller) + let error: NKError = results.error == .success ? .invalidData : results.error await showErrorBanner( windowScene: windowScene, - text: results.error.errorDescription, - errorCode: results.error.errorCode + error: error ) return } diff --git a/iOSClient/Main/NCDragDrop.swift b/iOSClient/Main/NCDragDrop.swift index 39f53ad598..5e35094b87 100644 --- a/iOSClient/Main/NCDragDrop.swift +++ b/iOSClient/Main/NCDragDrop.swift @@ -244,8 +244,8 @@ class NCDragDrop: NSObject { } guard results.nkError == .success else { await showErrorBanner(windowScene: windowScene, - text: results.nkError.errorDescription, - errorCode: results.nkError.errorCode) + error: results.nkError, + context: .download(fileName: metadata.fileNameView)) break } } @@ -275,7 +275,8 @@ class NCDragDrop: NSObject { } guard results.error == .success else { await showErrorBanner(windowScene: windowScene, - text: results.error.errorDescription, errorCode: results.error.errorCode) + error: results.error, + context: .upload(fileName: metadata.fileNameView)) break } diff --git a/iOSClient/Menu/NCContextMenuComment.swift b/iOSClient/Menu/NCContextMenuComment.swift index 327fb318aa..568da4f435 100644 --- a/iOSClient/Menu/NCContextMenuComment.swift +++ b/iOSClient/Menu/NCContextMenuComment.swift @@ -59,7 +59,7 @@ class NCContextMenuComment: NSObject { NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterReloadDataNCShare) } else { Task { @MainActor in - await showErrorBanner(windowScene: self.windowScene, text: error.errorDescription, errorCode: error.errorCode) + await showErrorBanner(windowScene: self.windowScene, error: error) } } } @@ -85,7 +85,7 @@ class NCContextMenuComment: NSObject { (self.viewController as? NCActivity)?.loadComments() } else { Task { @MainActor in - await showErrorBanner(windowScene: self.windowScene, text: error.errorDescription, errorCode: error.errorCode) + await showErrorBanner(windowScene: self.windowScene, error: error) } } } diff --git a/iOSClient/Menu/NCContextMenuMain.swift b/iOSClient/Menu/NCContextMenuMain.swift index 30b9617ce8..978180124e 100644 --- a/iOSClient/Menu/NCContextMenuMain.swift +++ b/iOSClient/Menu/NCContextMenuMain.swift @@ -229,7 +229,7 @@ class NCContextMenuMain: NSObject { sceneIdentifier: self.sceneIdentifier ) if error != .success { - await showErrorBanner(windowScene: self.windowScene, text: error.errorDescription, errorCode: error.errorCode) + await showErrorBanner(windowScene: self.windowScene, error: error) } } } @@ -246,11 +246,7 @@ class NCContextMenuMain: NSObject { account: metadata.account ) guard accessError == .success else { - await showErrorBanner( - windowScene: self.windowScene, - text: accessError.errorDescription, - errorCode: accessError.errorCode - ) + await showErrorBanner(windowScene: self.windowScene, error: accessError) return } @@ -267,9 +263,7 @@ class NCContextMenuMain: NSObject { await NCManageDatabase.shared.setMetadataEncryptedAsync(ocId: metadata.ocId, encrypted: false) await (self.viewController as? NCCollectionViewCommon)?.reloadDataSource() } else { - await showErrorBanner(windowScene: self.windowScene, - text: results.error.errorDescription, - errorCode: results.error.errorCode) + await showErrorBanner(windowScene: self.windowScene, error: results.error) } } } @@ -390,7 +384,9 @@ class NCContextMenuMain: NSObject { if error == .success { completeHudBannerSuccess(token: results.token, banner: results.banner) } else { - completeHudBannerError(description: error.errorDescription, token: results.token, banner: results.banner) + let message = error.userFriendlyMessage(for: .delete(fileName: metadata.fileNameView)) ?? error.localizedGenericMessage + + completeHudBannerError(description: message, token: results.token, banner: results.banner) } await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in @@ -499,9 +495,7 @@ class NCContextMenuMain: NSObject { params: item.params ) if results.error != .success { - await showErrorBanner(windowScene: self.windowScene, - text: results.error.errorDescription, - errorCode: results.error.errorCode) + await showErrorBanner(windowScene: self.windowScene, error: results.error) } else { if let tooltip = results.uiResponse?.ocs.data.tooltip { await showInfoBanner(windowScene: self.windowScene, text: tooltip) diff --git a/iOSClient/Menu/NCContextMenuPlus.swift b/iOSClient/Menu/NCContextMenuPlus.swift index c3179c85c9..717ba9c6c8 100644 --- a/iOSClient/Menu/NCContextMenuPlus.swift +++ b/iOSClient/Menu/NCContextMenuPlus.swift @@ -138,12 +138,12 @@ class NCContextMenuPlus: NSObject { serverUrl: serverUrl, session: session, sceneIdentifier: controller.sceneIdentifier, - capabilities: capabilities) { error in + capabilities: capabilities) { error, folderName in if error != .success { Task { await showErrorBanner(windowScene: self.windowScene, - text: error.errorDescription, - errorCode: error.errorCode) + error: error, + context: .createFolder(folderName: folderName)) } } } @@ -164,12 +164,10 @@ class NCContextMenuPlus: NSObject { session: session, markE2ee: true, sceneIdentifier: controller.sceneIdentifier, - capabilities: capabilities) { error in + capabilities: capabilities) { error, _ in if error != .success { Task { - await showErrorBanner(windowScene: self.windowScene, - text: error.errorDescription, - errorCode: error.errorCode) + await showErrorBanner(windowScene: self.windowScene, error: error) } } } @@ -628,8 +626,7 @@ class NCContextMenuPlus: NSObject { let selectedTemplate = result.selectedTemplate else { await showErrorBanner( windowScene: windowScene, - text: result.error.errorDescription, - errorCode: result.error.errorCode + error: result.error ) return } diff --git a/iOSClient/Networking/E2EE/NCEndToEndMetadata.swift b/iOSClient/Networking/E2EE/NCEndToEndMetadata.swift index d5d9955674..acf18e32ac 100644 --- a/iOSClient/Networking/E2EE/NCEndToEndMetadata.swift +++ b/iOSClient/Networking/E2EE/NCEndToEndMetadata.swift @@ -26,7 +26,9 @@ class NCEndToEndMetadata: NSObject { } else if capabilities.e2EEApiVersion.hasPrefix("2.") { return await encodeMetadataV2(serverUrl: serverUrl, ocIdServerUrl: directory.ocId, addUserId: addUserId, addCertificate: addCertificate, removeUserId: removeUserId, session: session) } else { - return (nil, nil, 0, NKError(errorCode: NCGlobal.shared.errorE2EEVersion, errorDescription: "Server E2EE version " + capabilities.e2EEApiVersion + ", not compatible")) + nkLog(tag: NCGlobal.shared.logTagE2EE, emoji: .error, message: "Server E2EE version \(capabilities.e2EEApiVersion) is not supported.") + + return (nil, nil, 0, NKError(errorCode: NCGlobal.shared.errorE2EEVersion, errorDescription: NSLocalizedString("_e2ee_version_not_supported_", comment: ""))) } } @@ -41,8 +43,10 @@ class NCEndToEndMetadata: NSObject { session: NCSession.Session ) async -> (error: NKError, access: NCEndToEndKeySetAccess) { guard let data = metadata.data(using: .utf8), let directory = self.database.getTableDirectory(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@", session.account, serverUrl)) else { + nkLog(tag: NCGlobal.shared.logTagE2EE, emoji: .error, message: "Unable to decode the metadata file: invalid data or missing directory.") + return ( - NKError(errorCode: NCGlobal.shared.errorE2EEJSon, errorDescription: "Unable to decode the metadata file"), + NKError(errorCode: NCGlobal.shared.errorE2EEJSon, errorDescription: NSLocalizedString("_e2ee_decode_metadata_", comment: "")), .unavailable ) } @@ -53,10 +57,12 @@ class NCEndToEndMetadata: NSObject { let isMetadataV2 = metadataV2 != nil guard isMetadataV1 || isMetadataV12 || isMetadataV2 else { + nkLog(tag: NCGlobal.shared.logTagE2EE, emoji: .error, message: "Unable to decode the metadata file: unknown metadata format.") + return ( NKError( errorCode: NCGlobal.shared.errorE2EEVersion, - errorDescription: "Unable to decode the metadata file" + errorDescription: NSLocalizedString("_e2ee_decode_metadata_", comment: "") ), .unavailable ) @@ -68,10 +74,12 @@ class NCEndToEndMetadata: NSObject { } catch { nkLog( tag: NCGlobal.shared.logTagE2EE, - message: "Unable to resolve E2EE key-set access." + emoji: .error, + message: "Unable to resolve E2EE key-set access: \(error)" ) + return ( - NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: error.localizedDescription), + NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: NSLocalizedString("_e2ee_no_metadataKey_found_", comment: "")), .unavailable ) } diff --git a/iOSClient/Networking/E2EE/NCEndToEndMetadataV1.swift b/iOSClient/Networking/E2EE/NCEndToEndMetadataV1.swift index fda556f48a..75dc3b0c73 100644 --- a/iOSClient/Networking/E2EE/NCEndToEndMetadataV1.swift +++ b/iOSClient/Networking/E2EE/NCEndToEndMetadataV1.swift @@ -117,7 +117,9 @@ extension NCEndToEndMetadata { } fileNameIdentifiers.append(e2eEncryption.fileNameIdentifier) } catch let error { - return (nil, nil, 0, NKError(errorCode: NCGlobal.shared.errorE2EEJSon, errorDescription: error.localizedDescription)) + nkLog(tag: NCGlobal.shared.logTagE2EE, emoji: .error, message: "Unable to encode the E2EE metadata: \(error)") + + return (nil, nil, 0, NKError(errorCode: NCGlobal.shared.errorE2EEJSon, errorDescription: NSLocalizedString("_e2ee_encode_metadata_", comment: ""))) } } @@ -144,7 +146,9 @@ extension NCEndToEndMetadata { filedrop.updateValue(record, forKey: e2eEncryption.fileNameIdentifier) } } catch let error { - return (nil, nil, 0, NKError(errorCode: NCGlobal.shared.errorE2EEJSon, errorDescription: error.localizedDescription)) + nkLog(tag: NCGlobal.shared.logTagE2EE, emoji: .error, message: "Unable to encode the E2EE metadata: \(error)") + + return (nil, nil, 0, NKError(errorCode: NCGlobal.shared.errorE2EEJSon, errorDescription: NSLocalizedString("_e2ee_encode_metadata_", comment: ""))) } } } @@ -168,7 +172,9 @@ extension NCEndToEndMetadata { } return (jsonString, nil, 0, NKError()) } catch let error { - return (nil, nil, 0, NKError(errorCode: NCGlobal.shared.errorE2EEJSon, errorDescription: error.localizedDescription)) + nkLog(tag: NCGlobal.shared.logTagE2EE, emoji: .error, message: "Unable to encode the E2EE metadata: \(error)") + + return (nil, nil, 0, NKError(errorCode: NCGlobal.shared.errorE2EEJSon, errorDescription: NSLocalizedString("_e2ee_encode_metadata_", comment: ""))) } } @@ -270,7 +276,9 @@ extension NCEndToEndMetadata { } } catch let error { - return NKError(errorCode: NCGlobal.shared.errorE2EEJSon, errorDescription: error.localizedDescription) + nkLog(tag: NCGlobal.shared.logTagE2EE, emoji: .error, message: "Unable to decode the E2EE metadata: \(error)") + + return NKError(errorCode: NCGlobal.shared.errorE2EEJSon, errorDescription: NSLocalizedString("_e2ee_decode_metadata_", comment: "")) } } } @@ -329,7 +337,9 @@ extension NCEndToEndMetadata { } } catch let error { - return NKError(errorCode: NCGlobal.shared.errorE2EEJSon, errorDescription: error.localizedDescription) + nkLog(tag: NCGlobal.shared.logTagE2EE, emoji: .error, message: "Unable to decode the E2EE metadata: \(error)") + + return NKError(errorCode: NCGlobal.shared.errorE2EEJSon, errorDescription: NSLocalizedString("_e2ee_decode_metadata_", comment: "")) } } } @@ -344,7 +354,9 @@ extension NCEndToEndMetadata { errorDescription: NSLocalizedString("_e2ee_no_match_checksum_", comment: "")) } } catch let error { - return NKError(errorCode: NCGlobal.shared.errorE2EEJSon, errorDescription: error.localizedDescription) + nkLog(tag: NCGlobal.shared.logTagE2EE, emoji: .error, message: "Unable to decode the E2EE metadata: \(error)") + + return NKError(errorCode: NCGlobal.shared.errorE2EEJSon, errorDescription: NSLocalizedString("_e2ee_decode_metadata_", comment: "")) } return NKError() @@ -400,7 +412,9 @@ extension NCEndToEndMetadata { // if let tableE2eMetadata = await self.database.getE2eMetadataAsync(account: session.account, serverUrl: serverUrl) { if tableE2eMetadata.version > metadataVersion { - return NKError(errorCode: NCGlobal.shared.errorE2EEVersion, errorDescription: "Version \(tableE2eMetadata.version)") + nkLog(tag: NCGlobal.shared.logTagE2EE, emoji: .error, message: "E2EE metadata version \(metadataVersion) is older than the stored version \(tableE2eMetadata.version).") + + return NKError(errorCode: NCGlobal.shared.errorE2EEVersion, errorDescription: NSLocalizedString("_e2ee_version_not_supported_", comment: "")) } } @@ -456,13 +470,17 @@ extension NCEndToEndMetadata { } } catch let error { - return NKError(errorCode: NCGlobal.shared.errorE2EEJSon, errorDescription: error.localizedDescription) + nkLog(tag: NCGlobal.shared.logTagE2EE, emoji: .error, message: "Unable to decode the E2EE metadata: \(error)") + + return NKError(errorCode: NCGlobal.shared.errorE2EEJSon, errorDescription: NSLocalizedString("_e2ee_decode_metadata_", comment: "")) } } } } } catch let error { - return NKError(errorCode: NCGlobal.shared.errorE2EEJSon, errorDescription: error.localizedDescription) + nkLog(tag: NCGlobal.shared.logTagE2EE, emoji: .error, message: "Unable to decode the E2EE metadata: \(error)") + + return NKError(errorCode: NCGlobal.shared.errorE2EEJSon, errorDescription: NSLocalizedString("_e2ee_decode_metadata_", comment: "")) } return NKError() diff --git a/iOSClient/Networking/E2EE/NCEndToEndMetadataV2.swift b/iOSClient/Networking/E2EE/NCEndToEndMetadataV2.swift index 0e5a9710b6..31fedd54da 100644 --- a/iOSClient/Networking/E2EE/NCEndToEndMetadataV2.swift +++ b/iOSClient/Networking/E2EE/NCEndToEndMetadataV2.swift @@ -201,7 +201,9 @@ extension NCEndToEndMetadata { return (e2eeJson, signature, counter, NKError()) } catch let error { - return (nil, nil, counter, NKError(errorCode: NCGlobal.shared.errorE2EEJSon, errorDescription: error.localizedDescription)) + nkLog(tag: NCGlobal.shared.logTagE2EE, emoji: .error, message: "Unable to encode the E2EE metadata: \(error)") + + return (nil, nil, counter, NKError(errorCode: NCGlobal.shared.errorE2EEJSon, errorDescription: NSLocalizedString("_e2ee_encode_metadata_", comment: ""))) } } @@ -410,8 +412,10 @@ extension NCEndToEndMetadata { } } catch let error { + nkLog(tag: global.logTagE2EE, emoji: .error, message: "Unable to decode the E2EE metadata: \(error)") + return NKError(errorCode: NCGlobal.shared.errorE2EEJSon, - errorDescription: error.localizedDescription) + errorDescription: NSLocalizedString("_e2ee_decode_metadata_", comment: "")) } return returnError diff --git a/iOSClient/Networking/E2EE/NCNetworkingE2EE.swift b/iOSClient/Networking/E2EE/NCNetworkingE2EE.swift index 279b1cd398..c96f6ec8d0 100644 --- a/iOSClient/Networking/E2EE/NCNetworkingE2EE.swift +++ b/iOSClient/Networking/E2EE/NCNetworkingE2EE.swift @@ -214,8 +214,9 @@ class NCNetworkingE2EE: NSObject { nkLog(tag: NCGlobal.shared.logTagE2EE, message: "Archived the active key set and marked it as stale.") return serverKeyChangedError } catch { - nkLog(tag: NCGlobal.shared.logTagE2EE, message: "Unable to archive the active key set after server-key validation.") - return NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: error.localizedDescription) + nkLog(tag: NCGlobal.shared.logTagE2EE, emoji: .error, message: "Unable to archive the active key set after server-key validation: \(error)") + + return NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: NSLocalizedString("_e2ee_store_keys_", comment: "")) } } @@ -255,7 +256,9 @@ class NCNetworkingE2EE: NSObject { } } default: - return ("", "", nil, nil, nil, NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: "version e2ee not available")) + nkLog(tag: NCGlobal.shared.logTagE2EE, emoji: .error, message: "Server E2EE version \(capabilities.e2EEApiVersion) is not supported.") + + return ("", "", nil, nil, nil, NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: NSLocalizedString("_e2ee_version_not_supported_", comment: ""))) } } @@ -360,8 +363,10 @@ class NCNetworkingE2EE: NSObject { session: session ) } catch { + nkLog(tag: NCGlobal.shared.logTagE2EE, emoji: .error, message: "Unable to resolve the stored E2EE key set: \(error)") + await unlock(account: session.account, serverUrl: serverUrl) - return NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: error.localizedDescription) + return NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: NSLocalizedString("_e2ee_no_metadataKey_found_", comment: "")) } guard storedAccess.writeAccessError == .success else { @@ -447,7 +452,9 @@ class NCNetworkingE2EE: NSObject { session: session ) } catch { - return NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: error.localizedDescription) + nkLog(tag: NCGlobal.shared.logTagE2EE, emoji: .error, message: "Unable to resolve E2EE key-set access: \(error)") + + return NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: NSLocalizedString("_e2ee_no_metadataKey_found_", comment: "")) } let writeAccessError = access.writeAccessError @@ -505,7 +512,9 @@ class NCNetworkingE2EE: NSObject { return storedAccess.writeAccessError } } catch { - return NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: error.localizedDescription) + nkLog(tag: NCGlobal.shared.logTagE2EE, emoji: .error, message: "Unable to resolve the stored E2EE key set: \(error)") + + return NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: NSLocalizedString("_e2ee_no_metadataKey_found_", comment: "")) } } return resultsGetE2EEMetadata.error diff --git a/iOSClient/Networking/NCErrorContext.swift b/iOSClient/Networking/NCErrorContext.swift new file mode 100644 index 0000000000..339fd9b41e --- /dev/null +++ b/iOSClient/Networking/NCErrorContext.swift @@ -0,0 +1,118 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Milen Pivchev +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation + +/// What the user was doing when an error happened, so the message can say what failed. +enum NCErrorContext: Equatable { + case generic + case upload(fileName: String) + case download(fileName: String) + case delete(fileName: String) + case rename + case moveOrCopy(fileName: String) + case createFolder(folderName: String) + case favorite(fileName: String) + case createDocument(fileName: String) + case openDocument(fileName: String) + /// A request that sends the user's credentials. + case login + + /// The context of a transfer event, from its `networkingStatus`. + init(networkingStatus: String, fileName: String) { + let global = NCGlobal.shared + + switch networkingStatus { + case global.networkingStatusUploading, global.networkingStatusUploaded: + self = .upload(fileName: fileName) + + case global.networkingStatusDownloading, global.networkingStatusDownloaded: + self = .download(fileName: fileName) + + case global.networkingStatusDelete: + self = .delete(fileName: fileName) + + case global.networkingStatusRename: + self = .rename + + case global.networkingStatusCopyMove: + self = .moveOrCopy(fileName: fileName) + + case global.networkingStatusCreateFolder: + self = .createFolder(folderName: fileName) + + case global.networkingStatusFavorite: + self = .favorite(fileName: fileName) + + default: + self = .generic + } + } + + /// The context of the operation a metadata is waiting for or failed at, from its metadata `status`. + init(metadataStatus: Int, fileName: String) { + let global = NCGlobal.shared + + switch metadataStatus { + case global.metadataStatusWaitUpload, global.metadataStatusUploading, global.metadataStatusUploadError: + self = .upload(fileName: fileName) + + case global.metadataStatusWaitDownload, global.metadataStatusDownloading, global.metadataStatusDownloadError: + self = .download(fileName: fileName) + + case global.metadataStatusWaitDelete: + self = .delete(fileName: fileName) + + case global.metadataStatusWaitRename: + self = .rename + + case global.metadataStatusWaitCopy, global.metadataStatusWaitMove: + self = .moveOrCopy(fileName: fileName) + + case global.metadataStatusWaitCreateFolder: + self = .createFolder(folderName: fileName) + + case global.metadataStatusWaitFavorite: + self = .favorite(fileName: fileName) + + default: + self = .generic + } + } + + /// A title saying what failed, e.g. "Couldn't upload “IMG_0042.jpg”"; `nil` for `generic` and `login`. + var failureTitle: String? { + switch self { + case .generic, .login: + return nil + + case .upload(let fileName): + return String(format: NSLocalizedString("_error_title_upload_", comment: ""), fileName) + + case .download(let fileName): + return String(format: NSLocalizedString("_error_title_download_", comment: ""), fileName) + + case .delete(let fileName): + return String(format: NSLocalizedString("_error_title_delete_", comment: ""), fileName) + + case .rename: + return NSLocalizedString("_error_title_rename_", comment: "") + + case .moveOrCopy(let fileName): + return String(format: NSLocalizedString("_error_title_move_copy_", comment: ""), fileName) + + case .createFolder(let folderName): + return String(format: NSLocalizedString("_error_title_create_folder_", comment: ""), folderName) + + case .favorite(let fileName): + return String(format: NSLocalizedString("_error_title_favorite_", comment: ""), fileName) + + case .createDocument(let fileName): + return String(format: NSLocalizedString("_error_title_create_document_", comment: ""), fileName) + + case .openDocument(let fileName): + return String(format: NSLocalizedString("_error_title_open_document_", comment: ""), fileName) + } + } +} diff --git a/iOSClient/Networking/NCNetworking+TransferDelegate.swift b/iOSClient/Networking/NCNetworking+TransferDelegate.swift index 5ebe490f74..ef43f099c4 100644 --- a/iOSClient/Networking/NCNetworking+TransferDelegate.swift +++ b/iOSClient/Networking/NCNetworking+TransferDelegate.swift @@ -228,10 +228,11 @@ extension NCNetworking: NCTransferDelegate { let resultsFile = await NextcloudKit.shared.getFileFromFileIdAsync(fileId: fileId, account: account) guard resultsFile.error == .success, let file = resultsFile.file else { + let error = resultsFile.error == .success ? NKError(errorCode: NCGlobal.shared.errorResourceNotFound, errorDescription: "File not found") : resultsFile.error + Task { await showErrorBanner(windowScene: windowScene, - text: resultsFile.error.errorDescription, - errorCode: resultsFile.error.errorCode) + error: error) } return } diff --git a/iOSClient/Networking/NCNetworking+Upload.swift b/iOSClient/Networking/NCNetworking+Upload.swift index 97433aee68..1714def4d8 100644 --- a/iOSClient/Networking/NCNetworking+Upload.swift +++ b/iOSClient/Networking/NCNetworking+Upload.swift @@ -470,7 +470,9 @@ extension NCNetworking { @MainActor func uploadForbidden(metadata: tableMetadata, error: NKError) async { let newFileName = self.utilityFileSystem.createFileName(metadata.fileName, serverUrl: metadata.serverUrl, account: metadata.account) - let alertController = UIAlertController(title: error.errorDescription, message: NSLocalizedString("_change_upload_filename_", comment: ""), preferredStyle: .alert) + + let title = error.userFriendlyMessage(for: .upload(fileName: metadata.fileNameView)) ?? error.localizedGenericMessage + let alertController = UIAlertController(title: title, message: NSLocalizedString("_change_upload_filename_", comment: ""), preferredStyle: .alert) alertController.addAction(UIAlertAction(title: String(format: NSLocalizedString("_save_file_as_", comment: ""), newFileName), style: .default, handler: { _ in Task { @@ -515,7 +517,7 @@ extension NCNetworking { let newFileName = self.utilityFileSystem.createFileName(metadata.fileName, serverUrl: metadata.serverUrl, account: metadata.account) - let alertController = UIAlertController(title: results.error.errorDescription, message: NSLocalizedString("_change_upload_filename_", comment: ""), preferredStyle: .alert) + let alertController = UIAlertController(title: results.error.userFriendlyMessage(), message: NSLocalizedString("_change_upload_filename_", comment: ""), preferredStyle: .alert) alertController.addAction(UIAlertAction(title: String(format: NSLocalizedString("_save_file_as_", comment: ""), newFileName), style: .default, handler: { _ in Task { diff --git a/iOSClient/Networking/NCNetworking+WebDAV.swift b/iOSClient/Networking/NCNetworking+WebDAV.swift index 6df3aeed1f..f991f3a99c 100644 --- a/iOSClient/Networking/NCNetworking+WebDAV.swift +++ b/iOSClient/Networking/NCNetworking+WebDAV.swift @@ -170,7 +170,7 @@ extension NCNetworking { fileNameFolder = utilityFileSystem.createFileName(fileNameFolder, serverUrl: serverUrl, account: session.account) } if fileNameFolder.isEmpty { - return NKError(errorCode: global.errorIncorrectFileName, errorDescription: "") + return NKError(errorCode: global.errorIncorrectFileName, errorDescription: "", kind: .invalidName) } let serverUrlFileName = utilityFileSystem.createServerUrl(serverUrl: serverUrl, fileName: fileNameFolder) @@ -414,7 +414,7 @@ extension NCNetworking { let error = await NCNetworkingE2EERename().rename(metadata: metadata, fileNameNew: fileNameNew, windowScene: windowScene) if error != .success { - return NKError(errorCode: error.errorCode, errorDescription: error.errorDescription) + return error } } else { let ocId = metadata.ocId diff --git a/iOSClient/Networking/NKError+UserFriendlyMessage.swift b/iOSClient/Networking/NKError+UserFriendlyMessage.swift new file mode 100644 index 0000000000..78b0d446a9 --- /dev/null +++ b/iOSClient/Networking/NKError+UserFriendlyMessage.swift @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Milen Pivchev +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation +import NextcloudKit + +extension NKError { + /// The message to show users for this error, or `nil` when nothing should be shown (success or cancellation). + /// + /// Tries the app's own text, then the few operations where the generic message would be wrong, then + /// NextcloudKit's generic message. Keep `errorDescription` for logs. + func userFriendlyMessage(for context: NCErrorContext = .generic) -> String? { + guard let kind, kind != .cancelled, errorCode != NCGlobal.shared.errorTaskCancelled else { return nil } + + if let appMessage { + return appMessage + } + + if let operationMessage = operationMessage(for: context, kind: kind) { + return operationMessage + } + + return localizedGenericMessage + } + + /// Text the app wrote itself: a localization key, or the description of one of its own error codes. + private var appMessage: String? { + let localized = NSLocalizedString(errorDescription, comment: "") + + if localized != errorDescription { + return localized + } + + let global = NCGlobal.shared + let isAppError = (global.errorInternalError...global.errorE2EENotEnabled).contains(errorCode) + + return isAppError && !errorDescription.isEmpty ? errorDescription : nil + } + + private var isVirusDetected: Bool { + let global = NCGlobal.shared + let isRejectedUpload = errorCode == global.errorBadRequest || errorCode == global.errorUnsupportedMediaType + + return isRejectedUpload && errorDescription.localizedCaseInsensitiveContains("virus") + } + + /// Unique cases that require a specific non-generic error + private func operationMessage(for context: NCErrorContext, kind: Kind) -> String? { + switch (context, kind) { + case (.upload, .unknown) where isVirusDetected: + return NSLocalizedString("_virus_detect_", comment: "") + + case (.upload, .conflict) where errorCode == NCGlobal.shared.errorConflict: + return NSLocalizedString("_error_upload_no_folder_", comment: "") + + case (.createFolder, .conflict): + return NSLocalizedString("_error_create_folder_no_parent_", comment: "") + + case (.createDocument, .forbidden): + return NSLocalizedString("_error_create_document_", comment: "") + + case (.openDocument, .forbidden): + return NSLocalizedString("_error_open_document_", comment: "") + + case (.login, .unauthorized), (.login, .forbidden): + return NSLocalizedString("_error_login_credentials_", comment: "") + + default: + return nil + } + } +} diff --git a/iOSClient/Notification/NCNotification.swift b/iOSClient/Notification/NCNotification.swift index b857bfb611..1678d7e056 100644 --- a/iOSClient/Notification/NCNotification.swift +++ b/iOSClient/Notification/NCNotification.swift @@ -275,7 +275,7 @@ class NCNotification: UITableViewController, NCNotificationCellDelegate { self.tableView.reloadData() } else if error != .success { Task { - await showErrorBanner(windowScene: self.windowScene, text: error.errorDescription, errorCode: error.errorCode) + await showErrorBanner(windowScene: self.windowScene, error: error) } } else { print("[Error] The user has been changed during networking process.") @@ -317,7 +317,7 @@ class NCNotification: UITableViewController, NCNotificationCellDelegate { } } else if error != .success { Task { - await showErrorBanner(windowScene: self.windowScene, text: error.errorDescription, errorCode: error.errorCode) + await showErrorBanner(windowScene: self.windowScene, error: error) } } else { print("[Error] The user has been changed during networking process.") diff --git a/iOSClient/RichWorkspace/NCRichWorkspaceCoordinator.swift b/iOSClient/RichWorkspace/NCRichWorkspaceCoordinator.swift index fa3748790d..052aab06c6 100644 --- a/iOSClient/RichWorkspace/NCRichWorkspaceCoordinator.swift +++ b/iOSClient/RichWorkspace/NCRichWorkspaceCoordinator.swift @@ -33,7 +33,7 @@ class NCRichWorkspaceCoordinator: NSObject { let resultError: NKError = error == .success ? .invalidData : error Task { let windowScene = await SceneManager.shared.getWindowScene(controller: controller) - await showErrorBanner(windowScene: windowScene, text: resultError.errorDescription, errorCode: resultError.errorCode) + await showErrorBanner(windowScene: windowScene, error: resultError, context: .createDocument(fileName: NCGlobal.shared.fileNameRichWorkspace)) } return } @@ -69,9 +69,11 @@ class NCRichWorkspaceCoordinator: NSObject { NCActivityIndicator.shared.stop() guard error == .success, let url else { let resultError: NKError = error == .success ? .invalidData : error + let fileName = metadata.fileNameView + Task { let windowScene = await SceneManager.shared.getWindowScene(controller: controller) - await showErrorBanner(windowScene: windowScene, text: resultError.errorDescription, errorCode: resultError.errorCode) + await showErrorBanner(windowScene: windowScene, error: resultError, context: .openDocument(fileName: fileName)) } return } diff --git a/iOSClient/Select/NCSelect.swift b/iOSClient/Select/NCSelect.swift index b837efe5fb..e0cd77d9a9 100644 --- a/iOSClient/Select/NCSelect.swift +++ b/iOSClient/Select/NCSelect.swift @@ -217,7 +217,7 @@ class NCSelect: UIViewController, UIGestureRecognizerDelegate, UIAdaptivePresent if error != .success { Task { let windowScene = SceneManager.shared.getWindow(sceneIdentifier: sceneIdentifier)?.windowScene - await showErrorBanner(windowScene: windowScene, text: error.errorDescription, errorCode: error.errorCode) + await showErrorBanner(windowScene: windowScene, error: error, context: NCErrorContext(networkingStatus: networkingStatus, fileName: fileName)) } } @@ -258,12 +258,12 @@ class NCSelect: UIViewController, UIGestureRecognizerDelegate, UIAdaptivePresent func createFolderButtonPressed(_ sender: UIButton) { Task { @MainActor in let capabilities = await NKCapabilities.shared.getCapabilities(for: session.account) - let alertController = UIAlertController.createFolderWith(serverUrl: serverUrl, session: session, capabilities: capabilities) { error in + let alertController = UIAlertController.createFolderWith(serverUrl: serverUrl, session: session, capabilities: capabilities) { error, folderName in if error != .success { Task { await showErrorBanner(windowScene: self.windowScene, - text: error.errorDescription, - errorCode: error.errorCode) + error: error, + context: .createFolder(folderName: folderName)) } } } diff --git a/iOSClient/Settings/AutoUpload/Utility/NCAutoUploadCounter.swift b/iOSClient/Settings/AutoUpload/Utility/NCAutoUploadCounter.swift index 8228baac6b..e0c4ee269d 100644 --- a/iOSClient/Settings/AutoUpload/Utility/NCAutoUploadCounter.swift +++ b/iOSClient/Settings/AutoUpload/Utility/NCAutoUploadCounter.swift @@ -182,7 +182,7 @@ final class NCAutoUploadCounter { ascending: false, limit: 1 )?.first - let error = metadata?.sessionError.trimmingCharacters(in: .whitespacesAndNewlines) + let error = metadata?.sessionErrorMessage.trimmingCharacters(in: .whitespacesAndNewlines) if let error, !error.isEmpty { suspensionError = error diff --git a/iOSClient/Settings/E2EE/NCEndToEndSetup.swift b/iOSClient/Settings/E2EE/NCEndToEndSetup.swift index 9a45b123fe..be1edf012e 100644 --- a/iOSClient/Settings/E2EE/NCEndToEndSetup.swift +++ b/iOSClient/Settings/E2EE/NCEndToEndSetup.swift @@ -588,6 +588,21 @@ class NCEndToEndSetup { } let deleteError = await NextcloudKit.shared.deleteE2EEPublicKeyAsync(account: session.account, password: password, options: options).error + + let passwordRejectedCodes = [global.errorUnauthorized, global.errorForbidden, global.errorUnauthorized997] + + if password?.isEmpty == false, passwordRejectedCodes.contains(deleteError.errorCode) { + nkLog(tag: global.logTagE2EE, emoji: .error, message: "Deleting the public key was rejected: \(deleteError.errorCode) \(deleteError.errorDescription)") + + throw NKError( + errorCode: global.errorInternalError, + errorDescription: NSLocalizedString( + "_error_password_incorrect_", + comment: "" + ) + ) + } + guard deleteError == .success else { throw deleteError } diff --git a/iOSClient/Settings/E2EE/NCManageE2EEModel.swift b/iOSClient/Settings/E2EE/NCManageE2EEModel.swift index 8493b99bc9..4db86cf711 100644 --- a/iOSClient/Settings/E2EE/NCManageE2EEModel.swift +++ b/iOSClient/Settings/E2EE/NCManageE2EEModel.swift @@ -82,13 +82,12 @@ class NCManageE2EE: NSObject, ObservableObject, ViewOnAppearHandling, TOPasscode } await showErrorBanner( windowScene: windowScene, - text: error.errorDescription + error: error ) } catch { - // fallback (non NKError) await showErrorBanner( windowScene: windowScene, - text: error.localizedDescription + error: NKError(error: error) ) } } @@ -145,13 +144,12 @@ class NCManageE2EE: NSObject, ObservableObject, ViewOnAppearHandling, TOPasscode } await showErrorBanner( windowScene: windowScene, - text: error.errorDescription + error: error ) } catch { - // fallback (non NKError) await showErrorBanner( windowScene: windowScene, - text: error.localizedDescription + error: NKError(error: error) ) } } diff --git a/iOSClient/Settings/E2EE/NCManageE2EEView.swift b/iOSClient/Settings/E2EE/NCManageE2EEView.swift index 0d45c0facc..4b9ce81250 100644 --- a/iOSClient/Settings/E2EE/NCManageE2EEView.swift +++ b/iOSClient/Settings/E2EE/NCManageE2EEView.swift @@ -245,8 +245,7 @@ struct NCManageE2EEView: View { text: "E2E delete publicKey") } else { await showErrorBanner(windowScene: model.windowScene, - text: results.error.errorDescription, - errorCode: results.error.errorCode) + error: results.error) } } } @@ -277,8 +276,7 @@ struct NCManageE2EEView: View { text: "E2E delete privateKey") } else { await showErrorBanner(windowScene: model.windowScene, - text: results.error.errorDescription, - errorCode: results.error.errorCode) + error: results.error) } } } @@ -308,8 +306,7 @@ struct NCManageE2EEView: View { text: "E2E delete Keys from FS") } else { await showErrorBanner(windowScene: model.windowScene, - text: results.error.errorDescription, - errorCode: results.error.errorCode) + error: results.error) } } } diff --git a/iOSClient/Supporting Files/en.lproj/Localizable.strings b/iOSClient/Supporting Files/en.lproj/Localizable.strings index 4823b05018..e83977f6e9 100644 --- a/iOSClient/Supporting Files/en.lproj/Localizable.strings +++ b/iOSClient/Supporting Files/en.lproj/Localizable.strings @@ -112,9 +112,6 @@ "_view_in_folder_" = "View in folder"; "_active_" = "Active"; "_disabled_" = "Disabled"; -"_compact_" = "Compact"; -"_normal_" = "Normal"; -"_verbose_" = "Verbose"; "_error_occurred_" = "An error occurred, please try again."; "_retry_" = "Retry"; "_images_" = "Images"; @@ -599,8 +596,6 @@ "_while_charging_" = "While charging"; "_keep_screen_awake_" = "Keep screen awake\nwhile transferring files"; "_error_not_found_" = "The requested resource could not be found"; -"_error_conflict_" = "The request could not be completed due to a conflict with the current state of the resource"; -"_error_precondition_" = "The server does not meet one of the preconditions of the requester"; "_downloading_" = "Downloading"; "_additional_options_" = "Additional options"; "_unauthorizedFilesPasscode_" = "Files app cannot be used with an activated passcode"; @@ -718,13 +713,9 @@ // MARK: Login poll -// MARK: File name validator (used by NextcloudKit, don't remove) +// MARK: File name validator "_file_name_validator_error_reserved_name_" = "\"%@\" is a forbidden name."; -"_file_name_validator_error_forbidden_file_extension_" = "\".%@\" is a forbidden file extension."; -"_file_name_validator_error_invalid_character_" = "Name contains an invalid character: \"%@\"."; -"_file_name_validator_error_space_" = "Name must not contain spaces at the beginning or end."; "_file_name_new_extension_" = "Changing the extension might cause this file to open in a different application."; -"_file_name_empty_" = "File name cannot be empty."; "_please_rename_file_" = "Please rename the file or folder."; "hidden_file_name_warning" = "Name will result in a hidden file."; @@ -785,6 +776,9 @@ "_e2ee_checksum_error_message_" = "The encrypted file checksum is missing or invalid.\n\nThis may be caused by incomplete, outdated, or inconsistent encryption metadata and does not necessarily mean that the file or server has been compromised.\n\nContinue only if you trust this server and understand the risk. Otherwise, exit without opening the file."; "_e2ee_key_checksum_" = "End-to-end encryption error, key checksum failed"; "_e2ee_decode_metadata_" = "End-to-end encryption error, cannot decode metadata"; +"_e2ee_encode_metadata_" = "End-to-end encryption error, cannot encode metadata"; +"_e2ee_version_not_supported_" = "End-to-end encryption error, unsupported encryption version"; +"_e2ee_store_keys_" = "End-to-end encryption error, cannot store the encryption keys"; "_e2ee_no_lock_" = "End-to-end encryption error, unable to lock file"; "_e2ee_no_decrypt_metadata_" = "End-to-end encryption error, cannot decrypt metadata"; "_e2ee_no_enc_file_" = "End-to-end encryption error, cannot encrypt file"; @@ -879,3 +873,21 @@ "_seek_backward_10_" = "Go back 10 seconds"; "_seek_forward_10_" = "Go forward 10 seconds"; + +// MARK: Error messages + +"_error_title_upload_" = "Couldn't upload “%@”"; +"_error_title_download_" = "Couldn't download “%@”"; +"_error_title_delete_" = "Couldn't delete “%@”"; +"_error_title_rename_" = "Couldn't rename the file or folder"; +"_error_title_move_copy_" = "Couldn't move or copy “%@”"; +"_error_title_create_folder_" = "Couldn't create the folder “%@”"; +"_error_title_favorite_" = "Couldn't update the favorite for “%@”"; +"_error_upload_no_folder_" = "The folder you're uploading to doesn't exist anymore."; +"_error_create_folder_no_parent_" = "The parent folder doesn't exist."; +"_error_title_create_document_" = "Couldn't create “%@”"; +"_error_title_open_document_" = "Couldn't open “%@”"; +"_error_create_document_" = "A file with this name may already exist, or you can't add files here."; +"_error_open_document_" = "The document may have been moved or deleted, or you no longer have access to it."; +"_error_login_credentials_" = "The login details are incorrect. Please try again."; +"_error_password_incorrect_" = "The password is incorrect."; diff --git a/iOSClient/Terms of service/NCTermOfServiceModel.swift b/iOSClient/Terms of service/NCTermOfServiceModel.swift index e4d7a10861..a3432dc66b 100644 --- a/iOSClient/Terms of service/NCTermOfServiceModel.swift +++ b/iOSClient/Terms of service/NCTermOfServiceModel.swift @@ -59,7 +59,7 @@ class NCTermOfServiceModel: ObservableObject { } } else { let windowScene = SceneManager.shared.getWindowScene(controller: controller) - await showErrorBanner(windowScene: windowScene, text: error.errorDescription, errorCode: error.errorCode) + await showErrorBanner(windowScene: windowScene, error: error) } } self.dismissView = true diff --git a/iOSClient/Transfers/NCTransfersModel.swift b/iOSClient/Transfers/NCTransfersModel.swift index 6de7ef6891..2706d7f9fe 100644 --- a/iOSClient/Transfers/NCTransfersModel.swift +++ b/iOSClient/Transfers/NCTransfersModel.swift @@ -236,7 +236,7 @@ final class TransfersViewModel: ObservableObject, NCMetadataDownloadTransfersSuc status += " – " + NSLocalizedString("_retry_soon_", comment: "") } } - return (symbol, status, item.sessionError) + return (symbol, status, item.sessionErrorMessage) case global.metadataStatusNormal: return ("checkmark.circle", NSLocalizedString("_done_", comment: ""), sizeText) default: diff --git a/iOSClient/Trash/NCTrash+Networking.swift b/iOSClient/Trash/NCTrash+Networking.swift index 4338787036..093b7da858 100644 --- a/iOSClient/Trash/NCTrash+Networking.swift +++ b/iOSClient/Trash/NCTrash+Networking.swift @@ -55,7 +55,7 @@ extension NCTrash { let results = await NextcloudKit.shared.deleteFileOrFolderAsync(serverUrlFileName: serverUrlFileName, account: session.account) if results.error != .success { - await showErrorBanner(windowScene: self.windowScene, text: results.error.errorDescription, errorCode: results.error.errorCode) + await showErrorBanner(windowScene: self.windowScene, error: results.error) } await self.database.deleteTrashAsync(fileId: nil, account: session.account) await self.reloadDataSource() @@ -70,7 +70,7 @@ extension NCTrash { let results = await NextcloudKit.shared.deleteFileOrFolderAsync(serverUrlFileName: serverUrlFileName, account: session.account) if results.error != .success { - await showErrorBanner(windowScene: self.windowScene, text: results.error.errorDescription, errorCode: results.error.errorCode) + await showErrorBanner(windowScene: self.windowScene, error: results.error, context: .delete(fileName: result.trashbinFileName)) } await self.database.deleteTrashAsync(fileId: fileId, account: session.account) await self.reloadDataSource() diff --git a/iOSClient/Viewer/NCViewer.swift b/iOSClient/Viewer/NCViewer.swift index 1bcc5d1b61..ac8184ea1e 100644 --- a/iOSClient/Viewer/NCViewer.swift +++ b/iOSClient/Viewer/NCViewer.swift @@ -89,7 +89,7 @@ class NCViewer: NSObject { let editorError = (error as? NKError) ?? .invalidData let windowScene = SceneManager.shared.getWindowScene(controller: delegate?.tabBarController as? NCMainTabBarController) - await showErrorBanner(windowScene: windowScene, text: editorError.errorDescription, errorCode: editorError.errorCode) + await showErrorBanner(windowScene: windowScene, error: editorError, context: .openDocument(fileName: metadata.fileNameView)) return nil } diff --git a/iOSClient/Viewer/NCViewerDirectEditing/NCViewerDirectEditing.swift b/iOSClient/Viewer/NCViewerDirectEditing/NCViewerDirectEditing.swift index 572c566371..2f75e80e9b 100644 --- a/iOSClient/Viewer/NCViewerDirectEditing/NCViewerDirectEditing.swift +++ b/iOSClient/Viewer/NCViewerDirectEditing/NCViewerDirectEditing.swift @@ -437,7 +437,7 @@ final class NCViewerDirectEditing: UIViewController, WKNavigationDelegate, WKScr guard error == .success, account == self.metadata.account else { Task { let windowScene = SceneManager.shared.getWindow(sceneIdentifier: self.sceneIdentifier)?.windowScene - await showErrorBanner(windowScene: windowScene, text: error.errorDescription, errorCode: error.errorCode) + await showErrorBanner(windowScene: windowScene, error: error) } return } @@ -565,7 +565,7 @@ extension NCViewerDirectEditing: NCSelectDelegate { } else { Task { let windowScene = SceneManager.shared.getWindow(sceneIdentifier: self.sceneIdentifier)?.windowScene - await showErrorBanner(windowScene: windowScene, text: error.errorDescription, errorCode: error.errorCode) + await showErrorBanner(windowScene: windowScene, error: error == .success ? .invalidData : error) } } } diff --git a/iOSClient/Viewer/NCViewerProviderContextMenu.swift b/iOSClient/Viewer/NCViewerProviderContextMenu.swift index e61cac09fa..b07fdb51c8 100644 --- a/iOSClient/Viewer/NCViewerProviderContextMenu.swift +++ b/iOSClient/Viewer/NCViewerProviderContextMenu.swift @@ -176,7 +176,7 @@ extension NCViewerProviderContextMenu: NCTransferDelegate { guard let self, ocId == metadata?.ocId, metadata?.isImage == true else { return } if error != .success { let windowScene = SceneManager.shared.getWindow(sceneIdentifier: sceneIdentifier)?.windowScene - await showErrorBanner(windowScene: windowScene, text: error.errorDescription, errorCode: error.errorCode) + await showErrorBanner(windowScene: windowScene, error: error, context: NCErrorContext(networkingStatus: networkingStatus, fileName: fileName)) } switch networkingStatus { diff --git a/iOSClient/Viewer/NCViewerQuickLook/NCViewerQuickLook.swift b/iOSClient/Viewer/NCViewerQuickLook/NCViewerQuickLook.swift index 36dab8777f..7a4479defa 100644 --- a/iOSClient/Viewer/NCViewerQuickLook/NCViewerQuickLook.swift +++ b/iOSClient/Viewer/NCViewerQuickLook/NCViewerQuickLook.swift @@ -256,9 +256,12 @@ extension NCViewerQuickLook: QLPreviewControllerDataSource, QLPreviewControllerD } error = results.error + var errorContext = NCErrorContext.upload(fileName: fileName) + if error == .success { let results = await NCNetworking.shared.readFileAsync(serverUrlFileName: serverUrlFileName, account: metadata.account) error = results.error + errorContext = .generic if results.error == .success, let metadata = results.metadata { // clean dir @@ -279,7 +282,7 @@ extension NCViewerQuickLook: QLPreviewControllerDataSource, QLPreviewControllerD } if error != .success { - await showErrorBanner(windowScene: windowScene, text: error.errorDescription, errorCode: error.errorCode) + await showErrorBanner(windowScene: windowScene, error: error, context: errorContext) } self.dismiss(animated: true) diff --git a/iOSClient/Viewer/NCViewerRichdocuments/NCViewerRichdocuments.swift b/iOSClient/Viewer/NCViewerRichdocuments/NCViewerRichdocuments.swift index 2a77c68955..725961cf19 100644 --- a/iOSClient/Viewer/NCViewerRichdocuments/NCViewerRichdocuments.swift +++ b/iOSClient/Viewer/NCViewerRichdocuments/NCViewerRichdocuments.swift @@ -286,7 +286,7 @@ class NCViewerRichdocuments: UIViewController, WKNavigationDelegate, WKScriptMes } else { Task { let windowScene = SceneManager.shared.getWindow(sceneIdentifier: self.sceneIdentifier)?.windowScene - await showErrorBanner(windowScene: windowScene, text: error.errorDescription, errorCode: error.errorCode) + await showErrorBanner(windowScene: windowScene, error: error) } } }) @@ -343,7 +343,7 @@ class NCViewerRichdocuments: UIViewController, WKNavigationDelegate, WKScriptMes } else { Task { let windowScene = SceneManager.shared.getWindow(sceneIdentifier: self.sceneIdentifier)?.windowScene - await showErrorBanner(windowScene: windowScene, text: error.errorDescription, errorCode: error.errorCode) + await showErrorBanner(windowScene: windowScene, error: error == .success ? .invalidData : error) } } } @@ -361,7 +361,7 @@ class NCViewerRichdocuments: UIViewController, WKNavigationDelegate, WKScriptMes } else { Task { let windowScene = SceneManager.shared.getWindow(sceneIdentifier: self.sceneIdentifier)?.windowScene - await showErrorBanner(windowScene: windowScene, text: error.errorDescription, errorCode: error.errorCode) + await showErrorBanner(windowScene: windowScene, error: error == .success ? .invalidData : error) } } }