From 8980373b066e88d1e65ddd616eacb8e50d39ba5a Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Wed, 14 Oct 2026 16:58:20 +0200 Subject: [PATCH 1/5] 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/5] 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/5] 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/5] 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/5] 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) } } }