From 8980373b066e88d1e65ddd616eacb8e50d39ba5a Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Wed, 14 Oct 2026 16:58:20 +0200 Subject: [PATCH 1/7] WIP Signed-off-by: Milen Pivchev --- Nextcloud.xcodeproj/project.pbxproj | 24 ++- .../xcshareddata/swiftpm/Package.resolved | 20 +-- .../NKErrorUserMessageTests.swift | 122 +++++++++++++++ .../tableMetadata+SessionErrorMessage.swift | 21 +++ .../GUI/Lucid Banner/ErrorBannerView.swift | 11 +- .../Collection Common/Cell/NCGridCell.swift | 2 +- .../Collection Common/Cell/NCListCell.swift | 2 +- ...ionViewCommon+CollectionViewDelegate.swift | 2 +- ...Common+UIEditMenuInteractionDelegate.swift | 4 +- .../NCCollectionViewCommon.swift | 2 +- iOSClient/Main/NCDragDrop.swift | 7 +- iOSClient/Networking/NCErrorContext.swift | 108 +++++++++++++ .../Networking/NKError+UserMessage.swift | 148 ++++++++++++++++++ iOSClient/Select/NCSelect.swift | 2 +- .../Utility/NCAutoUploadCounter.swift | 2 +- .../en.lproj/Localizable.strings | 33 ++++ iOSClient/Transfers/NCTransfersModel.swift | 2 +- .../Viewer/NCViewerProviderContextMenu.swift | 2 +- 18 files changed, 487 insertions(+), 27 deletions(-) create mode 100644 Tests/NextcloudUnitTests/NKErrorUserMessageTests.swift create mode 100644 iOSClient/Data/tableMetadata+SessionErrorMessage.swift create mode 100644 iOSClient/Networking/NCErrorContext.swift create mode 100644 iOSClient/Networking/NKError+UserMessage.swift diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 17e2732831..adf8f25a75 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -8,12 +8,16 @@ /* Begin PBXBuildFile section */ 22BA14432CF44BDBA2E6C291 /* NCDocumentEditorSupportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B74B947E07444CC9CFE0A6C /* NCDocumentEditorSupportTests.swift */; }; + 27D0041A6F7CA1E970AD09B2 /* tableMetadata+SessionErrorMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = F840C79FAF28213971E832D4 /* tableMetadata+SessionErrorMessage.swift */; }; 2C1D5D7623E2DE3300334ABB /* NCManageDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7BAADB51ED5A87C00B7EAD4 /* NCManageDatabase.swift */; }; 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+UserMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79DFE0BDF3898A66910E67CA /* NKError+UserMessage.swift */; }; 370D26AF248A3D7A00121797 /* NCCellMain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 370D26AE248A3D7A00121797 /* NCCellMain.swift */; }; + 52FF854678FFAD3269661979 /* tableMetadata+SessionErrorMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = F840C79FAF28213971E832D4 /* tableMetadata+SessionErrorMessage.swift */; }; 63563FE53573412998852DD3 /* NCAlbumsPersistenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 439616AAA50F4360A761E5EF /* NCAlbumsPersistenceTests.swift */; }; + 822D68B06B101563A759AD59 /* NKError+UserMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79DFE0BDF3898A66910E67CA /* NKError+UserMessage.swift */; }; A12720261001000000000001 /* NCBackgroundUploadExtensionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F793E7C230417C7100F011FE /* NCBackgroundUploadExtensionManager.swift */; }; A1B2C3D430B0000100ABCDEF /* NCMediaViewerLoadingPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D330B0000100ABCDEF /* NCMediaViewerLoadingPolicy.swift */; }; A1E2EE010000000000000002 /* NCEndToEndKeySet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1E2EE010000000000000001 /* NCEndToEndKeySet.swift */; }; @@ -29,6 +33,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,10 +117,12 @@ 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 /* NKErrorUserMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 320C20803A58DAF7575440A8 /* NKErrorUserMessageTests.swift */; }; C0DECA022F65000100C0D001 /* NCCameraRollTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DECA012F65000100C0D001 /* NCCameraRollTests.swift */; }; 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 */; }; @@ -1318,9 +1325,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 /* NKErrorUserMessageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NKErrorUserMessageTests.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+UserMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NKError+UserMessage.swift"; sourceTree = ""; }; 8932E90EC4278026D86CCCC9 /* NCContextMenuComment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCContextMenuComment.swift; sourceTree = ""; }; A1B2C3D330B0000100ABCDEF /* NCMediaViewerLoadingPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCMediaViewerLoadingPolicy.swift; sourceTree = ""; }; A1E2EE010000000000000001 /* NCEndToEndKeySet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCEndToEndKeySet.swift; sourceTree = ""; }; @@ -1462,6 +1471,7 @@ C04E2F202A17BB4D001BAD85 /* NextcloudIntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NextcloudIntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = ""; }; @@ -2129,6 +2139,7 @@ F7FDFF592E437E55000D7688 /* NCAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAccount.swift; sourceTree = ""; }; F7FF2CB02842159500EBB7A1 /* NCSectionHeader.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = NCSectionHeader.xib; sourceTree = ""; }; F7FFFC9D2FB300440015441E /* NCAssistantSharedTextStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAssistantSharedTextStore.swift; sourceTree = ""; }; + F840C79FAF28213971E832D4 /* tableMetadata+SessionErrorMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "tableMetadata+SessionErrorMessage.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ @@ -2355,6 +2366,7 @@ isa = PBXGroup; children = ( C0DEDA7E30C1000100A00001 /* NCAutoUploadDateTests.swift */, + 320C20803A58DAF7575440A8 /* NKErrorUserMessageTests.swift */, F76BED7B306D0000002E1442 /* NCPhotoLibraryAssetTests.swift */, A1E2EE010000000000000009 /* NCEndToEndKeySetArchiveTests.swift */, A1E2EE020000000000000007 /* NCEndToEndKeySetResolverTests.swift */, @@ -2902,6 +2914,8 @@ F785129A2D79899E0087DDD0 /* NCNetworking+TermsOfService.swift */, F71916102E2901E800E13E96 /* NCNetworking+Upload.swift */, F7327E2F2B73A86700A462C7 /* NCNetworking+WebDAV.swift */, + 79DFE0BDF3898A66910E67CA /* NKError+UserMessage.swift */, + CE0CD9A00D74B01055F238E9 /* NCErrorContext.swift */, F70D8D8024A4A9BF000A5756 /* NCNetworkingProcess.swift */, F755BD9A20594AC7008C5FBB /* NCService.swift */, ); @@ -3455,6 +3469,7 @@ F764C3E02FFB7DF800029FD5 /* NCManageDatabase+MediaMetadataBackfill.swift */, F7BDC1D1300F440600C5D9FA /* NCManageDatabase+MediaPreviewBackfill.swift */, AF4BF61827562A4B0081CEEF /* NCManageDatabase+Metadata.swift */, + F840C79FAF28213971E832D4 /* tableMetadata+SessionErrorMessage.swift */, F7B769A72B7A0B2000C1AAEB /* NCManageDatabase+Metadata+Session.swift */, F7C687E82D22BD46004757BC /* NCManageDatabase+RecommendedFiles.swift */, F7C9B91C2B582F550064EA91 /* NCManageDatabase+SecurityGuard.swift */, @@ -4794,6 +4809,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + BAF64949B487A080A98F83F2 /* NKErrorUserMessageTests.swift in Sources */, C0DEDA7E30C1000100A00002 /* NCAutoUploadDateTests.swift in Sources */, F76BED7E306D0000002E1442 /* NCPhotoLibraryAssetTests.swift in Sources */, A1E2EE01000000000000000A /* NCEndToEndKeySetArchiveTests.swift in Sources */, @@ -4885,6 +4901,9 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 52FF854678FFAD3269661979 /* tableMetadata+SessionErrorMessage.swift in Sources */, + 2EF7D3FD0885CE182717578E /* NKError+UserMessage.swift in Sources */, + EB511D22564B7B9A605E70AE /* NCErrorContext.swift in Sources */, F7864ACF2A78FE73004870E0 /* NCManageDatabase+LocalFile.swift in Sources */, F71F6D0A2B6A6A5E00F1EB15 /* ThreadSafeArray.swift in Sources */, F7245925289BB59100474787 /* ThreadSafeDictionary.swift in Sources */, @@ -5147,6 +5166,9 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 27D0041A6F7CA1E970AD09B2 /* tableMetadata+SessionErrorMessage.swift in Sources */, + 822D68B06B101563A759AD59 /* NKError+UserMessage.swift in Sources */, + A358B569BF5FDABA6D9012D1 /* NCErrorContext.swift in Sources */, F76BED77306D0000002E1442 /* NCLocalDatabase.swift in Sources */, F76BED78306D0000002E1442 /* NCLocalDatabaseMigrator.swift in Sources */, F76BED7C306D0000002E1442 /* NCLocalDatabase+photoLibraryAsset.swift in Sources */, @@ -7207,7 +7229,7 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/nextcloud/NextcloudKit"; requirement = { - branch = main; + branch = "friendly-errors"; kind = branch; }; }; diff --git a/Nextcloud.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Nextcloud.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 6390e9cc23..93df22b1f4 100644 --- a/Nextcloud.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Nextcloud.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/abseil-cpp-binary.git", "state" : { - "revision" : "26509120768c4add61cc1a63059f15f4fb49edb4", - "version" : "1.2025051202.0" + "revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5", + "version" : "1.2024072200.0" } }, { @@ -24,8 +24,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/app-check.git", "state" : { - "revision" : "c7eb17e5545f36e43dc50e70da09c2e999714e16", - "version" : "12.0.0" + "revision" : "97f7d74dd0e8f3d0fe5fc75cc8957aa73bdc948a", + "version" : "11.3.2" } }, { @@ -78,8 +78,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleAppMeasurement.git", "state" : { - "revision" : "33f4d83faebca7dc908ecee956eb195cfc4a87d0", - "version" : "13.0.0" + "revision" : "b8455dd2ff55cc269fda7a4f9eac6c1e3588c53f", + "version" : "12.19.2" } }, { @@ -114,8 +114,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/grpc-binary.git", "state" : { - "revision" : "9d5e9f05670c9eab8d8c22731ddd5d4d9cf867a8", - "version" : "1.83.1" + "revision" : "75b31c842f664a0f46a2e590a570e370249fd8f6", + "version" : "1.69.1" } }, { @@ -204,8 +204,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/nextcloud/NextcloudKit", "state" : { - "branch" : "main", - "revision" : "053d6eb161a7024e00e1064ab2825cad3e9317a2" + "branch" : "friendly-errors", + "revision" : "636d643d90c9eb041781798d198d4c6c22584080" } }, { diff --git a/Tests/NextcloudUnitTests/NKErrorUserMessageTests.swift b/Tests/NextcloudUnitTests/NKErrorUserMessageTests.swift new file mode 100644 index 0000000000..d0ae662645 --- /dev/null +++ b/Tests/NextcloudUnitTests/NKErrorUserMessageTests.swift @@ -0,0 +1,122 @@ +// 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 NKErrorUserMessageTests { + 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.userMessage() == nil) + #expect(NKError.cancelled.userMessage(for: .upload(fileName: "a.jpg")) == nil) + #expect(NKError(errorCode: NCGlobal.shared.errorTaskCancelled, errorDescription: "Task cancelled").userMessage() == nil) + } + + @Test("Keeps text the app wrote itself") + func appMessages() { + #expect(NKError(errorCode: NCGlobal.shared.errorOfflineNotAllowed, errorDescription: "_offline_not_allowed_").userMessage() == localized("_offline_not_allowed_")) + #expect(NKError(errorCode: NCGlobal.shared.errorE2EELock, errorDescription: "Already translated").userMessage() == "Already translated") + } + + @Test("Prefers the server's message when Nextcloud wrote it for people") + func userFacingServerMessage() { + let body = Data(#"{"ocs":{"meta":{"statuscode":403},"data":"Group sharing is disabled by the administrator"}}"#.utf8) + let error = NKError(errorCode: 403, errorDescription: "403: Forbidden", responseData: body) + + #expect(error.userMessage(for: .delete(fileName: "a.jpg")) == "Group sharing is disabled by the administrator") + } + + @Test("Replaces technical server text") + func technicalServerText() { + let error = NKError(statusCode: 409, fallbackDescription: "Parent node does not exist") + + #expect(error.userMessage(for: .createFolder(folderName: "a/b")) == localized("_error_create_folder_no_parent_")) + #expect(error.userMessage() == localized("_error_changed_on_server_")) + } + + @Test("Words errors for the operation that failed") + func operationMessages() { + let forbidden = NKError(errorCode: 403, errorDescription: "403: Forbidden") + + #expect(forbidden.userMessage(for: .delete(fileName: "a.jpg")) == localized("_no_permission_delete_file_")) + #expect(forbidden.userMessage(for: .rename) == localized("_no_permission_modify_file_")) + #expect(forbidden.userMessage(for: .upload(fileName: "a.jpg")) == localized("_no_file_no_permission_to_create_")) + #expect(forbidden.userMessage() == localized("_error_forbidden_")) + #expect(NKError(errorCode: 405, errorDescription: "405", kind: .alreadyExists).userMessage(for: .createFolder(folderName: "Photos")) == localized("_error_create_folder_exists_")) + #expect(NKError(errorCode: 404, errorDescription: "404: Not found").userMessage(for: .download(fileName: "a.jpg")) == localized("_error_download_not_found_")) + } + + @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.userMessage(for: .upload(fileName: "eicar.com")) == localized("_virus_detect_")) + } + + @Test("Falls back to the kind of failure") + func kindMessages() { + #expect(NKError(errorCode: NSURLErrorTimedOut, errorDescription: "The request timed out.").userMessage(for: .upload(fileName: "a.jpg")) == localized("_error_timeout_")) + #expect(NKError(errorCode: 507, errorDescription: "507: Storage quota is reached").userMessage() == localized("_error_quota_exceeded_")) + #expect(NKError(errorCode: 418, errorDescription: "418: I'm a teapot").userMessage() == localized("_error_occurred_")) + } + + @Test("Every kind has a translated message", arguments: NKErrorUserMessageTests.shownKinds) + func everyKind(kind: NKError.Kind) { + let message = NKError(errorCode: -1, errorDescription: "Technical", kind: kind).userMessage() + + #expect(message != nil) + #expect(message != "Technical") + #expect(message?.hasPrefix("_") == false) + } + + @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")] + + #expect(NCErrorContext.generic.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 == localized("_error_quota_exceeded_")) + + metadata.errorCode = NSURLErrorCancelled + metadata.sessionError = "Transfer was cancelled." + + #expect(metadata.sessionErrorMessage == localized("_error_transfer_cancelled_")) + } +} diff --git a/iOSClient/Data/tableMetadata+SessionErrorMessage.swift b/iOSClient/Data/tableMetadata+SessionErrorMessage.swift new file mode 100644 index 0000000000..8f29fc6436 --- /dev/null +++ b/iOSClient/Data/tableMetadata+SessionErrorMessage.swift @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Milen Pivchev +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation +import NextcloudKit + +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) + + if error.kind == .cancelled { + return NSLocalizedString("_error_transfer_cancelled_", comment: "") + } + + let context = NCErrorContext(metadataStatus: status, fileName: fileNameView) + + return error.userMessage(for: context) ?? sessionError + } +} diff --git a/iOSClient/GUI/Lucid Banner/ErrorBannerView.swift b/iOSClient/GUI/Lucid Banner/ErrorBannerView.swift index 3af5768d52..abe8ed7226 100644 --- a/iOSClient/GUI/Lucid Banner/ErrorBannerView.swift +++ b/iOSClient/GUI/Lucid Banner/ErrorBannerView.swift @@ -9,10 +9,15 @@ import Alamofire @MainActor func showErrorBanner(windowScene: UIWindowScene?, - error: NKError) async { + error: NKError, + context: NCErrorContext = .generic) async { + guard let text = error.userMessage(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) } 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 6c221d267b..38dfe47f58 100755 --- a/iOSClient/Main/Collection Common/Cell/NCListCell.swift +++ b/iOSClient/Main/Collection Common/Cell/NCListCell.swift @@ -446,7 +446,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..72cef04143 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegate.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegate.swift @@ -81,7 +81,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+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 881e56f788..9308c1e5c2 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -846,7 +846,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/NCDragDrop.swift b/iOSClient/Main/NCDragDrop.swift index 70db9dab3b..f889cd849f 100644 --- a/iOSClient/Main/NCDragDrop.swift +++ b/iOSClient/Main/NCDragDrop.swift @@ -243,8 +243,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 } } @@ -276,7 +276,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/Networking/NCErrorContext.swift b/iOSClient/Networking/NCErrorContext.swift new file mode 100644 index 0000000000..54dd62ff87 --- /dev/null +++ b/iOSClient/Networking/NCErrorContext.swift @@ -0,0 +1,108 @@ +// 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) + + /// 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 an item 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`. + var failureTitle: String? { + switch self { + case .generic: + 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) + } + } +} diff --git a/iOSClient/Networking/NKError+UserMessage.swift b/iOSClient/Networking/NKError+UserMessage.swift new file mode 100644 index 0000000000..d1cfb76d05 --- /dev/null +++ b/iOSClient/Networking/NKError+UserMessage.swift @@ -0,0 +1,148 @@ +// 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 people for this error, or `nil` when nothing should be shown (success or cancellation). + /// + /// Tries the app's own text, the server's message when Nextcloud wrote it for people, a message for what was + /// being done, then one for the kind of failure. Keep `errorDescription` for logs. + func userMessage(for context: NCErrorContext = .generic) -> String? { + guard let kind, kind != .cancelled, errorCode != NCGlobal.shared.errorTaskCancelled else { return nil } + + if let appMessage { + return appMessage + } + + if let userFacingServerMessage { + return userFacingServerMessage + } + + return contextMessage(for: context, kind: kind) ?? kindMessage(for: kind) + } + + /// 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") + } + + private func contextMessage(for context: NCErrorContext, kind: Kind) -> String? { + switch (context, kind) { + case (.upload, .unknown) where isVirusDetected: + return NSLocalizedString("_virus_detect_", comment: "") + + case (.upload, .forbidden), (.createFolder, .forbidden): + return NSLocalizedString("_no_file_no_permission_to_create_", comment: "") + + case (.upload, .conflict) where errorCode == NCGlobal.shared.errorConflict: + return NSLocalizedString("_error_upload_no_folder_", comment: "") + + case (.download, .notFound): + return NSLocalizedString("_error_download_not_found_", comment: "") + + case (.delete, .forbidden): + return NSLocalizedString("_no_permission_delete_file_", comment: "") + + case (.delete, .locked): + return NSLocalizedString("_error_delete_locked_", comment: "") + + case (.rename, .forbidden), (.moveOrCopy, .forbidden): + return NSLocalizedString("_no_permission_modify_file_", comment: "") + + case (.rename, .alreadyExists): + return NSLocalizedString("_rename_already_exists_", comment: "") + + case (.moveOrCopy, .alreadyExists): + return NSLocalizedString("_file_already_exists_", comment: "") + + case (.createFolder, .alreadyExists): + return NSLocalizedString("_error_create_folder_exists_", comment: "") + + case (.createFolder, .conflict): + return NSLocalizedString("_error_create_folder_no_parent_", comment: "") + + case (.favorite, .forbidden): + return NSLocalizedString("_no_permission_favorite_file_", comment: "") + + default: + return nil + } + } + + private func kindMessage(for kind: Kind) -> String { + switch kind { + case .offline: + return NSLocalizedString("_error_offline_", comment: "") + + case .timeout: + return NSLocalizedString("_error_timeout_", comment: "") + + case .serverUnreachable: + return NSLocalizedString("_error_server_unreachable_", comment: "") + + case .secureConnection: + return NSLocalizedString("_error_secure_connection_", comment: "") + + case .unauthorized: + return NSLocalizedString("_error_unauthorized_", comment: "") + + case .forbidden: + return NSLocalizedString("_error_forbidden_", comment: "") + + case .notFound: + return NSLocalizedString("_error_not_found_on_server_", comment: "") + + case .alreadyExists: + return NSLocalizedString("_error_already_exists_", comment: "") + + case .conflict: + return NSLocalizedString("_error_changed_on_server_", comment: "") + + case .locked: + return NSLocalizedString("_error_locked_", comment: "") + + case .quotaExceeded: + return NSLocalizedString("_error_quota_exceeded_", comment: "") + + case .tooLarge: + return NSLocalizedString("_error_too_large_", comment: "") + + case .rateLimited: + return NSLocalizedString("_error_rate_limited_", comment: "") + + case .unavailable: + return NSLocalizedString("_error_unavailable_", comment: "") + + case .serverError: + return NSLocalizedString("_error_server_error_", comment: "") + + case .invalidResponse: + return NSLocalizedString("_error_invalid_response_", comment: "") + + case .localStorageFull: + return NSLocalizedString("_error_local_storage_full_", comment: "") + + case .cancelled, .unknown: + return NSLocalizedString("_error_occurred_", comment: "") + } + } +} diff --git a/iOSClient/Select/NCSelect.swift b/iOSClient/Select/NCSelect.swift index b837efe5fb..88669c4534 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)) } } diff --git a/iOSClient/Settings/AutoUpload/Utility/NCAutoUploadCounter.swift b/iOSClient/Settings/AutoUpload/Utility/NCAutoUploadCounter.swift index 0d92acaa80..2e18d2b0a7 100644 --- a/iOSClient/Settings/AutoUpload/Utility/NCAutoUploadCounter.swift +++ b/iOSClient/Settings/AutoUpload/Utility/NCAutoUploadCounter.swift @@ -179,7 +179,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/Supporting Files/en.lproj/Localizable.strings b/iOSClient/Supporting Files/en.lproj/Localizable.strings index 217db23a55..30b84c3ffa 100644 --- a/iOSClient/Supporting Files/en.lproj/Localizable.strings +++ b/iOSClient/Supporting Files/en.lproj/Localizable.strings @@ -864,3 +864,36 @@ "_autoupload_force_reupload_footer_" = "Previous upload records are ignored for photos and videos in the selected library or date range. New completed uploads are still recorded. This option stays enabled until you turn it off."; "_auto_upload_active_" = "Automatic photo and video upload active"; + +// 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 item"; +"_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_offline_" = "You're offline. Please check your internet connection."; +"_error_timeout_" = "The server took too long to respond. Try again later."; +"_error_server_unreachable_" = "Can't reach the server. Try again later."; +"_error_secure_connection_" = "A secure connection to the server couldn't be established."; +"_error_unauthorized_" = "Your login is no longer valid. Please log in again."; +"_error_forbidden_" = "You don't have permission to do this."; +"_error_not_found_on_server_" = "The item could not be found on the server."; +"_error_already_exists_" = "An item with this name already exists."; +"_error_changed_on_server_" = "The item was changed on the server. Refresh and try again."; +"_error_locked_" = "The file is locked. Try again later."; +"_error_quota_exceeded_" = "There is not enough storage space left on the server."; +"_error_too_large_" = "The file is too large for the server."; +"_error_rate_limited_" = "Too many requests. Wait a moment and try again."; +"_error_unavailable_" = "The server is temporarily unavailable. Try again later."; +"_error_server_error_" = "The server ran into a problem. Try again later."; +"_error_invalid_response_" = "The server sent an unexpected response. Try again later."; +"_error_local_storage_full_" = "There is not enough storage space left on this device."; +"_error_transfer_cancelled_" = "The transfer was cancelled."; +"_error_upload_no_folder_" = "The folder you're uploading to doesn't exist anymore."; +"_error_download_not_found_" = "This file is no longer available on the server."; +"_error_delete_locked_" = "This file is locked and can't be deleted right now."; +"_error_create_folder_exists_" = "A folder with this name already exists."; +"_error_create_folder_no_parent_" = "The parent folder doesn't exist."; diff --git a/iOSClient/Transfers/NCTransfersModel.swift b/iOSClient/Transfers/NCTransfersModel.swift index 427aba4b11..9bf935294b 100644 --- a/iOSClient/Transfers/NCTransfersModel.swift +++ b/iOSClient/Transfers/NCTransfersModel.swift @@ -243,7 +243,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/Viewer/NCViewerProviderContextMenu.swift b/iOSClient/Viewer/NCViewerProviderContextMenu.swift index 45a24e927c..36a80301d3 100644 --- a/iOSClient/Viewer/NCViewerProviderContextMenu.swift +++ b/iOSClient/Viewer/NCViewerProviderContextMenu.swift @@ -296,7 +296,7 @@ extension NCViewerProviderContextMenu: NCTransferDelegate { if error != .success { 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, context: NCErrorContext(networkingStatus: networkingStatus, fileName: fileName)) } } From c4c3c7c92cf7336800d4326f4bda5605b7135d06 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Thu, 15 Oct 2026 12:45:39 +0200 Subject: [PATCH 2/7] WIP Signed-off-by: Milen Pivchev --- Nextcloud.xcodeproj/project.pbxproj | 8 ++ .../NKErrorUserMessageTests.swift | 35 ++++--- .../tableMetadata+SessionErrorMessage.swift | 8 +- .../Networking/NKError+UserMessage.swift | 98 ++----------------- .../en.lproj/Localizable.strings | 21 ---- 5 files changed, 37 insertions(+), 133 deletions(-) diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index adf8f25a75..6f1213b6b8 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 6306971989EADE6757D44941 /* NextcloudKitUI in Frameworks */ = {isa = PBXBuildFile; productRef = 262894EB5592DF09FC34FCF9 /* NextcloudKitUI */; }; 22BA14432CF44BDBA2E6C291 /* NCDocumentEditorSupportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B74B947E07444CC9CFE0A6C /* NCDocumentEditorSupportTests.swift */; }; 27D0041A6F7CA1E970AD09B2 /* tableMetadata+SessionErrorMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = F840C79FAF28213971E832D4 /* tableMetadata+SessionErrorMessage.swift */; }; 2C1D5D7623E2DE3300334ABB /* NCManageDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7BAADB51ED5A87C00B7EAD4 /* NCManageDatabase.swift */; }; @@ -2212,6 +2213,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 6306971989EADE6757D44941 /* NextcloudKitUI in Frameworks */, F710FC80277B7D2700AA9FBF /* RealmSwift in Frameworks */, F70557B92ED44E4700135623 /* LucidBanner in Frameworks */, F72AD70F28C24BA1006CB92D /* NextcloudKit in Frameworks */, @@ -4151,6 +4153,7 @@ ); name = Share; packageProductDependencies = ( + 262894EB5592DF09FC34FCF9 /* NextcloudKitUI */, F72D7EB6263B1207000B3DFC /* MarkdownKit */, F710FC7F277B7D2700AA9FBF /* RealmSwift */, F72AD70E28C24BA1006CB92D /* NextcloudKit */, @@ -7284,6 +7287,11 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 262894EB5592DF09FC34FCF9 /* NextcloudKitUI */ = { + isa = XCSwiftPackageProductDependency; + package = F783034028B511D200B84583 /* XCRemoteSwiftPackageReference "NextcloudKit" */; + productName = NextcloudKitUI; + }; F3374AEF2D78B01B002A38F9 /* BitCollections */ = { isa = XCSwiftPackageProductDependency; package = F3374AEE2D78B01B002A38F9 /* XCRemoteSwiftPackageReference "swift-collections" */; diff --git a/Tests/NextcloudUnitTests/NKErrorUserMessageTests.swift b/Tests/NextcloudUnitTests/NKErrorUserMessageTests.swift index d0ae662645..1444594fbf 100644 --- a/Tests/NextcloudUnitTests/NKErrorUserMessageTests.swift +++ b/Tests/NextcloudUnitTests/NKErrorUserMessageTests.swift @@ -37,23 +37,24 @@ struct NKErrorUserMessageTests { } @Test("Replaces technical server text") - func technicalServerText() { + func technicalServerText() throws { let error = NKError(statusCode: 409, fallbackDescription: "Parent node does not exist") + let generic = try #require(error.userMessage()) #expect(error.userMessage(for: .createFolder(folderName: "a/b")) == localized("_error_create_folder_no_parent_")) - #expect(error.userMessage() == localized("_error_changed_on_server_")) + #expect(generic != error.errorDescription) + #expect(!generic.contains("409")) } - @Test("Words errors for the operation that failed") + @Test("Uses the generic message unless the operation needs its own") func operationMessages() { let forbidden = NKError(errorCode: 403, errorDescription: "403: Forbidden") + let generic = forbidden.userMessage() - #expect(forbidden.userMessage(for: .delete(fileName: "a.jpg")) == localized("_no_permission_delete_file_")) - #expect(forbidden.userMessage(for: .rename) == localized("_no_permission_modify_file_")) - #expect(forbidden.userMessage(for: .upload(fileName: "a.jpg")) == localized("_no_file_no_permission_to_create_")) - #expect(forbidden.userMessage() == localized("_error_forbidden_")) - #expect(NKError(errorCode: 405, errorDescription: "405", kind: .alreadyExists).userMessage(for: .createFolder(folderName: "Photos")) == localized("_error_create_folder_exists_")) - #expect(NKError(errorCode: 404, errorDescription: "404: Not found").userMessage(for: .download(fileName: "a.jpg")) == localized("_error_download_not_found_")) + #expect(generic != nil) + #expect(forbidden.userMessage(for: .delete(fileName: "a.jpg")) == generic) + #expect(forbidden.userMessage(for: .rename) == generic) + #expect(forbidden.userMessage(for: .upload(fileName: "a.jpg")) == generic) } @Test("Recognizes uploads rejected by the virus scanner") @@ -63,11 +64,14 @@ struct NKErrorUserMessageTests { #expect(error.userMessage(for: .upload(fileName: "eicar.com")) == localized("_virus_detect_")) } - @Test("Falls back to the kind of failure") + @Test("Words different kinds of failure differently") func kindMessages() { - #expect(NKError(errorCode: NSURLErrorTimedOut, errorDescription: "The request timed out.").userMessage(for: .upload(fileName: "a.jpg")) == localized("_error_timeout_")) - #expect(NKError(errorCode: 507, errorDescription: "507: Storage quota is reached").userMessage() == localized("_error_quota_exceeded_")) - #expect(NKError(errorCode: 418, errorDescription: "418: I'm a teapot").userMessage() == localized("_error_occurred_")) + let timeout = NKError(errorCode: NSURLErrorTimedOut, errorDescription: "The request timed out.").userMessage() + let quota = NKError(errorCode: 507, errorDescription: "507: Storage quota is reached").userMessage() + let unknown = NKError(errorCode: 418, errorDescription: "418: I'm a teapot").userMessage() + + #expect(Set([timeout, quota, unknown]).count == 3) + #expect(quota != "507: Storage quota is reached") } @Test("Every kind has a translated message", arguments: NKErrorUserMessageTests.shownKinds) @@ -112,11 +116,12 @@ struct NKErrorUserMessageTests { metadata.errorCode = 507 metadata.sessionError = "507: Storage quota is reached" - #expect(metadata.sessionErrorMessage == localized("_error_quota_exceeded_")) + #expect(metadata.sessionErrorMessage == NKError(errorCode: 507, errorDescription: "").userMessage()) metadata.errorCode = NSURLErrorCancelled metadata.sessionError = "Transfer was cancelled." - #expect(metadata.sessionErrorMessage == localized("_error_transfer_cancelled_")) + #expect(metadata.sessionErrorMessage != "Transfer was cancelled.") + #expect(!metadata.sessionErrorMessage.isEmpty) } } diff --git a/iOSClient/Data/tableMetadata+SessionErrorMessage.swift b/iOSClient/Data/tableMetadata+SessionErrorMessage.swift index 8f29fc6436..f29e9a8123 100644 --- a/iOSClient/Data/tableMetadata+SessionErrorMessage.swift +++ b/iOSClient/Data/tableMetadata+SessionErrorMessage.swift @@ -4,18 +4,14 @@ import Foundation import NextcloudKit +import NextcloudKitUI 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) - - if error.kind == .cancelled { - return NSLocalizedString("_error_transfer_cancelled_", comment: "") - } - let context = NCErrorContext(metadataStatus: status, fileName: fileNameView) - return error.userMessage(for: context) ?? sessionError + return error.userMessage(for: context) ?? error.localizedGenericMessage } } diff --git a/iOSClient/Networking/NKError+UserMessage.swift b/iOSClient/Networking/NKError+UserMessage.swift index d1cfb76d05..1dc39f2da9 100644 --- a/iOSClient/Networking/NKError+UserMessage.swift +++ b/iOSClient/Networking/NKError+UserMessage.swift @@ -4,12 +4,13 @@ import Foundation import NextcloudKit +import NextcloudKitUI extension NKError { /// The message to show people for this error, or `nil` when nothing should be shown (success or cancellation). /// - /// Tries the app's own text, the server's message when Nextcloud wrote it for people, a message for what was - /// being done, then one for the kind of failure. Keep `errorDescription` for logs. + /// 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 userMessage(for context: NCErrorContext = .generic) -> String? { guard let kind, kind != .cancelled, errorCode != NCGlobal.shared.errorTaskCancelled else { return nil } @@ -17,11 +18,11 @@ extension NKError { return appMessage } - if let userFacingServerMessage { - return userFacingServerMessage + if userFacingServerMessage == nil, let operationMessage = operationMessage(for: context, kind: kind) { + return operationMessage } - return contextMessage(for: context, kind: kind) ?? kindMessage(for: kind) + return localizedGenericMessage } /// Text the app wrote itself: a localization key, or the description of one of its own error codes. @@ -45,104 +46,19 @@ extension NKError { return isRejectedUpload && errorDescription.localizedCaseInsensitiveContains("virus") } - private func contextMessage(for context: NCErrorContext, kind: Kind) -> String? { + private func operationMessage(for context: NCErrorContext, kind: Kind) -> String? { switch (context, kind) { case (.upload, .unknown) where isVirusDetected: return NSLocalizedString("_virus_detect_", comment: "") - case (.upload, .forbidden), (.createFolder, .forbidden): - return NSLocalizedString("_no_file_no_permission_to_create_", comment: "") - case (.upload, .conflict) where errorCode == NCGlobal.shared.errorConflict: return NSLocalizedString("_error_upload_no_folder_", comment: "") - case (.download, .notFound): - return NSLocalizedString("_error_download_not_found_", comment: "") - - case (.delete, .forbidden): - return NSLocalizedString("_no_permission_delete_file_", comment: "") - - case (.delete, .locked): - return NSLocalizedString("_error_delete_locked_", comment: "") - - case (.rename, .forbidden), (.moveOrCopy, .forbidden): - return NSLocalizedString("_no_permission_modify_file_", comment: "") - - case (.rename, .alreadyExists): - return NSLocalizedString("_rename_already_exists_", comment: "") - - case (.moveOrCopy, .alreadyExists): - return NSLocalizedString("_file_already_exists_", comment: "") - - case (.createFolder, .alreadyExists): - return NSLocalizedString("_error_create_folder_exists_", comment: "") - case (.createFolder, .conflict): return NSLocalizedString("_error_create_folder_no_parent_", comment: "") - case (.favorite, .forbidden): - return NSLocalizedString("_no_permission_favorite_file_", comment: "") - default: return nil } } - - private func kindMessage(for kind: Kind) -> String { - switch kind { - case .offline: - return NSLocalizedString("_error_offline_", comment: "") - - case .timeout: - return NSLocalizedString("_error_timeout_", comment: "") - - case .serverUnreachable: - return NSLocalizedString("_error_server_unreachable_", comment: "") - - case .secureConnection: - return NSLocalizedString("_error_secure_connection_", comment: "") - - case .unauthorized: - return NSLocalizedString("_error_unauthorized_", comment: "") - - case .forbidden: - return NSLocalizedString("_error_forbidden_", comment: "") - - case .notFound: - return NSLocalizedString("_error_not_found_on_server_", comment: "") - - case .alreadyExists: - return NSLocalizedString("_error_already_exists_", comment: "") - - case .conflict: - return NSLocalizedString("_error_changed_on_server_", comment: "") - - case .locked: - return NSLocalizedString("_error_locked_", comment: "") - - case .quotaExceeded: - return NSLocalizedString("_error_quota_exceeded_", comment: "") - - case .tooLarge: - return NSLocalizedString("_error_too_large_", comment: "") - - case .rateLimited: - return NSLocalizedString("_error_rate_limited_", comment: "") - - case .unavailable: - return NSLocalizedString("_error_unavailable_", comment: "") - - case .serverError: - return NSLocalizedString("_error_server_error_", comment: "") - - case .invalidResponse: - return NSLocalizedString("_error_invalid_response_", comment: "") - - case .localStorageFull: - return NSLocalizedString("_error_local_storage_full_", comment: "") - - case .cancelled, .unknown: - return NSLocalizedString("_error_occurred_", comment: "") - } - } } diff --git a/iOSClient/Supporting Files/en.lproj/Localizable.strings b/iOSClient/Supporting Files/en.lproj/Localizable.strings index 30b84c3ffa..dba75f3a29 100644 --- a/iOSClient/Supporting Files/en.lproj/Localizable.strings +++ b/iOSClient/Supporting Files/en.lproj/Localizable.strings @@ -874,26 +874,5 @@ "_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_offline_" = "You're offline. Please check your internet connection."; -"_error_timeout_" = "The server took too long to respond. Try again later."; -"_error_server_unreachable_" = "Can't reach the server. Try again later."; -"_error_secure_connection_" = "A secure connection to the server couldn't be established."; -"_error_unauthorized_" = "Your login is no longer valid. Please log in again."; -"_error_forbidden_" = "You don't have permission to do this."; -"_error_not_found_on_server_" = "The item could not be found on the server."; -"_error_already_exists_" = "An item with this name already exists."; -"_error_changed_on_server_" = "The item was changed on the server. Refresh and try again."; -"_error_locked_" = "The file is locked. Try again later."; -"_error_quota_exceeded_" = "There is not enough storage space left on the server."; -"_error_too_large_" = "The file is too large for the server."; -"_error_rate_limited_" = "Too many requests. Wait a moment and try again."; -"_error_unavailable_" = "The server is temporarily unavailable. Try again later."; -"_error_server_error_" = "The server ran into a problem. Try again later."; -"_error_invalid_response_" = "The server sent an unexpected response. Try again later."; -"_error_local_storage_full_" = "There is not enough storage space left on this device."; -"_error_transfer_cancelled_" = "The transfer was cancelled."; "_error_upload_no_folder_" = "The folder you're uploading to doesn't exist anymore."; -"_error_download_not_found_" = "This file is no longer available on the server."; -"_error_delete_locked_" = "This file is locked and can't be deleted right now."; -"_error_create_folder_exists_" = "A folder with this name already exists."; "_error_create_folder_no_parent_" = "The parent folder doesn't exist."; From 776a023eebc5a4da1435388ead321f31186ff6c0 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Thu, 15 Oct 2026 13:55:25 +0200 Subject: [PATCH 3/7] WIP Signed-off-by: Milen Pivchev --- Nextcloud.xcodeproj/project.pbxproj | 8 -------- .../xcshareddata/swiftpm/Package.resolved | 2 +- iOSClient/Data/tableMetadata+SessionErrorMessage.swift | 1 - iOSClient/Networking/NKError+UserMessage.swift | 1 - 4 files changed, 1 insertion(+), 11 deletions(-) diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 6f1213b6b8..adf8f25a75 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -7,7 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - 6306971989EADE6757D44941 /* NextcloudKitUI in Frameworks */ = {isa = PBXBuildFile; productRef = 262894EB5592DF09FC34FCF9 /* NextcloudKitUI */; }; 22BA14432CF44BDBA2E6C291 /* NCDocumentEditorSupportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B74B947E07444CC9CFE0A6C /* NCDocumentEditorSupportTests.swift */; }; 27D0041A6F7CA1E970AD09B2 /* tableMetadata+SessionErrorMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = F840C79FAF28213971E832D4 /* tableMetadata+SessionErrorMessage.swift */; }; 2C1D5D7623E2DE3300334ABB /* NCManageDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7BAADB51ED5A87C00B7EAD4 /* NCManageDatabase.swift */; }; @@ -2213,7 +2212,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 6306971989EADE6757D44941 /* NextcloudKitUI in Frameworks */, F710FC80277B7D2700AA9FBF /* RealmSwift in Frameworks */, F70557B92ED44E4700135623 /* LucidBanner in Frameworks */, F72AD70F28C24BA1006CB92D /* NextcloudKit in Frameworks */, @@ -4153,7 +4151,6 @@ ); name = Share; packageProductDependencies = ( - 262894EB5592DF09FC34FCF9 /* NextcloudKitUI */, F72D7EB6263B1207000B3DFC /* MarkdownKit */, F710FC7F277B7D2700AA9FBF /* RealmSwift */, F72AD70E28C24BA1006CB92D /* NextcloudKit */, @@ -7287,11 +7284,6 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 262894EB5592DF09FC34FCF9 /* NextcloudKitUI */ = { - isa = XCSwiftPackageProductDependency; - package = F783034028B511D200B84583 /* XCRemoteSwiftPackageReference "NextcloudKit" */; - productName = NextcloudKitUI; - }; F3374AEF2D78B01B002A38F9 /* BitCollections */ = { isa = XCSwiftPackageProductDependency; package = F3374AEE2D78B01B002A38F9 /* XCRemoteSwiftPackageReference "swift-collections" */; diff --git a/Nextcloud.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Nextcloud.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 93df22b1f4..ede763f163 100644 --- a/Nextcloud.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Nextcloud.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -205,7 +205,7 @@ "location" : "https://github.com/nextcloud/NextcloudKit", "state" : { "branch" : "friendly-errors", - "revision" : "636d643d90c9eb041781798d198d4c6c22584080" + "revision" : "c1dfe63c65d869a864292b0b02330ef9f64c9cae" } }, { diff --git a/iOSClient/Data/tableMetadata+SessionErrorMessage.swift b/iOSClient/Data/tableMetadata+SessionErrorMessage.swift index f29e9a8123..56e9332a34 100644 --- a/iOSClient/Data/tableMetadata+SessionErrorMessage.swift +++ b/iOSClient/Data/tableMetadata+SessionErrorMessage.swift @@ -4,7 +4,6 @@ import Foundation import NextcloudKit -import NextcloudKitUI extension tableMetadata { /// The saved `sessionError`, worded for people from its `errorCode` and the operation the item failed at. diff --git a/iOSClient/Networking/NKError+UserMessage.swift b/iOSClient/Networking/NKError+UserMessage.swift index 1dc39f2da9..8541855ce7 100644 --- a/iOSClient/Networking/NKError+UserMessage.swift +++ b/iOSClient/Networking/NKError+UserMessage.swift @@ -4,7 +4,6 @@ import Foundation import NextcloudKit -import NextcloudKitUI extension NKError { /// The message to show people for this error, or `nil` when nothing should be shown (success or cancellation). From dfb2eb5cc45945d77c2b633e39e82f69c9c4bc0d Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Mon, 19 Oct 2026 18:43:07 +0200 Subject: [PATCH 4/7] WIP Signed-off-by: Milen Pivchev --- Tests/NextcloudUnitTests/NKErrorUserMessageTests.swift | 8 -------- iOSClient/Networking/NKError+UserMessage.swift | 2 +- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/Tests/NextcloudUnitTests/NKErrorUserMessageTests.swift b/Tests/NextcloudUnitTests/NKErrorUserMessageTests.swift index 1444594fbf..04af8689a2 100644 --- a/Tests/NextcloudUnitTests/NKErrorUserMessageTests.swift +++ b/Tests/NextcloudUnitTests/NKErrorUserMessageTests.swift @@ -28,14 +28,6 @@ struct NKErrorUserMessageTests { #expect(NKError(errorCode: NCGlobal.shared.errorE2EELock, errorDescription: "Already translated").userMessage() == "Already translated") } - @Test("Prefers the server's message when Nextcloud wrote it for people") - func userFacingServerMessage() { - let body = Data(#"{"ocs":{"meta":{"statuscode":403},"data":"Group sharing is disabled by the administrator"}}"#.utf8) - let error = NKError(errorCode: 403, errorDescription: "403: Forbidden", responseData: body) - - #expect(error.userMessage(for: .delete(fileName: "a.jpg")) == "Group sharing is disabled by the administrator") - } - @Test("Replaces technical server text") func technicalServerText() throws { let error = NKError(statusCode: 409, fallbackDescription: "Parent node does not exist") diff --git a/iOSClient/Networking/NKError+UserMessage.swift b/iOSClient/Networking/NKError+UserMessage.swift index 8541855ce7..26554c7b1c 100644 --- a/iOSClient/Networking/NKError+UserMessage.swift +++ b/iOSClient/Networking/NKError+UserMessage.swift @@ -17,7 +17,7 @@ extension NKError { return appMessage } - if userFacingServerMessage == nil, let operationMessage = operationMessage(for: context, kind: kind) { + if let operationMessage = operationMessage(for: context, kind: kind) { return operationMessage } From 30fdd3fde953c8afd836ebef0dae42f0370b5f46 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Tue, 20 Oct 2026 16:35:25 +0200 Subject: [PATCH 5/7] WIP Signed-off-by: Milen Pivchev --- Nextcloud.xcodeproj/project.pbxproj | 26 +++++------- .../xcshareddata/swiftpm/Package.resolved | 2 +- ... => NKErrorUserFriendlyMessageTests.swift} | 41 +++++++++---------- iOSClient/Activity/NCActivity.swift | 6 +-- .../Data/NCManageDatabase+Metadata.swift | 12 ++++++ .../tableMetadata+SessionErrorMessage.swift | 16 -------- .../GUI/Lucid Banner/ErrorBannerView.swift | 6 +-- .../NCCollectionViewCommon+Search.swift | 16 ++------ iOSClient/Main/Create/NCCreate.swift | 4 +- iOSClient/Menu/NCContextMenuComment.swift | 4 +- iOSClient/Networking/NCErrorContext.swift | 2 +- .../NCNetworking+TransferDelegate.swift | 5 ++- ...wift => NKError+UserFriendlyMessage.swift} | 5 ++- iOSClient/Notification/NCNotification.swift | 4 +- .../Settings/E2EE/NCManageE2EEModel.swift | 2 +- .../Settings/E2EE/NCManageE2EEView.swift | 9 ++-- .../en.lproj/Localizable.strings | 11 +---- .../NCTermOfServiceModel.swift | 2 +- iOSClient/Trash/NCTrash+Networking.swift | 4 +- .../NCViewerDirectEditing.swift | 4 +- .../NCViewerQuickLook/NCViewerQuickLook.swift | 5 ++- .../NCViewerRichdocuments.swift | 6 +-- 22 files changed, 81 insertions(+), 111 deletions(-) rename Tests/NextcloudUnitTests/{NKErrorUserMessageTests.swift => NKErrorUserFriendlyMessageTests.swift} (73%) delete mode 100644 iOSClient/Data/tableMetadata+SessionErrorMessage.swift rename iOSClient/Networking/{NKError+UserMessage.swift => NKError+UserFriendlyMessage.swift} (89%) diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index adf8f25a75..a2786bbbdd 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -8,16 +8,14 @@ /* Begin PBXBuildFile section */ 22BA14432CF44BDBA2E6C291 /* NCDocumentEditorSupportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B74B947E07444CC9CFE0A6C /* NCDocumentEditorSupportTests.swift */; }; - 27D0041A6F7CA1E970AD09B2 /* tableMetadata+SessionErrorMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = F840C79FAF28213971E832D4 /* tableMetadata+SessionErrorMessage.swift */; }; 2C1D5D7623E2DE3300334ABB /* NCManageDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7BAADB51ED5A87C00B7EAD4 /* NCManageDatabase.swift */; }; 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+UserMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79DFE0BDF3898A66910E67CA /* NKError+UserMessage.swift */; }; + 2EF7D3FD0885CE182717578E /* NKError+UserFriendlyMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79DFE0BDF3898A66910E67CA /* NKError+UserFriendlyMessage.swift */; }; 370D26AF248A3D7A00121797 /* NCCellMain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 370D26AE248A3D7A00121797 /* NCCellMain.swift */; }; - 52FF854678FFAD3269661979 /* tableMetadata+SessionErrorMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = F840C79FAF28213971E832D4 /* tableMetadata+SessionErrorMessage.swift */; }; 63563FE53573412998852DD3 /* NCAlbumsPersistenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 439616AAA50F4360A761E5EF /* NCAlbumsPersistenceTests.swift */; }; - 822D68B06B101563A759AD59 /* NKError+UserMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79DFE0BDF3898A66910E67CA /* NKError+UserMessage.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 */; }; A1B2C3D430B0000100ABCDEF /* NCMediaViewerLoadingPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D330B0000100ABCDEF /* NCMediaViewerLoadingPolicy.swift */; }; A1E2EE010000000000000002 /* NCEndToEndKeySet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1E2EE010000000000000001 /* NCEndToEndKeySet.swift */; }; @@ -117,7 +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 /* NKErrorUserMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 320C20803A58DAF7575440A8 /* NKErrorUserMessageTests.swift */; }; + BAF64949B487A080A98F83F2 /* NKErrorUserFriendlyMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 320C20803A58DAF7575440A8 /* NKErrorUserFriendlyMessageTests.swift */; }; C0DECA022F65000100C0D001 /* NCCameraRollTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DECA012F65000100C0D001 /* NCCameraRollTests.swift */; }; C0DEDA7E30C1000100A00002 /* NCAutoUploadDateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DEDA7E30C1000100A00001 /* NCAutoUploadDateTests.swift */; }; CB3666201AF7550816B5CD6A /* NCContextMenuComment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8932E90EC4278026D86CCCC9 /* NCContextMenuComment.swift */; }; @@ -1325,11 +1323,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 /* NKErrorUserMessageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NKErrorUserMessageTests.swift; 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+UserMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NKError+UserMessage.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 = ""; }; A1B2C3D330B0000100ABCDEF /* NCMediaViewerLoadingPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCMediaViewerLoadingPolicy.swift; sourceTree = ""; }; A1E2EE010000000000000001 /* NCEndToEndKeySet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCEndToEndKeySet.swift; sourceTree = ""; }; @@ -2139,7 +2137,6 @@ F7FDFF592E437E55000D7688 /* NCAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAccount.swift; sourceTree = ""; }; F7FF2CB02842159500EBB7A1 /* NCSectionHeader.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = NCSectionHeader.xib; sourceTree = ""; }; F7FFFC9D2FB300440015441E /* NCAssistantSharedTextStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAssistantSharedTextStore.swift; sourceTree = ""; }; - F840C79FAF28213971E832D4 /* tableMetadata+SessionErrorMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "tableMetadata+SessionErrorMessage.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ @@ -2366,7 +2363,7 @@ isa = PBXGroup; children = ( C0DEDA7E30C1000100A00001 /* NCAutoUploadDateTests.swift */, - 320C20803A58DAF7575440A8 /* NKErrorUserMessageTests.swift */, + 320C20803A58DAF7575440A8 /* NKErrorUserFriendlyMessageTests.swift */, F76BED7B306D0000002E1442 /* NCPhotoLibraryAssetTests.swift */, A1E2EE010000000000000009 /* NCEndToEndKeySetArchiveTests.swift */, A1E2EE020000000000000007 /* NCEndToEndKeySetResolverTests.swift */, @@ -2914,7 +2911,7 @@ F785129A2D79899E0087DDD0 /* NCNetworking+TermsOfService.swift */, F71916102E2901E800E13E96 /* NCNetworking+Upload.swift */, F7327E2F2B73A86700A462C7 /* NCNetworking+WebDAV.swift */, - 79DFE0BDF3898A66910E67CA /* NKError+UserMessage.swift */, + 79DFE0BDF3898A66910E67CA /* NKError+UserFriendlyMessage.swift */, CE0CD9A00D74B01055F238E9 /* NCErrorContext.swift */, F70D8D8024A4A9BF000A5756 /* NCNetworkingProcess.swift */, F755BD9A20594AC7008C5FBB /* NCService.swift */, @@ -3469,7 +3466,6 @@ F764C3E02FFB7DF800029FD5 /* NCManageDatabase+MediaMetadataBackfill.swift */, F7BDC1D1300F440600C5D9FA /* NCManageDatabase+MediaPreviewBackfill.swift */, AF4BF61827562A4B0081CEEF /* NCManageDatabase+Metadata.swift */, - F840C79FAF28213971E832D4 /* tableMetadata+SessionErrorMessage.swift */, F7B769A72B7A0B2000C1AAEB /* NCManageDatabase+Metadata+Session.swift */, F7C687E82D22BD46004757BC /* NCManageDatabase+RecommendedFiles.swift */, F7C9B91C2B582F550064EA91 /* NCManageDatabase+SecurityGuard.swift */, @@ -4809,7 +4805,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - BAF64949B487A080A98F83F2 /* NKErrorUserMessageTests.swift in Sources */, + BAF64949B487A080A98F83F2 /* NKErrorUserFriendlyMessageTests.swift in Sources */, C0DEDA7E30C1000100A00002 /* NCAutoUploadDateTests.swift in Sources */, F76BED7E306D0000002E1442 /* NCPhotoLibraryAssetTests.swift in Sources */, A1E2EE01000000000000000A /* NCEndToEndKeySetArchiveTests.swift in Sources */, @@ -4901,8 +4897,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 52FF854678FFAD3269661979 /* tableMetadata+SessionErrorMessage.swift in Sources */, - 2EF7D3FD0885CE182717578E /* NKError+UserMessage.swift in Sources */, + 2EF7D3FD0885CE182717578E /* NKError+UserFriendlyMessage.swift in Sources */, EB511D22564B7B9A605E70AE /* NCErrorContext.swift in Sources */, F7864ACF2A78FE73004870E0 /* NCManageDatabase+LocalFile.swift in Sources */, F71F6D0A2B6A6A5E00F1EB15 /* ThreadSafeArray.swift in Sources */, @@ -5166,8 +5161,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 27D0041A6F7CA1E970AD09B2 /* tableMetadata+SessionErrorMessage.swift in Sources */, - 822D68B06B101563A759AD59 /* NKError+UserMessage.swift in Sources */, + 822D68B06B101563A759AD59 /* NKError+UserFriendlyMessage.swift in Sources */, A358B569BF5FDABA6D9012D1 /* NCErrorContext.swift in Sources */, F76BED77306D0000002E1442 /* NCLocalDatabase.swift in Sources */, F76BED78306D0000002E1442 /* NCLocalDatabaseMigrator.swift in Sources */, diff --git a/Nextcloud.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Nextcloud.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index ede763f163..0747433320 100644 --- a/Nextcloud.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Nextcloud.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -205,7 +205,7 @@ "location" : "https://github.com/nextcloud/NextcloudKit", "state" : { "branch" : "friendly-errors", - "revision" : "c1dfe63c65d869a864292b0b02330ef9f64c9cae" + "revision" : "4ac49ab5fa4e5f3b112afff7658713d52dbaf593" } }, { diff --git a/Tests/NextcloudUnitTests/NKErrorUserMessageTests.swift b/Tests/NextcloudUnitTests/NKErrorUserFriendlyMessageTests.swift similarity index 73% rename from Tests/NextcloudUnitTests/NKErrorUserMessageTests.swift rename to Tests/NextcloudUnitTests/NKErrorUserFriendlyMessageTests.swift index 04af8689a2..bb77c5350a 100644 --- a/Tests/NextcloudUnitTests/NKErrorUserMessageTests.swift +++ b/Tests/NextcloudUnitTests/NKErrorUserFriendlyMessageTests.swift @@ -8,7 +8,7 @@ import Testing @testable import Nextcloud @Suite("Error messages shown to people") -struct NKErrorUserMessageTests { +struct NKErrorUserFriendlyMessageTests { static let shownKinds = NKError.Kind.allCases.filter { $0 != .cancelled } private func localized(_ key: String) -> String { @@ -17,23 +17,23 @@ struct NKErrorUserMessageTests { @Test("Nothing is shown for success or cancellation") func silentErrors() { - #expect(NKError.success.userMessage() == nil) - #expect(NKError.cancelled.userMessage(for: .upload(fileName: "a.jpg")) == nil) - #expect(NKError(errorCode: NCGlobal.shared.errorTaskCancelled, errorDescription: "Task cancelled").userMessage() == nil) + #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_").userMessage() == localized("_offline_not_allowed_")) - #expect(NKError(errorCode: NCGlobal.shared.errorE2EELock, errorDescription: "Already translated").userMessage() == "Already translated") + #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(statusCode: 409, fallbackDescription: "Parent node does not exist") - let generic = try #require(error.userMessage()) + let error = NKError(errorCode: 409, errorDescription: "Parent node does not exist") + let generic = try #require(error.userFriendlyMessage()) - #expect(error.userMessage(for: .createFolder(folderName: "a/b")) == localized("_error_create_folder_no_parent_")) + #expect(error.userFriendlyMessage(for: .createFolder(folderName: "a/b")) == localized("_error_create_folder_no_parent_")) #expect(generic != error.errorDescription) #expect(!generic.contains("409")) } @@ -41,38 +41,37 @@ struct NKErrorUserMessageTests { @Test("Uses the generic message unless the operation needs its own") func operationMessages() { let forbidden = NKError(errorCode: 403, errorDescription: "403: Forbidden") - let generic = forbidden.userMessage() + let generic = forbidden.userFriendlyMessage() #expect(generic != nil) - #expect(forbidden.userMessage(for: .delete(fileName: "a.jpg")) == generic) - #expect(forbidden.userMessage(for: .rename) == generic) - #expect(forbidden.userMessage(for: .upload(fileName: "a.jpg")) == generic) + #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("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.userMessage(for: .upload(fileName: "eicar.com")) == localized("_virus_detect_")) + #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.").userMessage() - let quota = NKError(errorCode: 507, errorDescription: "507: Storage quota is reached").userMessage() - let unknown = NKError(errorCode: 418, errorDescription: "418: I'm a teapot").userMessage() + 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: NKErrorUserMessageTests.shownKinds) + @Test("Every kind has a translated message", arguments: NKErrorUserFriendlyMessageTests.shownKinds) func everyKind(kind: NKError.Kind) { - let message = NKError(errorCode: -1, errorDescription: "Technical", kind: kind).userMessage() + let message = NKError(errorCode: -1, errorDescription: "Technical", kind: kind).userFriendlyMessage() #expect(message != nil) #expect(message != "Technical") - #expect(message?.hasPrefix("_") == false) } @Test("Maps transfer events and saved statuses to operations") @@ -108,7 +107,7 @@ struct NKErrorUserMessageTests { metadata.errorCode = 507 metadata.sessionError = "507: Storage quota is reached" - #expect(metadata.sessionErrorMessage == NKError(errorCode: 507, errorDescription: "").userMessage()) + #expect(metadata.sessionErrorMessage == NKError(errorCode: 507, errorDescription: "").userFriendlyMessage()) metadata.errorCode = NSURLErrorCancelled metadata.sessionError = "Transfer was cancelled." 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/Data/NCManageDatabase+Metadata.swift b/iOSClient/Data/NCManageDatabase+Metadata.swift index 471d86f832..226ec611c8 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/Data/tableMetadata+SessionErrorMessage.swift b/iOSClient/Data/tableMetadata+SessionErrorMessage.swift deleted file mode 100644 index 56e9332a34..0000000000 --- a/iOSClient/Data/tableMetadata+SessionErrorMessage.swift +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2026 Milen Pivchev -// SPDX-License-Identifier: GPL-3.0-or-later - -import Foundation -import NextcloudKit - -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.userMessage(for: context) ?? error.localizedGenericMessage - } -} diff --git a/iOSClient/GUI/Lucid Banner/ErrorBannerView.swift b/iOSClient/GUI/Lucid Banner/ErrorBannerView.swift index abe8ed7226..a77d1f778a 100644 --- a/iOSClient/GUI/Lucid Banner/ErrorBannerView.swift +++ b/iOSClient/GUI/Lucid Banner/ErrorBannerView.swift @@ -5,13 +5,12 @@ import SwiftUI import LucidBanner import NextcloudKit -import Alamofire @MainActor func showErrorBanner(windowScene: UIWindowScene?, error: NKError, context: NCErrorContext = .generic) async { - guard let text = error.userMessage(for: context) else { return } + guard let text = error.userFriendlyMessage(for: context) else { return } nkLog(error: "Error shown for \(context): \(error.errorCode) \(error.errorDescription)") @@ -26,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/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/Create/NCCreate.swift b/iOSClient/Main/Create/NCCreate.swift index 5217caab5f..0e03c5ab77 100644 --- a/iOSClient/Main/Create/NCCreate.swift +++ b/iOSClient/Main/Create/NCCreate.swift @@ -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/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/Networking/NCErrorContext.swift b/iOSClient/Networking/NCErrorContext.swift index 54dd62ff87..c2986e6097 100644 --- a/iOSClient/Networking/NCErrorContext.swift +++ b/iOSClient/Networking/NCErrorContext.swift @@ -46,7 +46,7 @@ enum NCErrorContext: Equatable { } } - /// The context of the operation an item is waiting for or failed at, from its metadata `status`. + /// 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 diff --git a/iOSClient/Networking/NCNetworking+TransferDelegate.swift b/iOSClient/Networking/NCNetworking+TransferDelegate.swift index 88c345ed0e..459156863b 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/NKError+UserMessage.swift b/iOSClient/Networking/NKError+UserFriendlyMessage.swift similarity index 89% rename from iOSClient/Networking/NKError+UserMessage.swift rename to iOSClient/Networking/NKError+UserFriendlyMessage.swift index 26554c7b1c..9d0841c666 100644 --- a/iOSClient/Networking/NKError+UserMessage.swift +++ b/iOSClient/Networking/NKError+UserFriendlyMessage.swift @@ -6,11 +6,11 @@ import Foundation import NextcloudKit extension NKError { - /// The message to show people for this error, or `nil` when nothing should be shown (success or cancellation). + /// 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 userMessage(for context: NCErrorContext = .generic) -> String? { + func userFriendlyMessage(for context: NCErrorContext = .generic) -> String? { guard let kind, kind != .cancelled, errorCode != NCGlobal.shared.errorTaskCancelled else { return nil } if let appMessage { @@ -45,6 +45,7 @@ extension NKError { 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: 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/Settings/E2EE/NCManageE2EEModel.swift b/iOSClient/Settings/E2EE/NCManageE2EEModel.swift index e303e86233..43f712bf67 100644 --- a/iOSClient/Settings/E2EE/NCManageE2EEModel.swift +++ b/iOSClient/Settings/E2EE/NCManageE2EEModel.swift @@ -145,7 +145,7 @@ class NCManageE2EE: NSObject, ObservableObject, ViewOnAppearHandling, TOPasscode } await showErrorBanner( windowScene: windowScene, - text: error.errorDescription + error: error ) } catch { // fallback (non NKError) 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 dba75f3a29..bdf5d3d480 100644 --- a/iOSClient/Supporting Files/en.lproj/Localizable.strings +++ b/iOSClient/Supporting Files/en.lproj/Localizable.strings @@ -104,9 +104,6 @@ "_recent_" = "Recent"; "_view_in_folder_" = "View in folder"; "_disabled_" = "Disabled"; -"_compact_" = "Compact"; -"_normal_" = "Normal"; -"_verbose_" = "Verbose"; "_error_occurred_" = "An error occurred, please try again."; "_retry_" = "Retry"; "_images_" = "Images"; @@ -592,8 +589,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"; @@ -706,13 +701,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."; 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/Trash/NCTrash+Networking.swift b/iOSClient/Trash/NCTrash+Networking.swift index df3279f41b..69fd577bcb 100644 --- a/iOSClient/Trash/NCTrash+Networking.swift +++ b/iOSClient/Trash/NCTrash+Networking.swift @@ -53,7 +53,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() @@ -68,7 +68,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/NCViewerDirectEditing/NCViewerDirectEditing.swift b/iOSClient/Viewer/NCViewerDirectEditing/NCViewerDirectEditing.swift index 367583b844..faf5905be4 100644 --- a/iOSClient/Viewer/NCViewerDirectEditing/NCViewerDirectEditing.swift +++ b/iOSClient/Viewer/NCViewerDirectEditing/NCViewerDirectEditing.swift @@ -387,7 +387,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 } @@ -515,7 +515,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/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) } } } From ce3dd5e8c2f22dd3d730cd34ba298b2984c6f390 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Mon, 26 Oct 2026 13:25:12 +0100 Subject: [PATCH 6/7] WIP Signed-off-by: Milen Pivchev --- Share/NCShareExtension.swift | 8 +++++--- iOSClient/Extensions/UIAlertController+Extension.swift | 2 +- iOSClient/Files/NCFiles.swift | 2 +- iOSClient/Menu/NCContextMenuMain.swift | 4 +++- iOSClient/Networking/NCNetworking+Upload.swift | 6 ++++-- iOSClient/Networking/NCNetworking+WebDAV.swift | 4 ++-- iOSClient/Supporting Files/en.lproj/Localizable.strings | 2 +- 7 files changed, 17 insertions(+), 11 deletions(-) diff --git a/Share/NCShareExtension.swift b/Share/NCShareExtension.swift index 65cf8eb0b7..b7d33036cb 100644 --- a/Share/NCShareExtension.swift +++ b/Share/NCShareExtension.swift @@ -349,8 +349,8 @@ class NCShareExtension: UIViewController { Task { await self.loadFolder() } - } else { - self.showAlert(title: "_error_createsubfolders_upload_", description: error.errorDescription) + } else if let message = error.userFriendlyMessage() { + 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/iOSClient/Extensions/UIAlertController+Extension.swift b/iOSClient/Extensions/UIAlertController+Extension.swift index 6b8a628966..5dc7c7471a 100644 --- a/iOSClient/Extensions/UIAlertController+Extension.swift +++ b/iOSClient/Extensions/UIAlertController+Extension.swift @@ -56,7 +56,7 @@ extension UIAlertController { if let banner { banner.dismiss() } - completion?(NKError(errorCode: createFolderResults.error.errorCode, errorDescription: createFolderResults.error.errorDescription)) + completion?(createFolderResults.error) } } } else if isDirectoryEncrypted { diff --git a/iOSClient/Files/NCFiles.swift b/iOSClient/Files/NCFiles.swift index de16570e39..950cb20549 100644 --- a/iOSClient/Files/NCFiles.swift +++ b/iOSClient/Files/NCFiles.swift @@ -380,7 +380,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/Menu/NCContextMenuMain.swift b/iOSClient/Menu/NCContextMenuMain.swift index 30b9617ce8..83a6084ea2 100644 --- a/iOSClient/Menu/NCContextMenuMain.swift +++ b/iOSClient/Menu/NCContextMenuMain.swift @@ -390,7 +390,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 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/Supporting Files/en.lproj/Localizable.strings b/iOSClient/Supporting Files/en.lproj/Localizable.strings index bb6f2d0228..50d5f1506e 100644 --- a/iOSClient/Supporting Files/en.lproj/Localizable.strings +++ b/iOSClient/Supporting Files/en.lproj/Localizable.strings @@ -876,7 +876,7 @@ "_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 item"; +"_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 “%@”"; From 54952d14eedc577c52d2e0d2c942cc940e844c9b Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Mon, 26 Oct 2026 16:53:25 +0100 Subject: [PATCH 7/7] WIP Signed-off-by: Milen Pivchev --- .../FileProviderExtension+Actions.swift | 4 +-- Share/NCShareExtension+Files.swift | 4 +-- Share/NCShareExtension.swift | 4 +-- .../NKErrorUserFriendlyMessageTests.swift | 19 +++++++++- Widget/Dashboard/DashboardData.swift | 2 +- Widget/Files/FilesData.swift | 2 +- iOSClient/Account/NCAccount.swift | 7 +++- .../Details/AlbumDetailsViewModel.swift | 17 ++++----- .../List/AlbumsListViewModel.swift | 9 ++--- .../UIAlertController+Extension.swift | 17 +++++---- iOSClient/Files/NCFiles.swift | 10 +++--- iOSClient/Login/NCLogin.swift | 23 ++++++++++-- ...ionViewCommon+CollectionViewDelegate.swift | 11 ++---- iOSClient/Main/Create/NCCreate.swift | 4 +-- iOSClient/Menu/NCContextMenuMain.swift | 16 +++------ iOSClient/Menu/NCContextMenuPlus.swift | 15 ++++---- .../Networking/E2EE/NCEndToEndMetadata.swift | 18 +++++++--- .../E2EE/NCEndToEndMetadataV1.swift | 36 ++++++++++++++----- .../E2EE/NCEndToEndMetadataV2.swift | 8 +++-- .../Networking/E2EE/NCNetworkingE2EE.swift | 21 +++++++---- iOSClient/Networking/NCErrorContext.swift | 14 ++++++-- .../NKError+UserFriendlyMessage.swift | 9 +++++ .../NCRichWorkspaceCoordinator.swift | 6 ++-- iOSClient/Select/NCSelect.swift | 6 ++-- iOSClient/Settings/E2EE/NCEndToEndSetup.swift | 15 ++++++++ .../Settings/E2EE/NCManageE2EEModel.swift | 8 ++--- .../en.lproj/Localizable.strings | 9 +++++ iOSClient/Viewer/NCViewer.swift | 2 +- 28 files changed, 206 insertions(+), 110 deletions(-) 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/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 b7d33036cb..f22b4564d1 100644 --- a/Share/NCShareExtension.swift +++ b/Share/NCShareExtension.swift @@ -344,12 +344,12 @@ 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 if let message = error.userFriendlyMessage() { + } else if let message = error.userFriendlyMessage(for: .createFolder(folderName: folderName)) { self.showAlert(title: "_error_createsubfolders_upload_", description: message) } } diff --git a/Tests/NextcloudUnitTests/NKErrorUserFriendlyMessageTests.swift b/Tests/NextcloudUnitTests/NKErrorUserFriendlyMessageTests.swift index bb77c5350a..8a20bee894 100644 --- a/Tests/NextcloudUnitTests/NKErrorUserFriendlyMessageTests.swift +++ b/Tests/NextcloudUnitTests/NKErrorUserFriendlyMessageTests.swift @@ -49,6 +49,22 @@ struct NKErrorUserFriendlyMessageTests { #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.") @@ -87,9 +103,10 @@ struct NKErrorUserFriendlyMessageTests { @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")] + 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 { 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/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/Extensions/UIAlertController+Extension.swift b/iOSClient/Extensions/UIAlertController+Extension.swift index 5dc7c7471a..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?(createFolderResults.error) + 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 950cb20549..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: "") ) } 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/NCCollectionViewCommon+CollectionViewDelegate.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegate.swift index 72cef04143..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 } } diff --git a/iOSClient/Main/Create/NCCreate.swift b/iOSClient/Main/Create/NCCreate.swift index 0e03c5ab77..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 } diff --git a/iOSClient/Menu/NCContextMenuMain.swift b/iOSClient/Menu/NCContextMenuMain.swift index 83a6084ea2..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) } } } @@ -501,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 index c2986e6097..339fd9b41e 100644 --- a/iOSClient/Networking/NCErrorContext.swift +++ b/iOSClient/Networking/NCErrorContext.swift @@ -14,6 +14,10 @@ enum NCErrorContext: Equatable { 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) { @@ -77,10 +81,10 @@ enum NCErrorContext: Equatable { } } - /// A title saying what failed, e.g. "Couldn't upload “IMG_0042.jpg”"; `nil` for `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: + case .generic, .login: return nil case .upload(let fileName): @@ -103,6 +107,12 @@ enum NCErrorContext: Equatable { 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/NKError+UserFriendlyMessage.swift b/iOSClient/Networking/NKError+UserFriendlyMessage.swift index 9d0841c666..78b0d446a9 100644 --- a/iOSClient/Networking/NKError+UserFriendlyMessage.swift +++ b/iOSClient/Networking/NKError+UserFriendlyMessage.swift @@ -57,6 +57,15 @@ extension NKError { 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/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 88669c4534..e0cd77d9a9 100644 --- a/iOSClient/Select/NCSelect.swift +++ b/iOSClient/Select/NCSelect.swift @@ -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/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 e47251389e..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) ) } } @@ -148,10 +147,9 @@ class NCManageE2EE: NSObject, ObservableObject, ViewOnAppearHandling, TOPasscode error: error ) } catch { - // fallback (non NKError) await showErrorBanner( windowScene: windowScene, - text: error.localizedDescription + error: NKError(error: error) ) } } diff --git a/iOSClient/Supporting Files/en.lproj/Localizable.strings b/iOSClient/Supporting Files/en.lproj/Localizable.strings index 50d5f1506e..e83977f6e9 100644 --- a/iOSClient/Supporting Files/en.lproj/Localizable.strings +++ b/iOSClient/Supporting Files/en.lproj/Localizable.strings @@ -776,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"; @@ -882,3 +885,9 @@ "_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/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 }