diff --git a/Localization/app.json b/Localization/app.json index e20e901db..a28b6e7f7 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -1,5 +1,25 @@ { "common": { + "errors": { + "item": { + "username": "username", + "email": "email", + "password": "password", + "agreement": "agreement", + "locale": "locale", + "reason": "reason" + }, + "ERR_BLOCKED": "is blocked", + "ERR_UNREACHABLE": "is unreachable", + "ERR_TAKEN": "is taken", + "ERR_RESERVED": "is reserved", + "ERR_ACCEPTED": "must be accepted", + "ERR_BLANK": "can't be blank", + "ERR_INVALID": "is invalid", + "ERR_TOO_LONG": "is too long", + "ERR_TOO_SHORT": "is too short", + "ERR_INCLUSION": "is inclusion" + }, "alerts": { "sign_up_failure": { "title": "Sign Up Failure" diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index e5429eeac..4b93b1f99 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -55,6 +55,7 @@ 2D61335825C188A000CAE157 /* APIService+Persist+Timeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335725C188A000CAE157 /* APIService+Persist+Timeline.swift */; }; 2D61335E25C1894B00CAE157 /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335D25C1894B00CAE157 /* APIService.swift */; }; 2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */; }; + 2D650FAB25ECDC9300851B58 /* Mastodon+Entidy+ErrorDetailReson.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D650FAA25ECDC9300851B58 /* Mastodon+Entidy+ErrorDetailReson.swift */; }; 2D69CFF425CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */; }; 2D69D00A25CAA00300C3A1B2 /* APIService+CoreData+Toot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D69D00925CAA00300C3A1B2 /* APIService+CoreData+Toot.swift */; }; 2D76316525C14BD100929FB9 /* PublicTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76316425C14BD100929FB9 /* PublicTimelineViewController.swift */; }; @@ -260,6 +261,7 @@ 2D5A3D6125CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeTimelineViewController+DebugAction.swift"; sourceTree = ""; }; 2D61335725C188A000CAE157 /* APIService+Persist+Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+Timeline.swift"; sourceTree = ""; }; 2D61335D25C1894B00CAE157 /* APIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIService.swift; sourceTree = ""; }; + 2D650FAA25ECDC9300851B58 /* Mastodon+Entidy+ErrorDetailReson.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entidy+ErrorDetailReson.swift"; sourceTree = ""; }; 2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadMoreConfigurableTableViewContainer.swift; sourceTree = ""; }; 2D69D00925CAA00300C3A1B2 /* APIService+CoreData+Toot.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+CoreData+Toot.swift"; sourceTree = ""; }; 2D76316425C14BD100929FB9 /* PublicTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicTimelineViewController.swift; sourceTree = ""; }; @@ -1007,6 +1009,7 @@ DB45FAD625CA6C76005A8AC7 /* UIBarButtonItem.swift */, 2D32EAB925CB9B0500C9ED86 /* UIView.swift */, 0FAA101B25E10E760017CCDE /* UIFont.swift */, + 2D650FAA25ECDC9300851B58 /* Mastodon+Entidy+ErrorDetailReson.swift */, ); path = Extension; sourceTree = ""; @@ -1502,6 +1505,7 @@ 2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */, DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */, 2D76319F25C1521200929FB9 /* StatusSection.swift in Sources */, + 2D650FAB25ECDC9300851B58 /* Mastodon+Entidy+ErrorDetailReson.swift in Sources */, DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */, DB084B5725CBC56C00F898ED /* Toot.swift in Sources */, DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */, diff --git a/Mastodon/Extension/Mastodon+Entidy+ErrorDetailReson.swift b/Mastodon/Extension/Mastodon+Entidy+ErrorDetailReson.swift new file mode 100644 index 000000000..c78e4dfc4 --- /dev/null +++ b/Mastodon/Extension/Mastodon+Entidy+ErrorDetailReson.swift @@ -0,0 +1,94 @@ +// +// Mastodon+Entidy+ErrorDetailReason.swift +// Mastodon +// +// Created by sxiaojian on 2021/3/1. +// +import MastodonSDK + +extension Mastodon.Entity.ErrorDetailReason { + func localizedDescription() -> String { + switch self.error { + case .ERR_BLOCKED: + return L10n.Common.Errors.errBlocked + case .ERR_UNREACHABLE: + return L10n.Common.Errors.errUnreachable + case .ERR_TAKEN: + return L10n.Common.Errors.errTaken + case .ERR_RESERVED: + return L10n.Common.Errors.errReserved + case .ERR_ACCEPTED: + return L10n.Common.Errors.errAccepted + case .ERR_BLANK: + return L10n.Common.Errors.errBlank + case .ERR_INVALID: + return L10n.Common.Errors.errInvalid + case .ERR_TOO_LONG: + return L10n.Common.Errors.errTooLong + case .ERR_TOO_SHORT: + return L10n.Common.Errors.errTooShort + case .ERR_INCLUSION: + return L10n.Common.Errors.errInclusion + case ._other: + return self.errorDescription ?? "" + } + } +} + +extension Mastodon.Entity.ErrorDetail { + func localizedDescription() -> String { + var messages: [String?] = [] + if let username = self.username { + if !username.isEmpty { + let errors = username.map { + L10n.Common.Errors.Item.username + " " + $0.localizedDescription() + } + messages.append(contentsOf: errors) + } + } + if let email = self.email { + if !email.isEmpty { + let errors = email.map { + L10n.Common.Errors.Item.email + " " + $0.localizedDescription() + } + messages.append(contentsOf: errors) + } + } + if let password = self.password { + if !password.isEmpty { + let errors = password.map { + L10n.Common.Errors.Item.password + " " + $0.localizedDescription() + } + messages.append(contentsOf: errors) + } + } + if let agreement = self.agreement { + if !agreement.isEmpty { + let errors = agreement.map { + L10n.Common.Errors.Item.agreement + " " + $0.localizedDescription() + } + messages.append(contentsOf: errors) + } + } + if let locale = self.locale { + if !locale.isEmpty { + let errors = locale.map { + L10n.Common.Errors.Item.locale + " " + $0.localizedDescription() + } + messages.append(contentsOf: errors) + } + } + if let reason = self.reason { + if !reason.isEmpty { + let errors = reason.map { + L10n.Common.Errors.Item.reason + " " + $0.localizedDescription() + } + messages.append(contentsOf: errors) + } + } + let message = messages + .compactMap { $0 } + .joined(separator: ", ") + return message + } +} diff --git a/Mastodon/Extension/UIAlertController.swift b/Mastodon/Extension/UIAlertController.swift index 83c0ff555..755acc1ae 100644 --- a/Mastodon/Extension/UIAlertController.swift +++ b/Mastodon/Extension/UIAlertController.swift @@ -4,7 +4,7 @@ // import UIKit - +import MastodonSDK // Reference: // https://nshipster.com/swift-foundation-error-protocols/ extension UIAlertController { @@ -43,3 +43,43 @@ extension UIAlertController { } } +extension UIAlertController { + convenience init( + for error: Mastodon.API.Error, + title: String?, + preferredStyle: UIAlertController.Style + ) { + let _title: String + let message: String? + switch error.mastodonError { + case .generic(let mastodonEntityError): + + if let title = title { + _title = title + } else { + _title = error.errorDescription ?? "Error" + } + var messages: [String?] = [] + if let details = mastodonEntityError.details { + message = details.localizedDescription() + } else { + messages.append(contentsOf: [ + error.failureReason, + error.recoverySuggestion + ]) + message = messages + .compactMap { $0 } + .joined(separator: " ") + } + default: + _title = "Internal Error" + message = error.localizedDescription + } + + self.init( + title: _title, + message: message, + preferredStyle: preferredStyle + ) + } +} diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index 47cbabab8..ce4ab38f8 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -82,6 +82,42 @@ internal enum L10n { internal static let single = L10n.tr("Localizable", "Common.Countable.Photo.Single") } } + internal enum Errors { + /// must be accepted + internal static let errAccepted = L10n.tr("Localizable", "Common.Errors.ErrAccepted") + /// can't be blank + internal static let errBlank = L10n.tr("Localizable", "Common.Errors.ErrBlank") + /// is blocked + internal static let errBlocked = L10n.tr("Localizable", "Common.Errors.ErrBlocked") + /// is inclusion + internal static let errInclusion = L10n.tr("Localizable", "Common.Errors.ErrInclusion") + /// is invalid + internal static let errInvalid = L10n.tr("Localizable", "Common.Errors.ErrInvalid") + /// is reserved + internal static let errReserved = L10n.tr("Localizable", "Common.Errors.ErrReserved") + /// is taken + internal static let errTaken = L10n.tr("Localizable", "Common.Errors.ErrTaken") + /// is too long + internal static let errTooLong = L10n.tr("Localizable", "Common.Errors.ErrTooLong") + /// is too short + internal static let errTooShort = L10n.tr("Localizable", "Common.Errors.ErrTooShort") + /// is unreachable + internal static let errUnreachable = L10n.tr("Localizable", "Common.Errors.ErrUnreachable") + internal enum Item { + /// agreement + internal static let agreement = L10n.tr("Localizable", "Common.Errors.Item.Agreement") + /// email + internal static let email = L10n.tr("Localizable", "Common.Errors.Item.Email") + /// locale + internal static let locale = L10n.tr("Localizable", "Common.Errors.Item.Locale") + /// password + internal static let password = L10n.tr("Localizable", "Common.Errors.Item.Password") + /// reason + internal static let reason = L10n.tr("Localizable", "Common.Errors.Item.Reason") + /// username + internal static let username = L10n.tr("Localizable", "Common.Errors.Item.Username") + } + } } internal enum Scene { diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index b3df9a77f..663ad25ad 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -23,6 +23,22 @@ "Common.Controls.Timeline.LoadMore" = "Load More"; "Common.Countable.Photo.Multiple" = "photos"; "Common.Countable.Photo.Single" = "photo"; +"Common.Errors.ErrAccepted" = "must be accepted"; +"Common.Errors.ErrBlank" = "can't be blank"; +"Common.Errors.ErrBlocked" = "is blocked"; +"Common.Errors.ErrInclusion" = "is inclusion"; +"Common.Errors.ErrInvalid" = "is invalid"; +"Common.Errors.ErrReserved" = "is reserved"; +"Common.Errors.ErrTaken" = "is taken"; +"Common.Errors.ErrTooLong" = "is too long"; +"Common.Errors.ErrTooShort" = "is too short"; +"Common.Errors.ErrUnreachable" = "is unreachable"; +"Common.Errors.Item.Agreement" = "agreement"; +"Common.Errors.Item.Email" = "email"; +"Common.Errors.Item.Locale" = "locale"; +"Common.Errors.Item.Password" = "password"; +"Common.Errors.Item.Reason" = "reason"; +"Common.Errors.Item.Username" = "username"; "Scene.ConfirmEmail.Button.DontReceiveEmail" = "I never got an email"; "Scene.ConfirmEmail.Button.OpenEmailApp" = "Open Email App"; "Scene.ConfirmEmail.DontReceiveEmail.Description" = "Check if your email address is correct as well as your junk folder if you haven’t."; @@ -62,4 +78,4 @@ any server."; "Scene.ServerRules.Subtitle" = "These rules are set by the admins of %@."; "Scene.ServerRules.Title" = "Some ground rules."; "Scene.Welcome.Slogan" = "Social networking -back in your hands."; +back in your hands."; \ No newline at end of file diff --git a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift index ff979c3dd..9bdaafd03 100644 --- a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift +++ b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift @@ -105,6 +105,20 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O let usernameIsTakenLabel: UILabel = { let label = UILabel() + let color = Asset.Colors.lightDangerRed.color + let font = UIFont.preferredFont(forTextStyle: .caption1) + let attributeString = NSMutableAttributedString() + + let errorImage = NSTextAttachment() + let configuration = UIImage.SymbolConfiguration(font: font) + errorImage.image = UIImage(systemName: "xmark.octagon.fill", withConfiguration: configuration)?.withTintColor(color) + let errorImageAttachment = NSAttributedString(attachment: errorImage) + attributeString.append(errorImageAttachment) + + let errorString = NSAttributedString(string: L10n.Common.Errors.Item.username + " " + L10n.Common.Errors.errTaken, attributes: [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: color]) + attributeString.append(errorString) + label.attributedText = attributeString + return label }() @@ -392,6 +406,7 @@ extension MastodonRegisterViewController { .receive(on: DispatchQueue.main) .sink { [weak self] error in guard let self = self else { return } + guard let error = error as? Mastodon.API.Error else { return } let alertController = UIAlertController(for: error, title: "Sign Up Failure", preferredStyle: .alert) let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil) alertController.addAction(okAction) diff --git a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift index cc7992e21..3886bda7c 100644 --- a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift +++ b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift @@ -8,6 +8,7 @@ import os.log import UIKit import Combine +import MastodonSDK final class MastodonServerRulesViewController: UIViewController, NeedsDependency { @@ -162,6 +163,7 @@ extension MastodonServerRulesViewController { .receive(on: DispatchQueue.main) .sink { [weak self] error in guard let self = self else { return } + guard let error = error as? Mastodon.API.Error else { return } let alertController = UIAlertController(for: error, title: "Sign Up Failure", preferredStyle: .alert) let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil) alertController.addAction(okAction) diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+ErrorDetail.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+ErrorDetail.swift index 397475e0c..4d90779bb 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+ErrorDetail.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+ErrorDetail.swift @@ -65,37 +65,38 @@ extension Mastodon.Entity.Error { } } } +extension Mastodon.Entity { + public struct ErrorDetail: Codable { + public let username: [ErrorDetailReason]? + public let email: [ErrorDetailReason]? + public let password: [ErrorDetailReason]? + public let agreement: [ErrorDetailReason]? + public let locale: [ErrorDetailReason]? + public let reason: [ErrorDetailReason]? -public struct ErrorDetail: Codable { - public let username: [ErrorDetailReson]? - public let email: [ErrorDetailReson]? - public let password: [ErrorDetailReson]? - public let agreement: [ErrorDetailReson]? - public let locale: [ErrorDetailReson]? - public let reason: [ErrorDetailReson]? + enum CodingKeys: String, CodingKey { + case username + case email + case password + case agreement + case locale + case reason + } + } - enum CodingKeys: String, CodingKey { - case username - case email - case password - case agreement - case locale - case reason - } -} - -public struct ErrorDetailReson: Codable { - public init(error: String, errorDescription: String?) { - self.error = Mastodon.Entity.Error.SignUpError(rawValue: error) ?? ._other(error) - self.errorDescription = errorDescription - } - - public let error: Mastodon.Entity.Error.SignUpError - public let errorDescription: String? - - - enum CodingKeys: String, CodingKey { - case error - case errorDescription = "description" + public struct ErrorDetailReason: Codable { + public init(error: String, errorDescription: String?) { + self.error = Mastodon.Entity.Error.SignUpError(rawValue: error) ?? ._other(error) + self.errorDescription = errorDescription + } + + public let error: Mastodon.Entity.Error.SignUpError + public let errorDescription: String? + + + enum CodingKeys: String, CodingKey { + case error + case errorDescription = "description" + } } }