diff --git a/Localization/app.json b/Localization/app.json index e20e901d..3a45922a 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -1,5 +1,31 @@ { "common": { + "errors": { + "item": { + "username": "username", + "email": "email", + "password": "password", + "agreement": "agreement", + "locale": "locale", + "reason": "reason" + }, + "itemDetail": { + "email_invalid": "This is not a valid e-mail address", + "username_invalid": "Username must only contain alphanumeric characters and underscores", + "password_too_shrot": "password is too short (must be at least 8 characters)", + "username_too_long": "username is too long (can't be longer than 30 characters)" + }, + "ERR_BLOCKED": "contains a disallowed e-mail provider", + "ERR_UNREACHABLE": "does not seem to exist", + "ERR_TAKEN": "is already in use", + "ERR_RESERVED": "is a reserved keyword", + "ERR_ACCEPTED": "must be accepted", + "ERR_BLANK": "is required", + "ERR_INVALID": "is invalid", + "ERR_TOO_LONG": "is too long", + "ERR_TOO_SHORT": "is too short", + "ERR_INCLUSION": "is not a supported value" + }, "alerts": { "sign_up_failure": { "title": "Sign Up Failure" diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index e611a693..8878f806 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+ErrorDetailReason.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D650FAA25ECDC9300851B58 /* Mastodon+Entidy+ErrorDetailReason.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 */; }; @@ -70,6 +71,7 @@ 2D927F0825C7E9A8004F19B8 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F0725C7E9A8004F19B8 /* Tag.swift */; }; 2D927F0E25C7E9C9004F19B8 /* History.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F0D25C7E9C9004F19B8 /* History.swift */; }; 2D927F1425C7EDD9004F19B8 /* Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F1325C7EDD9004F19B8 /* Emoji.swift */; }; + 2D939AB525EDD8A90076FA61 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D939AB425EDD8A90076FA61 /* String.swift */; }; 2DA7D04425CA52B200804E11 /* TimelineLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D04325CA52B200804E11 /* TimelineLoaderTableViewCell.swift */; }; 2DA7D04A25CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */; }; 2DA7D05725CA693F00804E11 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D05625CA693F00804E11 /* Application.swift */; }; @@ -261,6 +263,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+ErrorDetailReason.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entidy+ErrorDetailReason.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 = ""; }; @@ -276,6 +279,7 @@ 2D927F0725C7E9A8004F19B8 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = ""; }; 2D927F0D25C7E9C9004F19B8 /* History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = History.swift; sourceTree = ""; }; 2D927F1325C7EDD9004F19B8 /* Emoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Emoji.swift; sourceTree = ""; }; + 2D939AB425EDD8A90076FA61 /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; 2DA7D04325CA52B200804E11 /* TimelineLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineLoaderTableViewCell.swift; sourceTree = ""; }; 2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineBottomLoaderTableViewCell.swift; sourceTree = ""; }; 2DA7D05025CA545E00804E11 /* LoadMoreConfigurableTableViewContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadMoreConfigurableTableViewContainer.swift; sourceTree = ""; }; @@ -1010,6 +1014,8 @@ DB45FAD625CA6C76005A8AC7 /* UIBarButtonItem.swift */, 2D32EAB925CB9B0500C9ED86 /* UIView.swift */, 0FAA101B25E10E760017CCDE /* UIFont.swift */, + 2D650FAA25ECDC9300851B58 /* Mastodon+Entidy+ErrorDetailReason.swift */, + 2D939AB425EDD8A90076FA61 /* String.swift */, ); path = Extension; sourceTree = ""; @@ -1469,6 +1475,7 @@ DB45FAE325CA7181005A8AC7 /* MastodonUser.swift in Sources */, DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */, 2D69D00A25CAA00300C3A1B2 /* APIService+CoreData+Toot.swift in Sources */, + 2D939AB525EDD8A90076FA61 /* String.swift in Sources */, 0FAA101C25E10E760017CCDE /* UIFont.swift in Sources */, 2D38F1D525CD465300561493 /* HomeTimelineViewController.swift in Sources */, DB98338825C945ED00AD9700 /* Assets.swift in Sources */, @@ -1514,6 +1521,7 @@ 2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */, DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */, 2D76319F25C1521200929FB9 /* StatusSection.swift in Sources */, + 2D650FAB25ECDC9300851B58 /* Mastodon+Entidy+ErrorDetailReason.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+ErrorDetailReason.swift b/Mastodon/Extension/Mastodon+Entidy+ErrorDetailReason.swift new file mode 100644 index 00000000..cc1a4790 --- /dev/null +++ b/Mastodon/Extension/Mastodon+Entidy+ErrorDetailReason.swift @@ -0,0 +1,99 @@ +// +// Mastodon+Entity+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, !username.isEmpty { + let errors = username.map { errorDetailReason -> String in + switch errorDetailReason.error { + case .ERR_INVALID: + return L10n.Common.Errors.Itemdetail.usernameInvalid + case .ERR_TOO_LONG: + return L10n.Common.Errors.Itemdetail.usernameTooLong + default: + return L10n.Common.Errors.Item.username + " " + errorDetailReason.localizedDescription() + } + } + messages.append(contentsOf: errors) + } + + if let email = self.email, !email.isEmpty { + let errors = email.map { errorDetailReason -> String in + if errorDetailReason.error == .ERR_INVALID { + return L10n.Common.Errors.Itemdetail.emailInvalid + } else { + return L10n.Common.Errors.Item.email + " " + errorDetailReason.localizedDescription() + } + } + messages.append(contentsOf: errors) + } + if let password = self.password,!password.isEmpty { + let errors = password.map { errorDetailReason -> String in + if errorDetailReason.error == .ERR_TOO_SHORT { + return L10n.Common.Errors.Itemdetail.passwordTooShrot + } else { + return L10n.Common.Errors.Item.password + " " + errorDetailReason.localizedDescription() + } + } + messages.append(contentsOf: errors) + } + if let agreement = self.agreement, !agreement.isEmpty { + let errors = agreement.map { + L10n.Common.Errors.Item.agreement + " " + $0.localizedDescription() + } + messages.append(contentsOf: errors) + } + if let locale = self.locale, !locale.isEmpty { + let errors = locale.map { + L10n.Common.Errors.Item.locale + " " + $0.localizedDescription() + } + messages.append(contentsOf: errors) + } + if let reason = self.reason, !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.capitalizingFirstLetter() + } +} diff --git a/Mastodon/Extension/String.swift b/Mastodon/Extension/String.swift new file mode 100644 index 00000000..87028ffd --- /dev/null +++ b/Mastodon/Extension/String.swift @@ -0,0 +1,18 @@ +// +// String.swift +// Mastodon +// +// Created by sxiaojian on 2021/3/2. +// + +import Foundation + +extension String { + func capitalizingFirstLetter() -> String { + return prefix(1).capitalized + dropFirst() + } + + mutating func capitalizeFirstLetter() { + self = self.capitalizingFirstLetter() + } +} diff --git a/Mastodon/Extension/UIAlertController.swift b/Mastodon/Extension/UIAlertController.swift index 83c0ff55..755acc1a 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 47cbabab..8e93c804 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -82,6 +82,52 @@ 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") + /// is required + internal static let errBlank = L10n.tr("Localizable", "Common.Errors.ErrBlank") + /// contains a disallowed e-mail provider + internal static let errBlocked = L10n.tr("Localizable", "Common.Errors.ErrBlocked") + /// is not a supported value + internal static let errInclusion = L10n.tr("Localizable", "Common.Errors.ErrInclusion") + /// is invalid + internal static let errInvalid = L10n.tr("Localizable", "Common.Errors.ErrInvalid") + /// is a reserved keyword + internal static let errReserved = L10n.tr("Localizable", "Common.Errors.ErrReserved") + /// is already in use + 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") + /// does not seem to exist + 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 Itemdetail { + /// This is not a valid e-mail address + internal static let emailInvalid = L10n.tr("Localizable", "Common.Errors.Itemdetail.EmailInvalid") + /// password is too short (must be at least 8 characters) + internal static let passwordTooShrot = L10n.tr("Localizable", "Common.Errors.Itemdetail.PasswordTooShrot") + /// Username must only contain alphanumeric characters and underscores + internal static let usernameInvalid = L10n.tr("Localizable", "Common.Errors.Itemdetail.UsernameInvalid") + /// username is too long ( can't be longer than 30 characters) + internal static let usernameTooLong = L10n.tr("Localizable", "Common.Errors.Itemdetail.UsernameTooLong") + } + } } internal enum Scene { diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index b3df9a77..191ec0da 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -23,6 +23,26 @@ "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" = "is required"; +"Common.Errors.ErrBlocked" = "contains a disallowed e-mail provider"; +"Common.Errors.ErrInclusion" = "is not a supported value"; +"Common.Errors.ErrInvalid" = "is invalid"; +"Common.Errors.ErrReserved" = "is a reserved keyword"; +"Common.Errors.ErrTaken" = "is already in use"; +"Common.Errors.ErrTooLong" = "is too long"; +"Common.Errors.ErrTooShort" = "is too short"; +"Common.Errors.ErrUnreachable" = "does not seem to exist"; +"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"; +"Common.Errors.Itemdetail.EmailInvalid" = "This is not a valid e-mail address"; +"Common.Errors.Itemdetail.PasswordTooShrot" = "password is too short (must be at least 8 characters)"; +"Common.Errors.Itemdetail.UsernameInvalid" = "Username must only contain alphanumeric characters and underscores"; +"Common.Errors.Itemdetail.UsernameTooLong" = "username is too long ( can't be longer than 30 characters)"; "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 +82,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/PickServer/MastodonPickServerViewController.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift index 9e10cd32..909d6ec7 100644 --- a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift +++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift @@ -103,7 +103,9 @@ extension MastodonPickServerViewController { .sink { _ in } receiveValue: { [weak self] servers in + self?.tableView.beginUpdates() self?.tableView.reloadSections(IndexSet(integer: 3), with: .automatic) + self?.tableView.endUpdates() if let selectedServer = self?.viewModel.selectedServer.value, servers.contains(selectedServer) { // Previously selected server is still in the list, do nothing } else { @@ -265,13 +267,25 @@ extension MastodonPickServerViewController { } } receiveValue: { [weak self] response in guard let self = self else { return } - let mastodonRegisterViewModel = MastodonRegisterViewModel( - domain: server.domain, - authenticateInfo: response.authenticateInfo, - instance: response.instance.value, - applicationToken: response.applicationToken.value - ) - self.coordinator.present(scene: .mastodonRegister(viewModel: mastodonRegisterViewModel), from: nil, transition: .show) + if let rules = response.instance.value.rules, !rules.isEmpty { + // show server rules before register + let mastodonServerRulesViewModel = MastodonServerRulesViewModel( + domain: server.domain, + authenticateInfo: response.authenticateInfo, + rules: rules, + instance: response.instance.value, + applicationToken: response.applicationToken.value + ) + self.coordinator.present(scene: .mastodonServerRules(viewModel: mastodonServerRulesViewModel), from: self, transition: .show) + } else { + let mastodonRegisterViewModel = MastodonRegisterViewModel( + domain: server.domain, + authenticateInfo: response.authenticateInfo, + instance: response.instance.value, + applicationToken: response.applicationToken.value + ) + self.coordinator.present(scene: .mastodonRegister(viewModel: mastodonRegisterViewModel), from: nil, transition: .show) + } } .store(in: &disposeBag) } @@ -291,8 +305,7 @@ extension MastodonPickServerViewController: UITableViewDelegate { // Same reason as above return 10 case .serverList: - // Header with 1 height as the separator - return 1 + return 0 } } diff --git a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift index 3a701f09..a3b2a876 100644 --- a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift +++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift @@ -80,8 +80,6 @@ class MastodonPickServerViewModel: NSObject { weak var tableView: UITableView? -// private var expandServerDomainSet = Set() - var mastodonPinBasedAuthenticationViewController: UIViewController? init(context: AppContext, mode: PickServerMode) { diff --git a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift index 71182218..0ded9392 100644 --- a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift +++ b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift @@ -231,7 +231,7 @@ extension PickServerCell { // Set bottom separator seperator.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), containerView.trailingAnchor.constraint(equalTo: seperator.trailingAnchor), - containerView.bottomAnchor.constraint(equalTo: seperator.bottomAnchor), + containerView.topAnchor.constraint(equalTo: seperator.topAnchor), seperator.heightAnchor.constraint(equalToConstant: 1).priority(.defaultHigh), domainLabel.topAnchor.constraint(equalTo: containerView.layoutMarginsGuide.topAnchor), diff --git a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift index ff979c3d..9931a8b1 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 }() @@ -227,11 +241,12 @@ extension MastodonRegisterViewController { stackView.addArrangedSubview(largeTitleLabel) stackView.addArrangedSubview(photoView) stackView.addArrangedSubview(usernameTextField) + stackView.addArrangedSubview(usernameIsTakenLabel) stackView.addArrangedSubview(displayNameTextField) stackView.addArrangedSubview(emailTextField) stackView.addArrangedSubview(passwordTextField) stackView.addArrangedSubview(passwordCheckLabel) - if self.viewModel.approvalRequired { + if viewModel.approvalRequired { stackView.addArrangedSubview(inviteTextField) } // scrollView @@ -375,23 +390,48 @@ extension MastodonRegisterViewController { guard let self = self else { return } self.setTextFieldValidAppearance(self.passwordTextField, validateState: validateState) self.passwordCheckLabel.attributedText = self.viewModel.attributeStringForPassword(eightCharacters: validateState == .valid) - } .store(in: &disposeBag) viewModel.isAllValid - .receive(on: DispatchQueue.main) - .sink { [weak self] isAllValid in - guard let self = self else { return } - self.signUpButton.isEnabled = isAllValid - } - .store(in: &disposeBag) + .receive(on: DispatchQueue.main) + .sink { [weak self] isAllValid in + guard let self = self else { return } + self.signUpButton.isEnabled = isAllValid + } + .store(in: &disposeBag) + viewModel.isUsernameTaken + .receive(on: DispatchQueue.main) + .sink {[weak self] isUsernameTaken in + guard let self = self else { return } + if isUsernameTaken { + self.usernameIsTakenLabel.isHidden = false + stackView.setCustomSpacing(6, after: self.usernameTextField) + stackView.setCustomSpacing(16, after: self.usernameIsTakenLabel) + } else { + self.usernameIsTakenLabel.isHidden = true + stackView.setCustomSpacing(40, after: self.usernameTextField) + } + } + .store(in: &disposeBag) viewModel.error .compactMap { $0 } .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 } + switch error.mastodonError { + case .generic(let mastodonEntityError): + if let usernameTakenError = mastodonEntityError.details?.username { + let isUsernameAvaliable = usernameTakenError.filter { errorDetailReason -> Bool in + errorDetailReason.error == .ERR_TAKEN + }.isEmpty + self.viewModel.isUsernameTaken.value = !isUsernameAvaliable + } + default: + break + } 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) @@ -439,7 +479,7 @@ extension MastodonRegisterViewController { } .store(in: &disposeBag) - if self.viewModel.approvalRequired { + if viewModel.approvalRequired { inviteTextField.delegate = self NSLayoutConstraint.activate([ @@ -536,45 +576,29 @@ extension MastodonRegisterViewController { locale: "en" // TODO: ) - if let rules = viewModel.instance.rules, !rules.isEmpty { - // show server rules before register - let mastodonServerRulesViewModel = MastodonServerRulesViewModel( - context: context, - domain: viewModel.domain, - authenticateInfo: viewModel.authenticateInfo, - rules: rules, - registerQuery: query, - applicationAuthorization: viewModel.applicationAuthorization - ) - - viewModel.isRegistering.value = false - view.endEditing(true) - coordinator.present(scene: .mastodonServerRules(viewModel: mastodonServerRulesViewModel), from: self, transition: .show) - return - } else { - // register without show server rules - context.apiService.accountRegister( - domain: viewModel.domain, - query: query, - authorization: viewModel.applicationAuthorization - ) - .receive(on: DispatchQueue.main) - .sink { [weak self] completion in - guard let self = self else { return } - self.viewModel.isRegistering.value = false - switch completion { - case .failure(let error): - self.viewModel.error.send(error) - case .finished: - break - } - } receiveValue: { [weak self] response in - guard let self = self else { return } - let userToken = response.value - let viewModel = MastodonConfirmEmailViewModel(context: self.context, email: email, authenticateInfo: self.viewModel.authenticateInfo, userToken: userToken) - self.coordinator.present(scene: .mastodonConfirmEmail(viewModel: viewModel), from: self, transition: .show) + // register without show server rules + context.apiService.accountRegister( + domain: viewModel.domain, + query: query, + authorization: viewModel.applicationAuthorization + ) + .receive(on: DispatchQueue.main) + .sink { [weak self] completion in + guard let self = self else { return } + self.viewModel.isRegistering.value = false + switch completion { + case .failure(let error): + self.viewModel.error.send(error) + case .finished: + break } - .store(in: &disposeBag) + } receiveValue: { [weak self] response in + guard let self = self else { return } + let userToken = response.value + let viewModel = MastodonConfirmEmailViewModel(context: self.context, email: email, authenticateInfo: self.viewModel.authenticateInfo, userToken: userToken) + self.coordinator.present(scene: .mastodonConfirmEmail(viewModel: viewModel), from: self, transition: .show) } + .store(in: &disposeBag) + } } diff --git a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift index 5a909834..a32e5d04 100644 --- a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift +++ b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift @@ -34,6 +34,8 @@ final class MastodonRegisterViewModel { let passwordValidateState = CurrentValueSubject(.empty) let inviteValidateState = CurrentValueSubject(.empty) + let isUsernameTaken = CurrentValueSubject(false) + let isRegistering = CurrentValueSubject(false) let isAllValid = CurrentValueSubject(false) let error = CurrentValueSubject(nil) @@ -158,7 +160,7 @@ extension MastodonRegisterViewModel { let falseColor = UIColor.clear let attributeString = NSMutableAttributedString() - let start = NSAttributedString(string: "Your password needs at least:\n", attributes: [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: color]) + let start = NSAttributedString(string: "Your password needs at least:", attributes: [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: color]) attributeString.append(start) attributeString.append(checkmarkImage(color: eightCharacters ? color : falseColor)) diff --git a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift index cc7992e2..467239b8 100644 --- a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift +++ b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift @@ -148,30 +148,6 @@ extension MastodonServerRulesViewController { rulesLabel.attributedText = viewModel.rulesAttributedString confirmButton.addTarget(self, action: #selector(MastodonServerRulesViewController.confirmButtonPressed(_:)), for: .touchUpInside) - - viewModel.isRegistering - .receive(on: DispatchQueue.main) - .sink { [weak self] isRegistering in - guard let self = self else { return } - isRegistering ? self.confirmButton.showLoading() : self.confirmButton.stopLoading() - } - .store(in: &disposeBag) - - viewModel.error - .compactMap { $0 } - .receive(on: DispatchQueue.main) - .sink { [weak self] error in - guard let self = self 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) - self.coordinator.present( - scene: .alertController(alertController: alertController), - from: nil, - transition: .alertController(animated: true, completion: nil) - ) - } - .store(in: &disposeBag) } override func viewDidLayoutSubviews() { @@ -197,31 +173,9 @@ extension MastodonServerRulesViewController { extension MastodonServerRulesViewController { @objc private func confirmButtonPressed(_ sender: UIButton) { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - - let email = viewModel.registerQuery.email - - context.apiService.accountRegister( - domain: viewModel.domain, - query: viewModel.registerQuery, - authorization: viewModel.applicationAuthorization - ) - .receive(on: DispatchQueue.main) - .sink { [weak self] completion in - guard let self = self else { return } - self.viewModel.isRegistering.value = false - switch completion { - case .failure(let error): - self.viewModel.error.send(error) - case .finished: - break - } - } receiveValue: { [weak self] response in - guard let self = self else { return } - let userToken = response.value - let viewModel = MastodonConfirmEmailViewModel(context: self.context, email: email, authenticateInfo: self.viewModel.authenticateInfo, userToken: userToken) - self.coordinator.present(scene: .mastodonConfirmEmail(viewModel: viewModel), from: self, transition: .show) - } - .store(in: &disposeBag) + + let viewModel = MastodonRegisterViewModel(domain: self.viewModel.domain, authenticateInfo: self.viewModel.authenticateInfo, instance: self.viewModel.instance, applicationToken: self.viewModel.applicationToken) + self.coordinator.present(scene: .mastodonRegister(viewModel: viewModel), from: self, transition: .show) } } diff --git a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewModel.swift b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewModel.swift index 9569ffe8..89f31bbc 100644 --- a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewModel.swift +++ b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewModel.swift @@ -10,34 +10,27 @@ import Combine import MastodonSDK final class MastodonServerRulesViewModel { - // input - let context: AppContext + let domain: String let authenticateInfo: AuthenticationViewModel.AuthenticateInfo let rules: [Mastodon.Entity.Instance.Rule] - let registerQuery: Mastodon.API.Account.RegisterQuery - let applicationAuthorization: Mastodon.API.OAuth.Authorization - - // output - let isRegistering = CurrentValueSubject(false) - let error = CurrentValueSubject(nil) + let instance: Mastodon.Entity.Instance + let applicationToken: Mastodon.Entity.Token init( - context: AppContext, domain: String, authenticateInfo: AuthenticationViewModel.AuthenticateInfo, rules: [Mastodon.Entity.Instance.Rule], - registerQuery: Mastodon.API.Account.RegisterQuery, - applicationAuthorization: Mastodon.API.OAuth.Authorization + instance: Mastodon.Entity.Instance, + applicationToken: Mastodon.Entity.Token ) { - self.context = context self.domain = domain self.authenticateInfo = authenticateInfo self.rules = rules - self.registerQuery = registerQuery - self.applicationAuthorization = applicationAuthorization + self.instance = instance + self.applicationToken = applicationToken } var rulesAttributedString: NSAttributedString { diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Error.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Error.swift index 9d5ae2a5..a3602574 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Error.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Error.swift @@ -19,10 +19,12 @@ extension Mastodon.Entity { public struct Error: Codable { public let error: String public let errorDescription: String? - + public let details: ErrorDetail? + enum CodingKeys: String, CodingKey { case error case errorDescription = "error_description" + case details } } } diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+ErrorDetail.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+ErrorDetail.swift new file mode 100644 index 00000000..7881aaa0 --- /dev/null +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+ErrorDetail.swift @@ -0,0 +1,102 @@ +// +// Mastodon+Entity+ErrorDetail.swift +// +// +// Created by sxiaojian on 2021/3/1. +// + +import Foundation +extension Mastodon.Entity.Error { + /// ERR_BLOCKED When e-mail provider is not allowed + /// ERR_UNREACHABLE When e-mail address does not resolve to any IP via DNS (MX, A, AAAA) + /// ERR_TAKEN When username or e-mail are already taken + /// ERR_RESERVED When a username is reserved, e.g. "webmaster" or "admin" + /// ERR_ACCEPTED When agreement has not been accepted + /// ERR_BLANK When a required attribute is blank + /// ERR_INVALID When an attribute is malformed, e.g. wrong characters or invalid e-mail address + /// ERR_TOO_LONG When an attribute is over the character limit + /// ERR_INCLUSION When an attribute is not one of the allowed values, e.g. unsupported locale + public enum SignUpError: RawRepresentable, Codable { + case ERR_BLOCKED + case ERR_UNREACHABLE + case ERR_TAKEN + case ERR_RESERVED + case ERR_ACCEPTED + case ERR_BLANK + case ERR_INVALID + case ERR_TOO_LONG + case ERR_TOO_SHORT + case ERR_INCLUSION + case _other(String) + + public init?(rawValue: String) { + switch rawValue { + case "ERR_BLOCKED": self = .ERR_BLOCKED + case "ERR_UNREACHABLE": self = .ERR_UNREACHABLE + case "ERR_TAKEN": self = .ERR_TAKEN + case "ERR_RESERVED": self = .ERR_RESERVED + case "ERR_ACCEPTED": self = .ERR_ACCEPTED + case "ERR_BLANK": self = .ERR_BLANK + case "ERR_INVALID": self = .ERR_INVALID + case "ERR_TOO_LONG": self = .ERR_TOO_LONG + case "ERR_TOO_SHORT": self = .ERR_TOO_SHORT + case "ERR_INCLUSION": self = .ERR_INCLUSION + + default: + self = ._other(rawValue) + } + } + + public var rawValue: String { + switch self { + case .ERR_BLOCKED: return "ERR_BLOCKED" + case .ERR_UNREACHABLE: return "ERR_UNREACHABLE" + case .ERR_TAKEN: return "ERR_TAKEN" + case .ERR_RESERVED: return "ERR_RESERVED" + case .ERR_ACCEPTED: return "ERR_ACCEPTED" + case .ERR_BLANK: return "ERR_BLANK" + case .ERR_INVALID: return "ERR_INVALID" + case .ERR_TOO_LONG: return "ERR_TOO_LONG" + case .ERR_TOO_SHORT: return "ERR_TOO_SHORT" + case .ERR_INCLUSION: return "ERR_INCLUSION" + + case ._other(let value): return value + } + } + } +} +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]? + + enum CodingKeys: String, CodingKey { + case username + case email + case password + case agreement + case locale + case reason + } + } + + 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" + } + } +} diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Instance.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Instance.swift index 18e41b3e..226af40f 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Instance.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Instance.swift @@ -47,7 +47,7 @@ extension Mastodon.Entity { case approvalRequired = "approval_required" case invitesEnabled = "invites_enabled" case urls - case statistics + case statistics = "stats" case thumbnail case contactAccount = "contact_account"