From 8ef5a34a40cf6f97c1e229001ed0221d4b34aaf9 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Sat, 20 Feb 2021 13:55:06 +0800 Subject: [PATCH 01/12] feat: finish sign up page --- Mastodon.xcodeproj/project.pbxproj | 4 + .../Extension/UITapGestureRecognizer.swift | 26 ++ Mastodon/Generated/Assets.swift | 3 + .../Label/secondary.colorset/Contents.json | 6 +- .../Colors/TextField/Contents.json | 9 + .../successGreen.colorset/Contents.json | 20 + .../MastodonRegisterViewController.swift | 380 +++++++++++++++--- .../Register/MastodonRegisterViewModel.swift | 68 +++- .../API/Mastodon+API+Account.swift | 4 +- 9 files changed, 451 insertions(+), 69 deletions(-) create mode 100644 Mastodon/Extension/UITapGestureRecognizer.swift create mode 100644 Mastodon/Resources/Assets.xcassets/Colors/TextField/Contents.json create mode 100644 Mastodon/Resources/Assets.xcassets/Colors/TextField/successGreen.colorset/Contents.json diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 4d26a731..889bd465 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -23,6 +23,7 @@ 2D38F1F725CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F1F625CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift */; }; 2D38F1FE25CD481700561493 /* StatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F1FD25CD481700561493 /* StatusProvider.swift */; }; 2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F20725CD491300561493 /* DisposeBagCollectable.swift */; }; + 2D3F9E0425DFA133004262D9 /* UITapGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D3F9E0325DFA133004262D9 /* UITapGestureRecognizer.swift */; }; 2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */ = {isa = PBXBuildFile; productRef = 2D42FF6025C8177C004A627A /* ActiveLabel */; }; 2D42FF6B25C817D2004A627A /* MastodonContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF6A25C817D2004A627A /* MastodonContent.swift */; }; 2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF7D25C82218004A627A /* ActionToolBarContainer.swift */; }; @@ -196,6 +197,7 @@ 2D38F1F625CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeTimelineViewModel+LoadOldestState.swift"; sourceTree = ""; }; 2D38F1FD25CD481700561493 /* StatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusProvider.swift; sourceTree = ""; }; 2D38F20725CD491300561493 /* DisposeBagCollectable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisposeBagCollectable.swift; sourceTree = ""; }; + 2D3F9E0325DFA133004262D9 /* UITapGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITapGestureRecognizer.swift; sourceTree = ""; }; 2D42FF6A25C817D2004A627A /* MastodonContent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonContent.swift; sourceTree = ""; }; 2D42FF7D25C82218004A627A /* ActionToolBarContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionToolBarContainer.swift; sourceTree = ""; }; 2D42FF8425C8224F004A627A /* HitTestExpandedButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HitTestExpandedButton.swift; sourceTree = ""; }; @@ -815,6 +817,7 @@ children = ( DB084B5125CBC56300F898ED /* CoreDataStack */, DB0140CE25C42AEE00F9F3CF /* OSLog.swift */, + 2D3F9E0325DFA133004262D9 /* UITapGestureRecognizer.swift */, DB8AF55C25C138B7002E6C99 /* UIViewController.swift */, 2D46975D25C2A54100CF4AA9 /* NSLayoutConstraint.swift */, 2D46976325C2A71500CF4AA9 /* UIIamge.swift */, @@ -1243,6 +1246,7 @@ 2D45E5BF25C9549700A6D639 /* PublicTimelineViewModel+State.swift in Sources */, 2D46976425C2A71500CF4AA9 /* UIIamge.swift in Sources */, DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */, + 2D3F9E0425DFA133004262D9 /* UITapGestureRecognizer.swift in Sources */, DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */, 2DF75BA125D0E29D00694EC8 /* StatusProvider+TimelinePostTableViewCellDelegate.swift in Sources */, 2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */, diff --git a/Mastodon/Extension/UITapGestureRecognizer.swift b/Mastodon/Extension/UITapGestureRecognizer.swift new file mode 100644 index 00000000..7e1628f3 --- /dev/null +++ b/Mastodon/Extension/UITapGestureRecognizer.swift @@ -0,0 +1,26 @@ +// +// UITapGestureRecognizer.swift +// Mastodon +// +// Created by sxiaojian on 2021/2/19. +// + +import UIKit + +extension UITapGestureRecognizer { + + static var singleTapGestureRecognizer: UITapGestureRecognizer { + let tapGestureRecognizer = UITapGestureRecognizer() + tapGestureRecognizer.numberOfTapsRequired = 1 + tapGestureRecognizer.numberOfTouchesRequired = 1 + return tapGestureRecognizer + } + + static var doubleTapGestureRecognizer: UITapGestureRecognizer { + let tapGestureRecognizer = UITapGestureRecognizer() + tapGestureRecognizer.numberOfTapsRequired = 2 + tapGestureRecognizer.numberOfTouchesRequired = 1 + return tapGestureRecognizer + } + +} diff --git a/Mastodon/Generated/Assets.swift b/Mastodon/Generated/Assets.swift index ba540499..84de0ae9 100644 --- a/Mastodon/Generated/Assets.swift +++ b/Mastodon/Generated/Assets.swift @@ -44,6 +44,9 @@ internal enum Asset { internal static let primary = ColorAsset(name: "Colors/Label/primary") internal static let secondary = ColorAsset(name: "Colors/Label/secondary") } + internal enum TextField { + internal static let successGreen = ColorAsset(name: "Colors/TextField/successGreen") + } internal static let lightAlertYellow = ColorAsset(name: "Colors/lightAlertYellow") internal static let lightBackground = ColorAsset(name: "Colors/lightBackground") internal static let lightBrandBlue = ColorAsset(name: "Colors/lightBrandBlue") diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Label/secondary.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Label/secondary.colorset/Contents.json index 275226a1..a47dfc69 100644 --- a/Mastodon/Resources/Assets.xcassets/Colors/Label/secondary.colorset/Contents.json +++ b/Mastodon/Resources/Assets.xcassets/Colors/Label/secondary.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0x84", - "green" : "0x69", - "red" : "0x60" + "blue" : "132", + "green" : "105", + "red" : "96" } }, "idiom" : "universal" diff --git a/Mastodon/Resources/Assets.xcassets/Colors/TextField/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/TextField/Contents.json new file mode 100644 index 00000000..6e965652 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Colors/TextField/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/Mastodon/Resources/Assets.xcassets/Colors/TextField/successGreen.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/TextField/successGreen.colorset/Contents.json new file mode 100644 index 00000000..7ccf54a1 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Colors/TextField/successGreen.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "89", + "green" : "199", + "red" : "52" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift b/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift index db9f3085..247841c2 100644 --- a/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift +++ b/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift @@ -5,79 +5,165 @@ // Created by MainasuK Cirno on 2021-2-5. // -import os.log -import UIKit import Combine import MastodonSDK +import os.log +import UIKit import UITextField_Shake final class MastodonRegisterViewController: UIViewController, NeedsDependency { - var disposeBag = Set() weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } var viewModel: MastodonRegisterViewModel! + + let tapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer - let usernameLabel: UILabel = { + let stackViewTopDistance: CGFloat = 16 + + var keyboardFrame: CGRect! + + var scrollview: UIScrollView = { + let scrollview = UIScrollView() + scrollview.showsVerticalScrollIndicator = false + scrollview.translatesAutoresizingMaskIntoConstraints = false + return scrollview + }() + + let largeTitleLabel: UILabel = { + let label = UILabel() + label.font = .preferredFont(forTextStyle: .largeTitle) + label.textColor = Asset.Colors.Label.black.color + label.text = "Tell us about you." + return label + }() + + let photoView: UIView = { + let view = UIView() + view.backgroundColor = .clear + return view + }() + + let photoButton: UIButton = { + let button = UIButton(type: .custom) + let boldFont = UIFont.systemFont(ofSize: 42) + let configuration = UIImage.SymbolConfiguration(font: boldFont) + let image = UIImage(systemName: "person.fill.viewfinder", withConfiguration: configuration) + + button.setImage(image?.withRenderingMode(UIImage.RenderingMode.alwaysTemplate), for: UIControl.State.normal) + button.imageView?.tintColor = Asset.Colors.Icon.photo.color + button.backgroundColor = .white + button.layer.cornerRadius = 45 + button.clipsToBounds = true + return button + }() + + let plusIcon: UIImageView = { + let icon = UIImageView() + let boldFont = UIFont.systemFont(ofSize: 24) + let configuration = UIImage.SymbolConfiguration(font: boldFont) + let image = UIImage(systemName: "plus.circle.fill", withConfiguration: configuration) + icon.image = image + icon.tintColor = Asset.Colors.Icon.plus.color + return icon + }() + + let domainLabel: UILabel = { let label = UILabel() label.font = .preferredFont(forTextStyle: .headline) - label.textColor = Asset.Colors.Label.primary.color - label.text = "Username:" + label.textColor = Asset.Colors.Label.black.color return label }() let usernameTextField: UITextField = { let textField = UITextField() - textField.placeholder = "Username" + textField.autocapitalizationType = .none textField.autocorrectionType = .no + textField.backgroundColor = .white + textField.textColor = .black + textField.attributedPlaceholder = NSAttributedString(string: "username", + attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.lightSecondaryText.color, + NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .headline)]) + textField.borderStyle = UITextField.BorderStyle.roundedRect + let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height)) + textField.leftView = paddingView + textField.leftViewMode = .always return textField }() - let emailLabel: UILabel = { + let usernameIsTakenLabel: UILabel = { let label = UILabel() - label.font = .preferredFont(forTextStyle: .headline) - label.textColor = Asset.Colors.Label.primary.color - label.text = "Email:" return label }() + let displayNameTextField: UITextField = { + let textField = UITextField() + textField.autocapitalizationType = .none + textField.autocorrectionType = .no + textField.backgroundColor = .white + textField.textColor = .black + textField.attributedPlaceholder = NSAttributedString(string: "display name", + attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.lightSecondaryText.color, + NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .headline)]) + textField.borderStyle = UITextField.BorderStyle.roundedRect + let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height)) + textField.leftView = paddingView + textField.leftViewMode = .always + return textField + }() + let emailTextField: UITextField = { let textField = UITextField() - textField.placeholder = "example@gmail.com" textField.autocapitalizationType = .none textField.autocorrectionType = .no textField.keyboardType = .emailAddress + textField.backgroundColor = .white + textField.textColor = .black + textField.attributedPlaceholder = NSAttributedString(string: "email", + attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.lightSecondaryText.color, + NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .headline)]) + textField.borderStyle = UITextField.BorderStyle.roundedRect + let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height)) + textField.leftView = paddingView + textField.leftViewMode = .always return textField }() - let passwordLabel: UILabel = { + let passwordCheckLabel: UILabel = { let label = UILabel() - label.font = .preferredFont(forTextStyle: .headline) - label.textColor = Asset.Colors.Label.primary.color - label.text = "Password:" + label.numberOfLines = 4 return label }() let passwordTextField: UITextField = { let textField = UITextField() - textField.placeholder = "Password" textField.autocapitalizationType = .none textField.autocorrectionType = .no textField.keyboardType = .asciiCapable textField.isSecureTextEntry = true + textField.backgroundColor = .white + textField.textColor = .black + textField.attributedPlaceholder = NSAttributedString(string: "password", + attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.lightSecondaryText.color, + NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .headline)]) + textField.borderStyle = UITextField.BorderStyle.roundedRect + let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height)) + textField.leftView = paddingView + textField.leftViewMode = .always return textField }() let signUpButton: UIButton = { let button = UIButton(type: .system) button.titleLabel?.font = .preferredFont(forTextStyle: .headline) - button.setBackgroundImage(UIImage.placeholder(color: Asset.Colors.Background.secondarySystemBackground.color), for: .normal) - button.setBackgroundImage(UIImage.placeholder(color: Asset.Colors.Background.secondarySystemBackground.color.withAlphaComponent(0.8)), for: .disabled) + button.setBackgroundImage(UIImage.placeholder(color: Asset.Colors.lightBrandBlue.color), for: .normal) + button.setBackgroundImage(UIImage.placeholder(color: Asset.Colors.lightDisabled.color), for: .disabled) + button.isEnabled = false button.setTitleColor(Asset.Colors.Label.primary.color, for: .normal) - button.setTitle("Sign up", for: .normal) + button.setTitle("Continue", for: .normal) button.layer.masksToBounds = true button.layer.cornerRadius = 8 button.layer.cornerCurve = .continuous @@ -89,36 +175,97 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency { activityIndicatorView.hidesWhenStopped = true return activityIndicatorView }() - } extension MastodonRegisterViewController { - override func viewDidLoad() { super.viewDidLoad() - title = "Sign Up" - view.backgroundColor = Asset.Colors.Background.systemBackground.color + navigationController?.navigationBar.isHidden = true + view.backgroundColor = Asset.Colors.Background.signUpSystemBackground.color + domainLabel.text = "@" + viewModel.domain + " " + domainLabel.sizeToFit() + passwordCheckLabel.attributedText = viewModel.attributeStringForPassword() + usernameTextField.rightView = domainLabel + usernameTextField.rightViewMode = .always + usernameTextField.delegate = self + displayNameTextField.delegate = self + emailTextField.delegate = self + passwordTextField.delegate = self + // gesture + view.addGestureRecognizer(tapGestureRecognizer) + tapGestureRecognizer.addTarget(self, action: #selector(_resignFirstResponder)) + + // stackview let stackView = UIStackView() - stackView.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(stackView) + stackView.axis = .vertical + stackView.distribution = .fill + stackView.spacing = 40 + stackView.layoutMargins = UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 4) + stackView.isLayoutMarginsRelativeArrangement = true + stackView.addArrangedSubview(largeTitleLabel) + stackView.addArrangedSubview(photoView) + stackView.addArrangedSubview(usernameTextField) + stackView.addArrangedSubview(displayNameTextField) + stackView.addArrangedSubview(emailTextField) + stackView.addArrangedSubview(passwordTextField) + stackView.addArrangedSubview(passwordCheckLabel) + + // scrollview + view.addSubview(scrollview) NSLayoutConstraint.activate([ - stackView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor, constant: 16), - stackView.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor), - stackView.trailingAnchor.constraint(equalTo: view.readableContentGuide.trailingAnchor), + scrollview.frameLayoutGuide.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor), + scrollview.frameLayoutGuide.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), + view.trailingAnchor.constraint(equalTo: scrollview.frameLayoutGuide.trailingAnchor, constant: 20), + scrollview.frameLayoutGuide.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor), ]) - stackView.axis = .vertical - stackView.spacing = 8 + // stackview + scrollview.addSubview(stackView) + stackView.translatesAutoresizingMaskIntoConstraints = false + let bottomEdgeLayoutConstraint: NSLayoutConstraint = scrollview.contentLayoutGuide.bottomAnchor.constraint(equalTo: stackView.bottomAnchor) + NSLayoutConstraint.activate([ + stackView.topAnchor.constraint(equalTo: scrollview.contentLayoutGuide.topAnchor, constant: stackViewTopDistance), + stackView.leadingAnchor.constraint(equalTo: scrollview.contentLayoutGuide.leadingAnchor), + stackView.trailingAnchor.constraint(equalTo: scrollview.contentLayoutGuide.trailingAnchor), + stackView.widthAnchor.constraint(equalTo: scrollview.frameLayoutGuide.widthAnchor), + bottomEdgeLayoutConstraint, + ]) + + // photoview + photoView.translatesAutoresizingMaskIntoConstraints = false + photoView.addSubview(photoButton) + NSLayoutConstraint.activate([ + photoView.heightAnchor.constraint(equalToConstant: 90).priority(.defaultHigh), + ]) + photoButton.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + photoButton.heightAnchor.constraint(equalToConstant: 90).priority(.defaultHigh), + photoButton.widthAnchor.constraint(equalToConstant: 90).priority(.defaultHigh), + photoButton.centerXAnchor.constraint(equalTo: photoView.centerXAnchor), + photoButton.centerYAnchor.constraint(equalTo: photoView.centerYAnchor), + ]) + plusIcon.translatesAutoresizingMaskIntoConstraints = false + photoView.addSubview(plusIcon) + NSLayoutConstraint.activate([ + plusIcon.trailingAnchor.constraint(equalTo: photoButton.trailingAnchor), + plusIcon.bottomAnchor.constraint(equalTo: photoButton.bottomAnchor), + ]) - stackView.addArrangedSubview(usernameLabel) - stackView.addArrangedSubview(usernameTextField) - stackView.addArrangedSubview(emailLabel) - stackView.addArrangedSubview(emailTextField) - stackView.addArrangedSubview(passwordLabel) - stackView.addArrangedSubview(passwordTextField) + // textfield + NSLayoutConstraint.activate([ + usernameTextField.heightAnchor.constraint(equalToConstant: 50).priority(.defaultHigh), + displayNameTextField.heightAnchor.constraint(equalToConstant: 50).priority(.defaultHigh), + emailTextField.heightAnchor.constraint(equalToConstant: 50).priority(.defaultHigh), + passwordTextField.heightAnchor.constraint(equalToConstant: 50).priority(.defaultHigh), + ]) + + // password + stackView.setCustomSpacing(6, after: passwordTextField) + stackView.setCustomSpacing(32, after: passwordCheckLabel) + // button signUpButton.translatesAutoresizingMaskIntoConstraints = false stackView.addArrangedSubview(signUpButton) NSLayoutConstraint.activate([ @@ -126,18 +273,31 @@ extension MastodonRegisterViewController { ]) signUpActivityIndicatorView.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(signUpActivityIndicatorView) + scrollview.addSubview(signUpActivityIndicatorView) NSLayoutConstraint.activate([ signUpActivityIndicatorView.centerXAnchor.constraint(equalTo: signUpButton.centerXAnchor), signUpActivityIndicatorView.centerYAnchor.constraint(equalTo: signUpButton.centerYAnchor), ]) + NotificationCenter.default.publisher(for: UIResponder.keyboardWillChangeFrameNotification, object: nil) + .sink { [weak self] notification in + guard let endFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { + return + } + self?.keyboardFrame = endFrame + UIView.animate(withDuration: 0.3) { + bottomEdgeLayoutConstraint.constant = UIScreen.main.bounds.height - endFrame.origin.y + 26 + self?.view.layoutIfNeeded() + } + } + .store(in: &disposeBag) + viewModel.isRegistering .receive(on: DispatchQueue.main) .sink { [weak self] isRegistering in guard let self = self else { return } isRegistering ? self.signUpActivityIndicatorView.startAnimating() : self.signUpActivityIndicatorView.stopAnimating() - self.signUpButton.setTitle(isRegistering ? "" : "Sign up", for: .normal) + self.signUpButton.setTitle(isRegistering ? "" : "Continue", for: .normal) self.signUpButton.isEnabled = !isRegistering } .store(in: &disposeBag) @@ -157,28 +317,125 @@ extension MastodonRegisterViewController { ) } .store(in: &disposeBag) + NotificationCenter.default + .publisher(for: UITextField.textDidChangeNotification, object: passwordTextField) + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + guard let self = self else { return } + guard let text = self.passwordTextField.text else { return } + + let validations = self.viewModel.validatePassword(text: text) + + self.passwordCheckLabel.attributedText = self.viewModel.attributeStringForPassword(eightCharacters: validations.0, oneNumber: validations.1, oneSpecialCharacter: validations.2) + } + .store(in: &disposeBag) signUpButton.addTarget(self, action: #selector(MastodonRegisterViewController.signUpButtonPressed(_:)), for: .touchUpInside) } - +} + +extension MastodonRegisterViewController: UITextFieldDelegate { + func textFieldDidBeginEditing(_ textField: UITextField) { + var bottomOffsetY: CGFloat = textField.frame.origin.y + textField.frame.height - scrollview.frame.height + keyboardFrame.size.height + stackViewTopDistance + if textField == passwordTextField { + bottomOffsetY += passwordCheckLabel.frame.height + } + + if bottomOffsetY > 0 { + scrollview.setContentOffset(CGPoint(x: 0, y: bottomOffsetY), animated: true) + } + } + + func textFieldDidEndEditing(_ textField: UITextField) { + let valid = validateTextField(textField: textField) + if valid { + if validateAllTextField() { + signUpButton.isEnabled = true + } + } + } + + func showShadowWithColor(color: UIColor, textField: UITextField) { + // To apply Shadow + textField.layer.shadowOpacity = 1 + textField.layer.shadowRadius = 2.0 + textField.layer.shadowOffset = CGSize.zero // Use any CGSize + textField.layer.shadowColor = color.cgColor + } + func validateUsername() -> Bool { + if usernameTextField.text?.count ?? 0 > 0 { + showShadowWithColor(color: Asset.Colors.TextField.successGreen.color, textField: usernameTextField) + return true + } else { + return false + } + } + func validateDisplayName() -> Bool { + if displayNameTextField.text?.count ?? 0 > 0 { + return true + } else { + return false + } + } + func validateEmail() -> Bool { + guard let email = emailTextField.text else { + return false + } + if !viewModel.isValidEmail(email) { + return false + } + return true + } + func validatePassword() -> Bool { + guard let password = passwordTextField.text else { + return false + } + + let result = viewModel.validatePassword(text: password) + if !(result.0 && result.1 && result.2) { + return false + } + return true + } + func validateTextField(textField: UITextField) -> Bool { + signUpButton.isEnabled = false + var isvalid = false + if textField == usernameTextField { + isvalid = validateUsername() + } + if textField == displayNameTextField { + isvalid = validateDisplayName() + } + if textField == emailTextField { + isvalid = validateEmail() + } + if textField == passwordTextField { + isvalid = validatePassword() + } + if isvalid { + showShadowWithColor(color: Asset.Colors.TextField.successGreen.color, textField: textField) + } else { + textField.shake() + showShadowWithColor(color: Asset.Colors.lightDangerRed.color, textField: textField) + } + return isvalid + } + func validateAllTextField() -> Bool { + return validateUsername() && validateDisplayName() && validateEmail() && validatePassword() + } } extension MastodonRegisterViewController { - + @objc private func _resignFirstResponder() { + usernameTextField.resignFirstResponder() + displayNameTextField.resignFirstResponder() + emailTextField.resignFirstResponder() + passwordTextField.resignFirstResponder() + } + @objc private func signUpButtonPressed(_ sender: UIButton) { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - guard let username = usernameTextField.text else { - usernameTextField.shake() - return - } - - guard let email = emailTextField.text else { - emailTextField.shake() - return - } - - guard let password = passwordTextField.text else { - passwordTextField.shake() + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", (#file as NSString).lastPathComponent, #line, #function) + if !validateAllTextField() { return } @@ -187,11 +444,12 @@ extension MastodonRegisterViewController { let query = Mastodon.API.Account.RegisterQuery( reason: nil, - username: username, - email: email, - password: password, - agreement: true, // TODO: - locale: "en" // TODO: + username: usernameTextField.text!, + displayname: displayNameTextField.text!, + email: emailTextField.text!, + password: passwordTextField.text!, + agreement: true, // TODO: + locale: "en" // TODO: ) context.apiService.accountRegister( @@ -211,7 +469,7 @@ extension MastodonRegisterViewController { } } receiveValue: { [weak self] response in guard let self = self else { return } - let _ = response.value + _ = response.value // TODO: let alertController = UIAlertController(title: "Success", message: "Regsiter request sent. Please check your email.\n(Auto sign in not implement yet.)", preferredStyle: .alert) let okAction = UIAlertAction(title: "OK", style: .default) { [weak self] _ in @@ -222,7 +480,5 @@ extension MastodonRegisterViewController { self.coordinator.present(scene: .alertController(alertController: alertController), from: self, transition: .alertController(animated: true, completion: nil)) } .store(in: &disposeBag) - } - } diff --git a/Mastodon/Scene/Authentication/Register/MastodonRegisterViewModel.swift b/Mastodon/Scene/Authentication/Register/MastodonRegisterViewModel.swift index f2404bb0..09897442 100644 --- a/Mastodon/Scene/Authentication/Register/MastodonRegisterViewModel.swift +++ b/Mastodon/Scene/Authentication/Register/MastodonRegisterViewModel.swift @@ -5,12 +5,12 @@ // Created by MainasuK Cirno on 2021-2-5. // -import Foundation import Combine +import Foundation import MastodonSDK +import UIKit final class MastodonRegisterViewModel { - // input let domain: String let applicationToken: Mastodon.Entity.Token @@ -25,5 +25,67 @@ final class MastodonRegisterViewModel { self.applicationToken = applicationToken self.applicationAuthorization = Mastodon.API.OAuth.Authorization(accessToken: applicationToken.accessToken) } - +} + +extension MastodonRegisterViewModel { + + func isValidEmail(_ email: String) -> Bool { + let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" + + let emailPred = NSPredicate(format:"SELF MATCHES %@", emailRegEx) + return emailPred.evaluate(with: email) + } + + func validatePassword(text: String) -> (Bool, Bool, Bool) { + let trimmedText = text.trimmingCharacters(in: .whitespaces) + let isEightCharacters = trimmedText.count >= 8 + let isOneNumber = trimmedText.range(of: ".*[0-9]", options: .regularExpression) != nil + let isOneSpecialCharacter = trimmedText.trimmingCharacters(in: .decimalDigits).trimmingCharacters(in: .letters).count > 0 + return (isEightCharacters, isOneNumber, isOneSpecialCharacter) + } + + func attributeStringForUsername() -> NSAttributedString { + let resultAttributeString = NSMutableAttributedString() + let redImage = NSTextAttachment() + let font = UIFont.preferredFont(forTextStyle: .caption1) + let configuration = UIImage.SymbolConfiguration(font: font) + redImage.image = UIImage(systemName: "xmark.octagon.fill", withConfiguration: configuration)?.withTintColor(Asset.Colors.lightDangerRed.color) + let imageAttribute = NSAttributedString(attachment: redImage) + let stringAttribute = NSAttributedString(string: "This username is taken.", attributes: [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: Asset.Colors.lightDangerRed.color]) + resultAttributeString.append(imageAttribute) + resultAttributeString.append(stringAttribute) + return resultAttributeString + } + + func attributeStringForPassword(eightCharacters: Bool = false, oneNumber: Bool = false, oneSpecialCharacter: Bool = false) -> NSAttributedString { + let font = UIFont.preferredFont(forTextStyle: .caption1) + let color = UIColor.black + 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]) + attributeString.append(start) + + attributeString.append(checkImage(color: eightCharacters ? color : falseColor)) + let eightCharactersDescription = NSAttributedString(string: "Eight characters\n", attributes: [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: color]) + attributeString.append(eightCharactersDescription) + + attributeString.append(checkImage(color: oneNumber ? color : falseColor)) + let oneNumberDescription = NSAttributedString(string: "One number\n", attributes: [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: color]) + attributeString.append(oneNumberDescription) + + attributeString.append(checkImage(color: oneSpecialCharacter ? color : falseColor)) + let oneSpecialCharacterDescription = NSAttributedString(string: "One special character\n", attributes: [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: color]) + attributeString.append(oneSpecialCharacterDescription) + + return attributeString + } + + func checkImage(color: UIColor) -> NSAttributedString { + let checkImage = NSTextAttachment() + let font = UIFont.preferredFont(forTextStyle: .caption1) + let configuration = UIImage.SymbolConfiguration(font: font) + checkImage.image = UIImage(systemName: "checkmark.circle.fill", withConfiguration: configuration)?.withTintColor(color) + return NSAttributedString(attachment: checkImage) + } } diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account.swift index d9b2a444..97659533 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account.swift @@ -161,14 +161,16 @@ extension Mastodon.API.Account { public struct RegisterQuery: Codable, PostQuery { public let reason: String? public let username: String + public let displayname: String public let email: String public let password: String public let agreement: Bool public let locale: String - public init(reason: String? = nil, username: String, email: String, password: String, agreement: Bool, locale: String) { + public init(reason: String? = nil, username: String, displayname: String, email: String, password: String, agreement: Bool, locale: String) { self.reason = reason self.username = username + self.displayname = displayname self.email = email self.password = password self.agreement = agreement From a74ac3a41a91434a8ce0acda11d11d6495fdff40 Mon Sep 17 00:00:00 2001 From: CMK Date: Sat, 20 Feb 2021 17:13:16 +0800 Subject: [PATCH 02/12] chore: add Combine style valid logic --- .../MastodonRegisterViewController.swift | 46 ++++++++++++++++--- .../Register/MastodonRegisterViewModel.swift | 15 ++++++ 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift b/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift index 247841c2..44939d6a 100644 --- a/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift +++ b/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift @@ -302,6 +302,15 @@ extension MastodonRegisterViewController { } .store(in: &disposeBag) + viewModel.isUsernameValid + .receive(on: DispatchQueue.main) + .sink { [weak self] isValid in + guard let self = self else { return } + self.setTextFieldValidAppearance(self.usernameTextField, isValid: isValid) + } + .store(in: &disposeBag) + + viewModel.error .compactMap { $0 } .receive(on: DispatchQueue.main) @@ -317,6 +326,7 @@ extension MastodonRegisterViewController { ) } .store(in: &disposeBag) + NotificationCenter.default .publisher(for: UITextField.textDidChangeNotification, object: passwordTextField) .receive(on: DispatchQueue.main) @@ -347,10 +357,15 @@ extension MastodonRegisterViewController: UITextFieldDelegate { } func textFieldDidEndEditing(_ textField: UITextField) { - let valid = validateTextField(textField: textField) - if valid { - if validateAllTextField() { - signUpButton.isEnabled = true + switch textField { + case usernameTextField: + viewModel.username.value = textField.text + default: + let valid = validateTextField(textField: textField) + if valid { + if validateAllTextField() { + signUpButton.isEnabled = true + } } } } @@ -361,6 +376,8 @@ extension MastodonRegisterViewController: UITextFieldDelegate { textField.layer.shadowRadius = 2.0 textField.layer.shadowOffset = CGSize.zero // Use any CGSize textField.layer.shadowColor = color.cgColor + textField.layer.shadowPath = UIBezierPath(roundedRect: textField.bounds, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: 2.0, height: 2.0)).cgPath + } func validateUsername() -> Bool { if usernameTextField.text?.count ?? 0 > 0 { @@ -400,9 +417,9 @@ extension MastodonRegisterViewController: UITextFieldDelegate { func validateTextField(textField: UITextField) -> Bool { signUpButton.isEnabled = false var isvalid = false - if textField == usernameTextField { - isvalid = validateUsername() - } +// if textField == usernameTextField { +// isvalid = validateUsername() +// } if textField == displayNameTextField { isvalid = validateDisplayName() } @@ -423,6 +440,21 @@ extension MastodonRegisterViewController: UITextFieldDelegate { func validateAllTextField() -> Bool { return validateUsername() && validateDisplayName() && validateEmail() && validatePassword() } + + private func setTextFieldValidAppearance(_ textField: UITextField, isValid: Bool?) { + guard let isValid = isValid else { + showShadowWithColor(color: .clear, textField: textField) + return + } + + if isValid { + showShadowWithColor(color: Asset.Colors.TextField.successGreen.color, textField: textField) + } else { + textField.shake() + showShadowWithColor(color: Asset.Colors.lightDangerRed.color, textField: textField) + } + } + } extension MastodonRegisterViewController { diff --git a/Mastodon/Scene/Authentication/Register/MastodonRegisterViewModel.swift b/Mastodon/Scene/Authentication/Register/MastodonRegisterViewModel.swift index 09897442..e5151364 100644 --- a/Mastodon/Scene/Authentication/Register/MastodonRegisterViewModel.swift +++ b/Mastodon/Scene/Authentication/Register/MastodonRegisterViewModel.swift @@ -11,19 +11,34 @@ import MastodonSDK import UIKit final class MastodonRegisterViewModel { + + var disposeBag = Set() + // input let domain: String let applicationToken: Mastodon.Entity.Token let isRegistering = CurrentValueSubject(false) + let username = CurrentValueSubject(nil) // output let applicationAuthorization: Mastodon.API.OAuth.Authorization + let isUsernameValid = CurrentValueSubject(nil) let error = CurrentValueSubject(nil) init(domain: String, applicationToken: Mastodon.Entity.Token) { self.domain = domain self.applicationToken = applicationToken self.applicationAuthorization = Mastodon.API.OAuth.Authorization(accessToken: applicationToken.accessToken) + + username + .map { username in + guard let username = username else { + return nil + } + return !username.isEmpty + } + .assign(to: \.value, on: isUsernameValid) + .store(in: &disposeBag) } } From 243d3362e6748dc097f4fc78341e2c376b9cabc2 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Sat, 20 Feb 2021 18:24:23 +0800 Subject: [PATCH 03/12] chore: add Combine style valid logic --- .../MastodonRegisterViewController.swift | 131 ++++++++++-------- .../Register/MastodonRegisterViewModel.swift | 56 ++++++-- .../API/Mastodon+API+Account.swift | 4 +- 3 files changed, 117 insertions(+), 74 deletions(-) diff --git a/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift b/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift index 44939d6a..300cff5b 100644 --- a/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift +++ b/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift @@ -34,7 +34,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency { let largeTitleLabel: UILabel = { let label = UILabel() - label.font = .preferredFont(forTextStyle: .largeTitle) + label.font = UIFontMetrics(forTextStyle: .largeTitle).scaledFont(for: UIFont.boldSystemFont(ofSize: 34)) label.textColor = Asset.Colors.Label.black.color label.text = "Tell us about you." return label @@ -60,6 +60,16 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency { return button }() + let plusIconBackground: UIImageView = { + let icon = UIImageView() + let boldFont = UIFont.systemFont(ofSize: 24) + let configuration = UIImage.SymbolConfiguration(font: boldFont) + let image = UIImage(systemName: "plus.circle", withConfiguration: configuration) + icon.image = image + icon.tintColor = .white + return icon + }() + let plusIcon: UIImageView = { let icon = UIImageView() let boldFont = UIFont.systemFont(ofSize: 24) @@ -246,6 +256,12 @@ extension MastodonRegisterViewController { photoButton.centerXAnchor.constraint(equalTo: photoView.centerXAnchor), photoButton.centerYAnchor.constraint(equalTo: photoView.centerYAnchor), ]) + plusIconBackground.translatesAutoresizingMaskIntoConstraints = false + photoView.addSubview(plusIconBackground) + NSLayoutConstraint.activate([ + plusIconBackground.trailingAnchor.constraint(equalTo: photoButton.trailingAnchor), + plusIconBackground.bottomAnchor.constraint(equalTo: photoButton.bottomAnchor), + ]) plusIcon.translatesAutoresizingMaskIntoConstraints = false photoView.addSubview(plusIcon) NSLayoutConstraint.activate([ @@ -309,7 +325,40 @@ extension MastodonRegisterViewController { self.setTextFieldValidAppearance(self.usernameTextField, isValid: isValid) } .store(in: &disposeBag) + viewModel.isDisplaynameValid + .receive(on: DispatchQueue.main) + .sink { [weak self] isValid in + guard let self = self else { return } + self.setTextFieldValidAppearance(self.displayNameTextField, isValid: isValid) + } + .store(in: &disposeBag) + viewModel.isEmailValid + .receive(on: DispatchQueue.main) + .sink { [weak self] isValid in + guard let self = self else { return } + self.setTextFieldValidAppearance(self.emailTextField, isValid: isValid) + } + .store(in: &disposeBag) + viewModel.isPasswordValid + .receive(on: DispatchQueue.main) + .sink { [weak self] isValid in + guard let self = self else { return } + self.setTextFieldValidAppearance(self.passwordTextField, isValid: isValid) + } + .store(in: &disposeBag) + Publishers.CombineLatest4( + viewModel.isUsernameValid, + viewModel.isDisplaynameValid, + viewModel.isEmailValid, + viewModel.isPasswordValid + ) + .receive(on: DispatchQueue.main) + .sink { [weak self] isUsernameValid, isDisplaynameValid, isEmailValid, isPasswordValid in + guard let self = self else { return } + self.signUpButton.isEnabled = isUsernameValid ?? false && isDisplaynameValid ?? false && isEmailValid ?? false && isPasswordValid ?? false + } + .store(in: &disposeBag) viewModel.error .compactMap { $0 } @@ -326,16 +375,13 @@ extension MastodonRegisterViewController { ) } .store(in: &disposeBag) - NotificationCenter.default .publisher(for: UITextField.textDidChangeNotification, object: passwordTextField) .receive(on: DispatchQueue.main) .sink { [weak self] _ in guard let self = self else { return } guard let text = self.passwordTextField.text else { return } - let validations = self.viewModel.validatePassword(text: text) - self.passwordCheckLabel.attributedText = self.viewModel.attributeStringForPassword(eightCharacters: validations.0, oneNumber: validations.1, oneSpecialCharacter: validations.2) } .store(in: &disposeBag) @@ -360,13 +406,13 @@ extension MastodonRegisterViewController: UITextFieldDelegate { switch textField { case usernameTextField: viewModel.username.value = textField.text - default: - let valid = validateTextField(textField: textField) - if valid { - if validateAllTextField() { - signUpButton.isEnabled = true - } - } + case displayNameTextField: + viewModel.displayname.value = textField.text + case emailTextField: + viewModel.email.value = textField.text + case passwordTextField: + viewModel.password.value = textField.text + default: break } } @@ -374,60 +420,24 @@ extension MastodonRegisterViewController: UITextFieldDelegate { // To apply Shadow textField.layer.shadowOpacity = 1 textField.layer.shadowRadius = 2.0 - textField.layer.shadowOffset = CGSize.zero // Use any CGSize + textField.layer.shadowOffset = CGSize.zero textField.layer.shadowColor = color.cgColor textField.layer.shadowPath = UIBezierPath(roundedRect: textField.bounds, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: 2.0, height: 2.0)).cgPath + } - } - func validateUsername() -> Bool { - if usernameTextField.text?.count ?? 0 > 0 { - showShadowWithColor(color: Asset.Colors.TextField.successGreen.color, textField: usernameTextField) - return true - } else { - return false - } - } - func validateDisplayName() -> Bool { - if displayNameTextField.text?.count ?? 0 > 0 { - return true - } else { - return false - } - } - func validateEmail() -> Bool { - guard let email = emailTextField.text else { - return false - } - if !viewModel.isValidEmail(email) { - return false - } - return true - } - func validatePassword() -> Bool { - guard let password = passwordTextField.text else { - return false - } - - let result = viewModel.validatePassword(text: password) - if !(result.0 && result.1 && result.2) { - return false - } - return true - } func validateTextField(textField: UITextField) -> Bool { - signUpButton.isEnabled = false var isvalid = false -// if textField == usernameTextField { -// isvalid = validateUsername() -// } + if textField == usernameTextField { + isvalid = viewModel.isUsernameValid.value ?? false + } if textField == displayNameTextField { - isvalid = validateDisplayName() + isvalid = viewModel.isDisplaynameValid.value ?? false } if textField == emailTextField { - isvalid = validateEmail() + isvalid = viewModel.isEmailValid.value ?? false } if textField == passwordTextField { - isvalid = validatePassword() + isvalid = viewModel.isPasswordValid.value ?? false } if isvalid { showShadowWithColor(color: Asset.Colors.TextField.successGreen.color, textField: textField) @@ -437,8 +447,9 @@ extension MastodonRegisterViewController: UITextFieldDelegate { } return isvalid } + func validateAllTextField() -> Bool { - return validateUsername() && validateDisplayName() && validateEmail() && validatePassword() + return viewModel.isUsernameValid.value ?? false && viewModel.isDisplaynameValid.value ?? false && viewModel.isEmailValid.value ?? false && viewModel.isPasswordValid.value ?? false } private func setTextFieldValidAppearance(_ textField: UITextField, isValid: Bool?) { @@ -454,7 +465,6 @@ extension MastodonRegisterViewController: UITextFieldDelegate { showShadowWithColor(color: Asset.Colors.lightDangerRed.color, textField: textField) } } - } extension MastodonRegisterViewController { @@ -476,10 +486,9 @@ extension MastodonRegisterViewController { let query = Mastodon.API.Account.RegisterQuery( reason: nil, - username: usernameTextField.text!, - displayname: displayNameTextField.text!, - email: emailTextField.text!, - password: passwordTextField.text!, + username: viewModel.username.value!, + email: viewModel.email.value!, + password: viewModel.password.value!, agreement: true, // TODO: locale: "en" // TODO: ) diff --git a/Mastodon/Scene/Authentication/Register/MastodonRegisterViewModel.swift b/Mastodon/Scene/Authentication/Register/MastodonRegisterViewModel.swift index e5151364..590f2c0d 100644 --- a/Mastodon/Scene/Authentication/Register/MastodonRegisterViewModel.swift +++ b/Mastodon/Scene/Authentication/Register/MastodonRegisterViewModel.swift @@ -11,7 +11,6 @@ import MastodonSDK import UIKit final class MastodonRegisterViewModel { - var disposeBag = Set() // input @@ -19,10 +18,18 @@ final class MastodonRegisterViewModel { let applicationToken: Mastodon.Entity.Token let isRegistering = CurrentValueSubject(false) let username = CurrentValueSubject(nil) + let displayname = CurrentValueSubject(nil) + let email = CurrentValueSubject(nil) + let password = CurrentValueSubject(nil) // output let applicationAuthorization: Mastodon.API.OAuth.Authorization + let isUsernameValid = CurrentValueSubject(nil) + let isDisplaynameValid = CurrentValueSubject(nil) + let isEmailValid = CurrentValueSubject(nil) + let isPasswordValid = CurrentValueSubject(nil) + let error = CurrentValueSubject(nil) init(domain: String, applicationToken: Mastodon.Entity.Token) { @@ -39,15 +46,44 @@ final class MastodonRegisterViewModel { } .assign(to: \.value, on: isUsernameValid) .store(in: &disposeBag) + displayname + .map { displayname in + guard let displayname = displayname else { + return nil + } + return !displayname.isEmpty + } + .assign(to: \.value, on: isDisplaynameValid) + .store(in: &disposeBag) + email + .map { [weak self] email in + guard let self = self else { return nil } + guard let email = email else { + return nil + } + return !email.isEmpty && self.isValidEmail(email) + } + .assign(to: \.value, on: isEmailValid) + .store(in: &disposeBag) + password + .map { [weak self] password in + guard let self = self else { return nil } + guard let password = password else { + return nil + } + let result = self.validatePassword(text: password) + return !password.isEmpty && result.0 && result.1 && result.2 + } + .assign(to: \.value, on: isPasswordValid) + .store(in: &disposeBag) } } extension MastodonRegisterViewModel { - func isValidEmail(_ email: String) -> Bool { let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" - let emailPred = NSPredicate(format:"SELF MATCHES %@", emailRegEx) + let emailPred = NSPredicate(format: "SELF MATCHES %@", emailRegEx) return emailPred.evaluate(with: email) } @@ -81,26 +117,26 @@ extension MastodonRegisterViewModel { let start = NSAttributedString(string: "Your password needs at least:\n", attributes: [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: color]) attributeString.append(start) - attributeString.append(checkImage(color: eightCharacters ? color : falseColor)) + attributeString.append(checkmarkImage(color: eightCharacters ? color : falseColor)) let eightCharactersDescription = NSAttributedString(string: "Eight characters\n", attributes: [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: color]) attributeString.append(eightCharactersDescription) - attributeString.append(checkImage(color: oneNumber ? color : falseColor)) + attributeString.append(checkmarkImage(color: oneNumber ? color : falseColor)) let oneNumberDescription = NSAttributedString(string: "One number\n", attributes: [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: color]) attributeString.append(oneNumberDescription) - attributeString.append(checkImage(color: oneSpecialCharacter ? color : falseColor)) + attributeString.append(checkmarkImage(color: oneSpecialCharacter ? color : falseColor)) let oneSpecialCharacterDescription = NSAttributedString(string: "One special character\n", attributes: [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: color]) attributeString.append(oneSpecialCharacterDescription) return attributeString } - func checkImage(color: UIColor) -> NSAttributedString { - let checkImage = NSTextAttachment() + func checkmarkImage(color: UIColor) -> NSAttributedString { + let checkmarkImage = NSTextAttachment() let font = UIFont.preferredFont(forTextStyle: .caption1) let configuration = UIImage.SymbolConfiguration(font: font) - checkImage.image = UIImage(systemName: "checkmark.circle.fill", withConfiguration: configuration)?.withTintColor(color) - return NSAttributedString(attachment: checkImage) + checkmarkImage.image = UIImage(systemName: "checkmark.circle.fill", withConfiguration: configuration)?.withTintColor(color) + return NSAttributedString(attachment: checkmarkImage) } } diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account.swift index 97659533..d9b2a444 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account.swift @@ -161,16 +161,14 @@ extension Mastodon.API.Account { public struct RegisterQuery: Codable, PostQuery { public let reason: String? public let username: String - public let displayname: String public let email: String public let password: String public let agreement: Bool public let locale: String - public init(reason: String? = nil, username: String, displayname: String, email: String, password: String, agreement: Bool, locale: String) { + public init(reason: String? = nil, username: String, email: String, password: String, agreement: Bool, locale: String) { self.reason = reason self.username = username - self.displayname = displayname self.email = email self.password = password self.agreement = agreement From 0e2aa4570d5272a00891d5da3e966b0d89aab77f Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Sat, 20 Feb 2021 18:26:39 +0800 Subject: [PATCH 04/12] chore: remove useless code --- .../MastodonRegisterViewController.swift | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift b/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift index 300cff5b..749e52ab 100644 --- a/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift +++ b/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift @@ -425,29 +425,6 @@ extension MastodonRegisterViewController: UITextFieldDelegate { textField.layer.shadowPath = UIBezierPath(roundedRect: textField.bounds, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: 2.0, height: 2.0)).cgPath } - func validateTextField(textField: UITextField) -> Bool { - var isvalid = false - if textField == usernameTextField { - isvalid = viewModel.isUsernameValid.value ?? false - } - if textField == displayNameTextField { - isvalid = viewModel.isDisplaynameValid.value ?? false - } - if textField == emailTextField { - isvalid = viewModel.isEmailValid.value ?? false - } - if textField == passwordTextField { - isvalid = viewModel.isPasswordValid.value ?? false - } - if isvalid { - showShadowWithColor(color: Asset.Colors.TextField.successGreen.color, textField: textField) - } else { - textField.shake() - showShadowWithColor(color: Asset.Colors.lightDangerRed.color, textField: textField) - } - return isvalid - } - func validateAllTextField() -> Bool { return viewModel.isUsernameValid.value ?? false && viewModel.isDisplaynameValid.value ?? false && viewModel.isEmailValid.value ?? false && viewModel.isPasswordValid.value ?? false } From 6285cb95faff300d20cbbbbcf06a54c54f31eee6 Mon Sep 17 00:00:00 2001 From: CMK Date: Sat, 20 Feb 2021 19:54:08 +0800 Subject: [PATCH 05/12] fix: AutoLayout issue. Update keyboard listener. Expose server error message --- Mastodon.xcodeproj/project.pbxproj | 4 + .../AuthenticationViewController.swift | 13 ++- .../MastodonRegisterViewController.swift | 99 ++++++++++++------- .../Register/MastodonRegisterViewModel.swift | 9 +- .../Service/KeyboardResponderService.swift | 87 ++++++++++++++++ .../API/Error/Mastodon+API+Error.swift | 24 +++++ 6 files changed, 194 insertions(+), 42 deletions(-) create mode 100644 Mastodon/Service/KeyboardResponderService.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 889bd465..3eac67d4 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -83,6 +83,7 @@ DB427DE225BAA00100D1B89D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DB427DE025BAA00100D1B89D /* LaunchScreen.storyboard */; }; DB427DED25BAA00100D1B89D /* MastodonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DEC25BAA00100D1B89D /* MastodonTests.swift */; }; DB427DF825BAA00100D1B89D /* MastodonUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DF725BAA00100D1B89D /* MastodonUITests.swift */; }; + DB4563BD25E11A24004DA0B9 /* KeyboardResponderService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4563BC25E11A24004DA0B9 /* KeyboardResponderService.swift */; }; DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FAB525CA5485005A8AC7 /* UIAlertController.swift */; }; DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FAD625CA6C76005A8AC7 /* UIBarButtonItem.swift */; }; DB45FADD25CA6F6B005A8AC7 /* APIService+CoreData+MastodonUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FADC25CA6F6B005A8AC7 /* APIService+CoreData+MastodonUser.swift */; }; @@ -266,6 +267,7 @@ DB427DF325BAA00100D1B89D /* MastodonUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MastodonUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; DB427DF725BAA00100D1B89D /* MastodonUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonUITests.swift; sourceTree = ""; }; DB427DF925BAA00100D1B89D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + DB4563BC25E11A24004DA0B9 /* KeyboardResponderService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardResponderService.swift; sourceTree = ""; }; DB45FAB525CA5485005A8AC7 /* UIAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIAlertController.swift; sourceTree = ""; }; DB45FAD625CA6C76005A8AC7 /* UIBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIBarButtonItem.swift; sourceTree = ""; }; DB45FADC25CA6F6B005A8AC7 /* APIService+CoreData+MastodonUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+CoreData+MastodonUser.swift"; sourceTree = ""; }; @@ -444,6 +446,7 @@ children = ( DB45FB0425CA87B4005A8AC7 /* APIService */, DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */, + DB4563BC25E11A24004DA0B9 /* KeyboardResponderService.swift */, ); path = Service; sourceTree = ""; @@ -1247,6 +1250,7 @@ 2D46976425C2A71500CF4AA9 /* UIIamge.swift in Sources */, DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */, 2D3F9E0425DFA133004262D9 /* UITapGestureRecognizer.swift in Sources */, + DB4563BD25E11A24004DA0B9 /* KeyboardResponderService.swift in Sources */, DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */, 2DF75BA125D0E29D00694EC8 /* StatusProvider+TimelinePostTableViewCellDelegate.swift in Sources */, 2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */, diff --git a/Mastodon/Scene/Authentication/AuthenticationViewController.swift b/Mastodon/Scene/Authentication/AuthenticationViewController.swift index e0a56191..b02cf3c4 100644 --- a/Mastodon/Scene/Authentication/AuthenticationViewController.swift +++ b/Mastodon/Scene/Authentication/AuthenticationViewController.swift @@ -273,6 +273,7 @@ extension AuthenticationViewController { } guard viewModel.isIdle.value else { return } viewModel.isRegistering.value = true + context.apiService.instance(domain: domain) .compactMap { [weak self] response -> AnyPublisher, Error>? in guard let self = self else { return nil } @@ -289,9 +290,11 @@ extension AuthenticationViewController { } return authenticateInfo } - .compactMap { [weak self] authenticateInfo -> AnyPublisher, Error>? in + .compactMap { [weak self] authenticateInfo -> AnyPublisher<(Mastodon.Response.Content, AuthenticationViewModel.AuthenticateInfo), Error>? in guard let self = self else { return nil } return self.context.apiService.applicationAccessToken(domain: domain, clientID: authenticateInfo.clientID, clientSecret: authenticateInfo.clientSecret) + .map { ($0, authenticateInfo) } + .eraseToAnyPublisher() } .switchToLatest() .receive(on: DispatchQueue.main) @@ -305,9 +308,13 @@ extension AuthenticationViewController { case .finished: break } - } receiveValue: { [weak self] response in + } receiveValue: { [weak self] response, authenticateInfo in guard let self = self else { return } - let mastodonRegisterViewModel = MastodonRegisterViewModel(domain: domain, applicationToken: response.value) + let mastodonRegisterViewModel = MastodonRegisterViewModel( + domain: domain, + authenticateInfo: authenticateInfo, + applicationToken: response.value + ) self.coordinator.present(scene: .mastodonRegister(viewModel: mastodonRegisterViewModel), from: self, transition: .show) } .store(in: &disposeBag) diff --git a/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift b/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift index 749e52ab..5361b311 100644 --- a/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift +++ b/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift @@ -20,15 +20,13 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency { var viewModel: MastodonRegisterViewModel! let tapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer - let stackViewTopDistance: CGFloat = 16 - - var keyboardFrame: CGRect! var scrollview: UIScrollView = { let scrollview = UIScrollView() scrollview.showsVerticalScrollIndicator = false scrollview.translatesAutoresizingMaskIntoConstraints = false + scrollview.keyboardDismissMode = .interactive return scrollview }() @@ -226,21 +224,21 @@ extension MastodonRegisterViewController { view.addSubview(scrollview) NSLayoutConstraint.activate([ scrollview.frameLayoutGuide.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor), - scrollview.frameLayoutGuide.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), - view.trailingAnchor.constraint(equalTo: scrollview.frameLayoutGuide.trailingAnchor, constant: 20), + scrollview.frameLayoutGuide.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor), + view.readableContentGuide.trailingAnchor.constraint(equalTo: scrollview.frameLayoutGuide.trailingAnchor), scrollview.frameLayoutGuide.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor), + scrollview.frameLayoutGuide.widthAnchor.constraint(equalTo: scrollview.contentLayoutGuide.widthAnchor), ]) - + // stackview scrollview.addSubview(stackView) stackView.translatesAutoresizingMaskIntoConstraints = false - let bottomEdgeLayoutConstraint: NSLayoutConstraint = scrollview.contentLayoutGuide.bottomAnchor.constraint(equalTo: stackView.bottomAnchor) NSLayoutConstraint.activate([ stackView.topAnchor.constraint(equalTo: scrollview.contentLayoutGuide.topAnchor, constant: stackViewTopDistance), stackView.leadingAnchor.constraint(equalTo: scrollview.contentLayoutGuide.leadingAnchor), stackView.trailingAnchor.constraint(equalTo: scrollview.contentLayoutGuide.trailingAnchor), stackView.widthAnchor.constraint(equalTo: scrollview.frameLayoutGuide.widthAnchor), - bottomEdgeLayoutConstraint, + scrollview.contentLayoutGuide.bottomAnchor.constraint(equalTo: stackView.bottomAnchor), ]) // photoview @@ -268,7 +266,7 @@ extension MastodonRegisterViewController { plusIcon.trailingAnchor.constraint(equalTo: photoButton.trailingAnchor), plusIcon.bottomAnchor.constraint(equalTo: photoButton.bottomAnchor), ]) - + // textfield NSLayoutConstraint.activate([ usernameTextField.heightAnchor.constraint(equalToConstant: 50).priority(.defaultHigh), @@ -280,34 +278,49 @@ extension MastodonRegisterViewController { // password stackView.setCustomSpacing(6, after: passwordTextField) stackView.setCustomSpacing(32, after: passwordCheckLabel) - + // button signUpButton.translatesAutoresizingMaskIntoConstraints = false stackView.addArrangedSubview(signUpButton) NSLayoutConstraint.activate([ signUpButton.heightAnchor.constraint(equalToConstant: 44).priority(.defaultHigh), ]) - + signUpActivityIndicatorView.translatesAutoresizingMaskIntoConstraints = false scrollview.addSubview(signUpActivityIndicatorView) NSLayoutConstraint.activate([ signUpActivityIndicatorView.centerXAnchor.constraint(equalTo: signUpButton.centerXAnchor), signUpActivityIndicatorView.centerYAnchor.constraint(equalTo: signUpButton.centerYAnchor), ]) - - NotificationCenter.default.publisher(for: UIResponder.keyboardWillChangeFrameNotification, object: nil) - .sink { [weak self] notification in - guard let endFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { - return - } - self?.keyboardFrame = endFrame - UIView.animate(withDuration: 0.3) { - bottomEdgeLayoutConstraint.constant = UIScreen.main.bounds.height - endFrame.origin.y + 26 - self?.view.layoutIfNeeded() - } + + Publishers.CombineLatest3( + KeyboardResponderService.shared.isShow.eraseToAnyPublisher(), + KeyboardResponderService.shared.state.eraseToAnyPublisher(), + KeyboardResponderService.shared.endFrame.eraseToAnyPublisher() + ) + .sink(receiveValue: { [weak self] isShow, state, endFrame in + guard let self = self else { return } + + guard isShow, state == .dock else { + self.scrollview.contentInset.bottom = 0.0 + self.scrollview.verticalScrollIndicatorInsets.bottom = 0.0 + return } - .store(in: &disposeBag) - + + // isShow AND dock state + let contentFrame = self.view.convert(self.scrollview.frame, to: nil) + let padding = contentFrame.maxY - endFrame.minY + guard padding > 0 else { + self.scrollview.contentInset.bottom = 0.0 + self.scrollview.verticalScrollIndicatorInsets.bottom = 0.0 + return + } + + self.scrollview.contentInset.bottom = padding + 16 + self.scrollview.verticalScrollIndicatorInsets.bottom = padding + 16 + }) + .store(in: &disposeBag) + viewModel.isRegistering .receive(on: DispatchQueue.main) .sink { [weak self] isRegistering in @@ -317,7 +330,7 @@ extension MastodonRegisterViewController { self.signUpButton.isEnabled = !isRegistering } .store(in: &disposeBag) - + viewModel.isUsernameValid .receive(on: DispatchQueue.main) .sink { [weak self] isValid in @@ -359,7 +372,7 @@ extension MastodonRegisterViewController { self.signUpButton.isEnabled = isUsernameValid ?? false && isDisplaynameValid ?? false && isEmailValid ?? false && isPasswordValid ?? false } .store(in: &disposeBag) - + viewModel.error .compactMap { $0 } .receive(on: DispatchQueue.main) @@ -375,6 +388,7 @@ extension MastodonRegisterViewController { ) } .store(in: &disposeBag) + NotificationCenter.default .publisher(for: UITextField.textDidChangeNotification, object: passwordTextField) .receive(on: DispatchQueue.main) @@ -385,20 +399,26 @@ extension MastodonRegisterViewController { self.passwordCheckLabel.attributedText = self.viewModel.attributeStringForPassword(eightCharacters: validations.0, oneNumber: validations.1, oneSpecialCharacter: validations.2) } .store(in: &disposeBag) - + signUpButton.addTarget(self, action: #selector(MastodonRegisterViewController.signUpButtonPressed(_:)), for: .touchUpInside) } } extension MastodonRegisterViewController: UITextFieldDelegate { func textFieldDidBeginEditing(_ textField: UITextField) { - var bottomOffsetY: CGFloat = textField.frame.origin.y + textField.frame.height - scrollview.frame.height + keyboardFrame.size.height + stackViewTopDistance - if textField == passwordTextField { - bottomOffsetY += passwordCheckLabel.frame.height - } - - if bottomOffsetY > 0 { - scrollview.setContentOffset(CGPoint(x: 0, y: bottomOffsetY), animated: true) + // align to password label when overlap + if textField === passwordTextField, + KeyboardResponderService.shared.isShow.value, + KeyboardResponderService.shared.state.value == .dock { + let endFrame = KeyboardResponderService.shared.endFrame.value + let contentFrame = self.scrollview.convert(self.passwordCheckLabel.frame, to: nil) + let padding = contentFrame.maxY - endFrame.minY + if padding > 0 { + let contentOffsetY = scrollview.contentOffset.y + DispatchQueue.main.async { + self.scrollview.setContentOffset(CGPoint(x: 0, y: contentOffsetY + padding + 16.0), animated: true) + } + } } } @@ -454,7 +474,10 @@ extension MastodonRegisterViewController { @objc private func signUpButtonPressed(_ sender: UIButton) { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", (#file as NSString).lastPathComponent, #line, #function) - if !validateAllTextField() { + guard validateAllTextField(), + let username = viewModel.username.value, + let email = viewModel.email.value, + let password = viewModel.password.value else { return } @@ -463,9 +486,9 @@ extension MastodonRegisterViewController { let query = Mastodon.API.Account.RegisterQuery( reason: nil, - username: viewModel.username.value!, - email: viewModel.email.value!, - password: viewModel.password.value!, + username: username, + email: email, + password: password, agreement: true, // TODO: locale: "en" // TODO: ) diff --git a/Mastodon/Scene/Authentication/Register/MastodonRegisterViewModel.swift b/Mastodon/Scene/Authentication/Register/MastodonRegisterViewModel.swift index 590f2c0d..944cb9b0 100644 --- a/Mastodon/Scene/Authentication/Register/MastodonRegisterViewModel.swift +++ b/Mastodon/Scene/Authentication/Register/MastodonRegisterViewModel.swift @@ -15,7 +15,9 @@ final class MastodonRegisterViewModel { // input let domain: String + let authenticateInfo: AuthenticationViewModel.AuthenticateInfo let applicationToken: Mastodon.Entity.Token + let isRegistering = CurrentValueSubject(false) let username = CurrentValueSubject(nil) let displayname = CurrentValueSubject(nil) @@ -32,8 +34,13 @@ final class MastodonRegisterViewModel { let error = CurrentValueSubject(nil) - init(domain: String, applicationToken: Mastodon.Entity.Token) { + init( + domain: String, + authenticateInfo: AuthenticationViewModel.AuthenticateInfo, + applicationToken: Mastodon.Entity.Token + ) { self.domain = domain + self.authenticateInfo = authenticateInfo self.applicationToken = applicationToken self.applicationAuthorization = Mastodon.API.OAuth.Authorization(accessToken: applicationToken.accessToken) diff --git a/Mastodon/Service/KeyboardResponderService.swift b/Mastodon/Service/KeyboardResponderService.swift new file mode 100644 index 00000000..1711680a --- /dev/null +++ b/Mastodon/Service/KeyboardResponderService.swift @@ -0,0 +1,87 @@ +// +// KeyboardResponderService.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-2-20. +// + +import UIKit +import Combine + +final class KeyboardResponderService { + + var disposeBag = Set() + + // MARK: - Singleton + public static let shared = KeyboardResponderService() + + // output + let isShow = CurrentValueSubject(false) + let state = CurrentValueSubject(.none) + let endFrame = CurrentValueSubject(.zero) + + private init() { + NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification, object: nil) + .sink { notification in + self.isShow.value = true + self.updateInternalStatus(notification: notification) + } + .store(in: &disposeBag) + + NotificationCenter.default.publisher(for: UIResponder.keyboardDidHideNotification, object: nil) + .sink { notification in + self.isShow.value = false + self.updateInternalStatus(notification: notification) + } + .store(in: &disposeBag) + + NotificationCenter.default.publisher(for: UIResponder.keyboardDidChangeFrameNotification, object: nil) + .sink { notification in + self.updateInternalStatus(notification: notification) + } + .store(in: &disposeBag) + } + +} + +extension KeyboardResponderService { + + private func updateInternalStatus(notification: Notification) { + guard let isLocal = notification.userInfo?[UIWindow.keyboardIsLocalUserInfoKey] as? Bool, + let endFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { + return + } + + self.endFrame.value = endFrame + + guard isLocal else { + self.state.value = .notLocal + return + } + + // check if floating + guard endFrame.width == UIScreen.main.bounds.width else { + self.state.value = .floating + return + } + + // check if undock | split + let dockMinY = UIScreen.main.bounds.height - endFrame.height + if endFrame.minY < dockMinY { + self.state.value = .notDock + } else { + self.state.value = .dock + } + } + +} + +extension KeyboardResponderService { + enum KeyboardState { + case none + case notLocal + case notDock // undock | split + case floating // iPhone size floating + case dock + } +} diff --git a/MastodonSDK/Sources/MastodonSDK/API/Error/Mastodon+API+Error.swift b/MastodonSDK/Sources/MastodonSDK/API/Error/Mastodon+API+Error.swift index 94d063c4..ee649b43 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Error/Mastodon+API+Error.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Error/Mastodon+API+Error.swift @@ -34,3 +34,27 @@ extension Mastodon.API { } } + +extension Mastodon.API.Error: LocalizedError { + + public var errorDescription: String? { + guard let mastodonError = mastodonError else { + return nil + } + switch mastodonError { + case .generic(let error): + return error.error + } + } + + public var failureReason: String? { + guard let mastodonError = mastodonError else { + return nil + } + switch mastodonError { + case .generic(let error): + return error.errorDescription + } + } + +} From 8a48eb58479a185752f9d9ceca8554eda5066833 Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 22 Feb 2021 12:26:44 +0800 Subject: [PATCH 06/12] feat: add rules attribute for Instance entity --- MastodonSDK.xctestplan | 24 +++++++++++++- .../Entity/Mastodon+Entity+Instance.swift | 13 ++++++-- .../API/MastodonSDK+API+Instance.swift | 32 +++++++++++++++++++ 3 files changed, 66 insertions(+), 3 deletions(-) diff --git a/MastodonSDK.xctestplan b/MastodonSDK.xctestplan index 35fa310e..bfea8bd7 100644 --- a/MastodonSDK.xctestplan +++ b/MastodonSDK.xctestplan @@ -47,6 +47,18 @@ } ] } + }, + { + "id" : "C229D9A6-6A83-4AF9-8A71-ADD1AD2AD9D8", + "name" : "mastodon.online", + "options" : { + "environmentVariableEntries" : [ + { + "key" : "domain", + "value" : "mastodon.online" + } + ] + } } ], "defaultOptions" : { @@ -54,8 +66,18 @@ }, "testTargets" : [ { + "skippedTests" : [ + "MastodonSDKTests\/testCreateAnAnpplication()", + "MastodonSDKTests\/testHomeTimeline()", + "MastodonSDKTests\/testOAuthAuthorize()", + "MastodonSDKTests\/testRetrieveAccountInfo()", + "MastodonSDKTests\/testRevokeToken()", + "MastodonSDKTests\/testUpdateCredentials()", + "MastodonSDKTests\/testVerifyAppCredentials()", + "MastodonSDKTests\/testVerifyCredentials()" + ], "target" : { - "containerPath" : "container:", + "containerPath" : "container:MastodonSDK", "identifier" : "MastodonSDKTests", "name" : "MastodonSDKTests" } diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Instance.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Instance.swift index 3154d63f..56984f84 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Instance.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Instance.swift @@ -13,7 +13,7 @@ extension Mastodon.Entity { /// - Since: 1.1.0 /// - Version: 3.3.0 /// # Last Update - /// 2021/2/5 + /// 2021/2/22 /// # Reference /// [Document](https://docs.joinmastodon.org/entities/instance/) public struct Instance: Codable { @@ -33,6 +33,7 @@ extension Mastodon.Entity { public let thumbnail: String? public let contactAccount: Account? + public let rules: [Rule]? enum CodingKeys: String, CodingKey { case uri @@ -48,8 +49,9 @@ extension Mastodon.Entity { case urls case statistics - case thumbnail = "thumbnail" + case thumbnail case contactAccount = "contact_account" + case rules } } } @@ -77,3 +79,10 @@ extension Mastodon.Entity.Instance { } } } + +extension Mastodon.Entity.Instance { + public struct Rule: Codable { + public let id: String + public let text: String + } +} diff --git a/MastodonSDK/Tests/MastodonSDKTests/API/MastodonSDK+API+Instance.swift b/MastodonSDK/Tests/MastodonSDKTests/API/MastodonSDK+API+Instance.swift index 211c70d0..ee02cf07 100644 --- a/MastodonSDK/Tests/MastodonSDKTests/API/MastodonSDK+API+Instance.swift +++ b/MastodonSDK/Tests/MastodonSDKTests/API/MastodonSDK+API+Instance.swift @@ -37,5 +37,37 @@ extension MastodonSDKTests { wait(for: [theExpectation], timeout: 10.0) } + + func testInstanceRules() throws { + switch domain { + case "mastodon.online": break + default: return + } + + try _testInstanceRules(domain: domain) + } + + func _testInstanceRules(domain: String) throws { + let theExpectation = expectation(description: "Fetch Instance Infomation") + + Mastodon.API.Instance.instance(session: session, domain: domain) + .receive(on: DispatchQueue.main) + .sink { completion in + switch completion { + case .failure(let error): + XCTFail(error.localizedDescription) + case .finished: + break + } + } receiveValue: { response in + XCTAssertNotEqual(response.value.uri, "") + XCTAssert(!(response.value.rules ?? []).isEmpty) + print(response.value.rules?.sorted(by: { $0.id < $1.id }) ?? "") + theExpectation.fulfill() + } + .store(in: &disposeBag) + + wait(for: [theExpectation], timeout: 10.0) + } } From e4989cfe305c11ca827e3f7863921b2de43cc066 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Mon, 22 Feb 2021 12:51:35 +0800 Subject: [PATCH 07/12] chore: add keyboardWillShowNotification to KeyboardResponderService --- .../MastodonRegisterViewController.swift | 21 +++++++++---------- .../Service/KeyboardResponderService.swift | 14 ++++++++++--- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift b/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift index 5361b311..6eda242b 100644 --- a/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift +++ b/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift @@ -142,7 +142,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency { let passwordCheckLabel: UILabel = { let label = UILabel() - label.numberOfLines = 4 + label.numberOfLines = 0 return label }() @@ -210,7 +210,7 @@ extension MastodonRegisterViewController { stackView.axis = .vertical stackView.distribution = .fill stackView.spacing = 40 - stackView.layoutMargins = UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 4) + stackView.layoutMargins = UIEdgeInsets(top: 0, left: 4, bottom: 26, right: 4) stackView.isLayoutMarginsRelativeArrangement = true stackView.addArrangedSubview(largeTitleLabel) stackView.addArrangedSubview(photoView) @@ -293,21 +293,19 @@ extension MastodonRegisterViewController { signUpActivityIndicatorView.centerYAnchor.constraint(equalTo: signUpButton.centerYAnchor), ]) - Publishers.CombineLatest3( - KeyboardResponderService.shared.isShow.eraseToAnyPublisher(), + Publishers.CombineLatest( KeyboardResponderService.shared.state.eraseToAnyPublisher(), - KeyboardResponderService.shared.endFrame.eraseToAnyPublisher() + KeyboardResponderService.shared.willEndFrame.eraseToAnyPublisher() ) - .sink(receiveValue: { [weak self] isShow, state, endFrame in + .sink(receiveValue: { [weak self] state, endFrame in guard let self = self else { return } - guard isShow, state == .dock else { + guard state == .dock else { self.scrollview.contentInset.bottom = 0.0 self.scrollview.verticalScrollIndicatorInsets.bottom = 0.0 return } - // isShow AND dock state let contentFrame = self.view.convert(self.scrollview.frame, to: nil) let padding = contentFrame.maxY - endFrame.minY guard padding > 0 else { @@ -409,9 +407,10 @@ extension MastodonRegisterViewController: UITextFieldDelegate { // align to password label when overlap if textField === passwordTextField, KeyboardResponderService.shared.isShow.value, - KeyboardResponderService.shared.state.value == .dock { - let endFrame = KeyboardResponderService.shared.endFrame.value - let contentFrame = self.scrollview.convert(self.passwordCheckLabel.frame, to: nil) + KeyboardResponderService.shared.state.value == .dock + { + let endFrame = KeyboardResponderService.shared.willEndFrame.value + let contentFrame = scrollview.convert(passwordCheckLabel.frame, to: nil) let padding = contentFrame.maxY - endFrame.minY if padding > 0 { let contentOffsetY = scrollview.contentOffset.y diff --git a/Mastodon/Service/KeyboardResponderService.swift b/Mastodon/Service/KeyboardResponderService.swift index 1711680a..b2173796 100644 --- a/Mastodon/Service/KeyboardResponderService.swift +++ b/Mastodon/Service/KeyboardResponderService.swift @@ -18,7 +18,8 @@ final class KeyboardResponderService { // output let isShow = CurrentValueSubject(false) let state = CurrentValueSubject(.none) - let endFrame = CurrentValueSubject(.zero) + let didEndFrame = CurrentValueSubject(.zero) + let willEndFrame = CurrentValueSubject(.zero) private init() { NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification, object: nil) @@ -37,6 +38,15 @@ final class KeyboardResponderService { NotificationCenter.default.publisher(for: UIResponder.keyboardDidChangeFrameNotification, object: nil) .sink { notification in + guard let endFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return } + self.didEndFrame.value = endFrame + self.updateInternalStatus(notification: notification) + } + .store(in: &disposeBag) + NotificationCenter.default.publisher(for: UIResponder.keyboardWillChangeFrameNotification, object: nil) + .sink { notification in + guard let endFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return } + self.willEndFrame.value = endFrame self.updateInternalStatus(notification: notification) } .store(in: &disposeBag) @@ -52,8 +62,6 @@ extension KeyboardResponderService { return } - self.endFrame.value = endFrame - guard isLocal else { self.state.value = .notLocal return From 34191c921a5201f3cc5c07fce6c2fa9238615bf0 Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 22 Feb 2021 16:20:23 +0800 Subject: [PATCH 08/12] feat: add localization helper --- Localization/README.md | 8 ++ Localization/StringsConvertor/Package.swift | 25 +++++ Localization/StringsConvertor/README.md | 12 +++ .../Sources/StringsConvertor/Parser.swift | 100 ++++++++++++++++++ .../Sources/StringsConvertor/main.swift | 74 +++++++++++++ .../StringsConvertor/Tests/LinuxMain.swift | 7 ++ .../StringsConvertorTests.swift | 47 ++++++++ .../XCTestManifests.swift | 9 ++ .../StringsConvertor/input/en_US/app.json | 78 ++++++++++++++ .../input/en_US/ios-infoPlist.json | 4 + .../output/en.lproj/Localizable.strings | 37 +++++++ .../output/en.lproj/infoPlist.strings | 2 + .../StringsConvertor/scripts/build.sh | 28 +++++ Localization/app.json | 78 ++++++++++++++ Localization/ios-infoPlist.json | 4 + Mastodon/Info.plist | 2 - 16 files changed, 513 insertions(+), 2 deletions(-) create mode 100644 Localization/README.md create mode 100644 Localization/StringsConvertor/Package.swift create mode 100644 Localization/StringsConvertor/README.md create mode 100644 Localization/StringsConvertor/Sources/StringsConvertor/Parser.swift create mode 100644 Localization/StringsConvertor/Sources/StringsConvertor/main.swift create mode 100644 Localization/StringsConvertor/Tests/LinuxMain.swift create mode 100644 Localization/StringsConvertor/Tests/StringsConvertorTests/StringsConvertorTests.swift create mode 100644 Localization/StringsConvertor/Tests/StringsConvertorTests/XCTestManifests.swift create mode 100644 Localization/StringsConvertor/input/en_US/app.json create mode 100644 Localization/StringsConvertor/input/en_US/ios-infoPlist.json create mode 100644 Localization/StringsConvertor/output/en.lproj/Localizable.strings create mode 100644 Localization/StringsConvertor/output/en.lproj/infoPlist.strings create mode 100755 Localization/StringsConvertor/scripts/build.sh create mode 100644 Localization/app.json create mode 100644 Localization/ios-infoPlist.json diff --git a/Localization/README.md b/Localization/README.md new file mode 100644 index 00000000..1e6975f8 --- /dev/null +++ b/Localization/README.md @@ -0,0 +1,8 @@ +# Localization + +Mastodon localization template file + + +## How to contribute? + +TBD \ No newline at end of file diff --git a/Localization/StringsConvertor/Package.swift b/Localization/StringsConvertor/Package.swift new file mode 100644 index 00000000..c4192456 --- /dev/null +++ b/Localization/StringsConvertor/Package.swift @@ -0,0 +1,25 @@ +// swift-tools-version:5.2 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "StringsConvertor", + platforms: [ + .macOS(.v10_15) + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + // .package(url: /* package url */, from: "1.0.0"), + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages which this package depends on. + .target( + name: "StringsConvertor", + dependencies: []), + .testTarget( + name: "StringsConvertorTests", + dependencies: ["StringsConvertor"]), + ] +) diff --git a/Localization/StringsConvertor/README.md b/Localization/StringsConvertor/README.md new file mode 100644 index 00000000..62df25d9 --- /dev/null +++ b/Localization/StringsConvertor/README.md @@ -0,0 +1,12 @@ +# StringsConvertor + +Convert i18n JSON file to Stings file. + + +## Usage +``` +chmod +x scripts/build.sh +./scripts/build.sh + +# lproj files will locate in output/ directory +``` diff --git a/Localization/StringsConvertor/Sources/StringsConvertor/Parser.swift b/Localization/StringsConvertor/Sources/StringsConvertor/Parser.swift new file mode 100644 index 00000000..ba9750cd --- /dev/null +++ b/Localization/StringsConvertor/Sources/StringsConvertor/Parser.swift @@ -0,0 +1,100 @@ +// +// File.swift +// +// +// Created by Cirno MainasuK on 2020-7-7. +// + +import Foundation + +class Parser { + + let json: [String: Any] + + init(data: Data) throws { + let dict = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] + self.json = dict ?? [:] + } + + +} + +extension Parser { + enum KeyStyle { + case infoPlist + case swiftgen + } +} + +extension Parser { + + func generateStrings(keyStyle: KeyStyle = .swiftgen) -> String { + let pairs = traval(dictionary: json, prefixKeys: []) + + var lines: [String] = [] + for pair in pairs { + let key = [ + "\"", + pair.prefix + .map { segment in + segment + .split(separator: "_") + .map { String($0) } + .map { + switch keyStyle { + case .infoPlist: return $0 + case .swiftgen: return $0.capitalized + } + } + .joined() + } + .joined(separator: "."), + "\"" + ].joined() + let value = [ + "\"", + pair.value.replacingOccurrences(of: "%s", with: "%@"), + "\"" + ].joined() + let line = [ + [key, value].joined(separator: " = "), + ";" + ].joined() + + lines.append(line) + } + + let strings = lines + .sorted() + .joined(separator: "\n") + return strings + } + +} + +extension Parser { + + typealias PrefixKeys = [String] + typealias LocalizationPair = (prefix: PrefixKeys, value: String) + + private func traval(dictionary: [String: Any], prefixKeys: PrefixKeys) -> [LocalizationPair] { + var pairs: [LocalizationPair] = [] + for (key, any) in dictionary { + let prefix = prefixKeys + [key] + + // if leaf node of dict tree + if let value = any as? String { + pairs.append(LocalizationPair(prefix: prefix, value: value)) + continue + } + + // if not leaf node of dict tree + if let dict = any as? [String: Any] { + let innerPairs = traval(dictionary: dict, prefixKeys: prefix) + pairs.append(contentsOf: innerPairs) + } + } + return pairs + } + +} diff --git a/Localization/StringsConvertor/Sources/StringsConvertor/main.swift b/Localization/StringsConvertor/Sources/StringsConvertor/main.swift new file mode 100644 index 00000000..4ccbb307 --- /dev/null +++ b/Localization/StringsConvertor/Sources/StringsConvertor/main.swift @@ -0,0 +1,74 @@ +import os.log +import Foundation + +let currentFileURL = URL(fileURLWithPath: "\(#file)", isDirectory: false) +let packageRootURL = currentFileURL.deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent() +let inputDirectoryURL = packageRootURL.appendingPathComponent("input", isDirectory: true) +let outputDirectoryURL = packageRootURL.appendingPathComponent("output", isDirectory: true) + +private func convert(from inputDirectory: URL, to outputDirectory: URL) { + do { + let inputLanguageDirectoryURLs = try FileManager.default.contentsOfDirectory( + at: inputDirectoryURL, + includingPropertiesForKeys: [.nameKey, .isDirectoryKey], + options: [] + ) + for inputLanguageDirectoryURL in inputLanguageDirectoryURLs { + let language = inputLanguageDirectoryURL.lastPathComponent + guard let mappedLanguage = map(language: language) else { continue } + let outputDirectoryURL = outputDirectory.appendingPathComponent(mappedLanguage + ".lproj", isDirectory: true) + os_log("%{public}s[%{public}ld], %{public}s: process %s -> %s", ((#file as NSString).lastPathComponent), #line, #function, language, mappedLanguage) + + let fileURLs = try FileManager.default.contentsOfDirectory( + at: inputLanguageDirectoryURL, + includingPropertiesForKeys: [.nameKey, .isDirectoryKey], + options: [] + ) + for jsonURL in fileURLs where jsonURL.pathExtension == "json" { + os_log("%{public}s[%{public}ld], %{public}s: process %s", ((#file as NSString).lastPathComponent), #line, #function, jsonURL.debugDescription) + let filename = jsonURL.deletingPathExtension().lastPathComponent + guard let (mappedFilename, keyStyle) = map(filename: filename) else { continue } + let outputFileURL = outputDirectoryURL.appendingPathComponent(mappedFilename).appendingPathExtension("strings") + let strings = try process(url: jsonURL, keyStyle: keyStyle) + try? FileManager.default.createDirectory(at: outputDirectoryURL, withIntermediateDirectories: true, attributes: nil) + try strings.write(to: outputFileURL, atomically: true, encoding: .utf8) + } + } + } catch { + os_log("%{public}s[%{public}ld], %{public}s: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) + exit(1) + } +} + +private func map(language: String) -> String? { + switch language { + case "en_US": return "en" + case "zh_CN": return "zh-Hans" + case "ja_JP": return "ja" + case "de_DE": return "de" + case "pt_BR": return "pt-BR" + default: return nil + } +} + +private func map(filename: String) -> (filename: String, keyStyle: Parser.KeyStyle)? { + switch filename { + case "app": return ("Localizable", .swiftgen) + case "ios-infoPlist": return ("infoPlist", .infoPlist) + default: return nil + } +} + +private func process(url: URL, keyStyle: Parser.KeyStyle) throws -> String { + do { + let data = try Data(contentsOf: url) + let parser = try Parser(data: data) + let strings = parser.generateStrings(keyStyle: keyStyle) + return strings + } catch { + os_log("%{public}s[%{public}ld], %{public}s: error: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) + throw error + } +} + +convert(from: inputDirectoryURL, to: outputDirectoryURL) diff --git a/Localization/StringsConvertor/Tests/LinuxMain.swift b/Localization/StringsConvertor/Tests/LinuxMain.swift new file mode 100644 index 00000000..7087778c --- /dev/null +++ b/Localization/StringsConvertor/Tests/LinuxMain.swift @@ -0,0 +1,7 @@ +import XCTest + +import StringsConvertorTests + +var tests = [XCTestCaseEntry]() +tests += StringsConvertorTests.allTests() +XCTMain(tests) diff --git a/Localization/StringsConvertor/Tests/StringsConvertorTests/StringsConvertorTests.swift b/Localization/StringsConvertor/Tests/StringsConvertorTests/StringsConvertorTests.swift new file mode 100644 index 00000000..cb59f3bb --- /dev/null +++ b/Localization/StringsConvertor/Tests/StringsConvertorTests/StringsConvertorTests.swift @@ -0,0 +1,47 @@ +import XCTest +import class Foundation.Bundle + +final class StringsConvertorTests: XCTestCase { + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct + // results. + + // Some of the APIs that we use below are available in macOS 10.13 and above. + guard #available(macOS 10.13, *) else { + return + } + + let fooBinary = productsDirectory.appendingPathComponent("StringsConvertor") + + let process = Process() + process.executableURL = fooBinary + + let pipe = Pipe() + process.standardOutput = pipe + + try process.run() + process.waitUntilExit() + + let data = pipe.fileHandleForReading.readDataToEndOfFile() + let output = String(data: data, encoding: .utf8) + + XCTAssertEqual(output, "Hello, world!\n") + } + + /// Returns path to the built products directory. + var productsDirectory: URL { + #if os(macOS) + for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") { + return bundle.bundleURL.deletingLastPathComponent() + } + fatalError("couldn't find the products directory") + #else + return Bundle.main.bundleURL + #endif + } + + static var allTests = [ + ("testExample", testExample), + ] +} diff --git a/Localization/StringsConvertor/Tests/StringsConvertorTests/XCTestManifests.swift b/Localization/StringsConvertor/Tests/StringsConvertorTests/XCTestManifests.swift new file mode 100644 index 00000000..81a65399 --- /dev/null +++ b/Localization/StringsConvertor/Tests/StringsConvertorTests/XCTestManifests.swift @@ -0,0 +1,9 @@ +import XCTest + +#if !canImport(ObjectiveC) +public func allTests() -> [XCTestCaseEntry] { + return [ + testCase(StringsConvertorTests.allTests), + ] +} +#endif diff --git a/Localization/StringsConvertor/input/en_US/app.json b/Localization/StringsConvertor/input/en_US/app.json new file mode 100644 index 00000000..0c3f16c7 --- /dev/null +++ b/Localization/StringsConvertor/input/en_US/app.json @@ -0,0 +1,78 @@ +{ + "common": { + "alerts": {}, + "controls": { + "actions": { + "add": "Add", + "remove": "Remove", + "edit": "Edit", + "save": "Save", + "ok": "OK", + "confirm": "Confirm", + "continue": "Continue", + "cancel": "Cancel", + "take_photo": "Take photo", + "save_photo": "Save photo", + "sign_in": "Sign in", + "sign_up": "Sign up", + "see_more": "See More", + "preview": "Preview", + "open_in_safari": "Open in Safari" + }, + "timeline": { + "load_more": "Load More" + } + }, + "countable": { + "photo": { + "single": "photo", + "multiple": "photos" + } + } + }, + "scene": { + "welcome": { + "slogan": "Social networking\nback in your hands." + }, + "server_picker": { + "title": "Pick a Server,\nany server.", + "input": { + "placeholder": "Find a server or join your own..." + } + }, + "register": { + "title": "Tell us about you.", + "input": { + "username": { + "placeholder": "username", + "duplicate_prompt": "This username is taken." + }, + "display_name": { + "placeholder": "display name" + }, + "email": { + "placeholder": "email" + }, + "password": { + "placeholder": "password", + "prompt": "Your password needs at least:", + "prompt_eight_characters": "Eight characters" + } + } + }, + "server_rules": { + "title": "Some ground rules.", + "subtitle": "These rules are set by the admins of %s.", + "prompt": "By continuing, you're subject to the terms of service and privacy policy for %s.", + "button": { + "confirm": "I Agree" + } + }, + "home_timeline": { + "title": "Home" + }, + "public_timeline": { + "title": "Public" + } + } +} \ No newline at end of file diff --git a/Localization/StringsConvertor/input/en_US/ios-infoPlist.json b/Localization/StringsConvertor/input/en_US/ios-infoPlist.json new file mode 100644 index 00000000..0a260c27 --- /dev/null +++ b/Localization/StringsConvertor/input/en_US/ios-infoPlist.json @@ -0,0 +1,4 @@ +{ + "NSCameraUsageDescription": "Used to take photo for toot", + "NSPhotoLibraryAddUsageDescription": "Used to save photo into the Photo Library" +} diff --git a/Localization/StringsConvertor/output/en.lproj/Localizable.strings b/Localization/StringsConvertor/output/en.lproj/Localizable.strings new file mode 100644 index 00000000..707ef3cc --- /dev/null +++ b/Localization/StringsConvertor/output/en.lproj/Localizable.strings @@ -0,0 +1,37 @@ +"Common.Controls.Actions.Add" = "Add"; +"Common.Controls.Actions.Cancel" = "Cancel"; +"Common.Controls.Actions.Confirm" = "Confirm"; +"Common.Controls.Actions.Continue" = "Continue"; +"Common.Controls.Actions.Edit" = "Edit"; +"Common.Controls.Actions.Ok" = "OK"; +"Common.Controls.Actions.OpenInSafari" = "Open in Safari"; +"Common.Controls.Actions.Preview" = "Preview"; +"Common.Controls.Actions.Remove" = "Remove"; +"Common.Controls.Actions.Save" = "Save"; +"Common.Controls.Actions.SavePhoto" = "Save photo"; +"Common.Controls.Actions.SeeMore" = "See More"; +"Common.Controls.Actions.SignIn" = "Sign in"; +"Common.Controls.Actions.SignUp" = "Sign up"; +"Common.Controls.Actions.TakePhoto" = "Take photo"; +"Common.Controls.Timeline.LoadMore" = "Load More"; +"Common.Countable.Photo.Multiple" = "photos"; +"Common.Countable.Photo.Single" = "photo"; +"Scene.HomeTimeline.Title" = "Home"; +"Scene.PublicTimeline.Title" = "Public"; +"Scene.Register.Input.DisplayName.Placeholder" = "display name"; +"Scene.Register.Input.Email.Placeholder" = "email"; +"Scene.Register.Input.Password.Placeholder" = "password"; +"Scene.Register.Input.Password.Prompt" = "Your password needs at least:"; +"Scene.Register.Input.Password.PromptEightCharacters" = "Eight characters"; +"Scene.Register.Input.Username.DuplicatePrompt" = "This username is taken."; +"Scene.Register.Input.Username.Placeholder" = "username"; +"Scene.Register.Title" = "Tell us about you."; +"Scene.ServerPicker.Input.Placeholder" = "Find a server or join your own..."; +"Scene.ServerPicker.Title" = "Pick a Server, +any server."; +"Scene.ServerRules.Button.Confirm" = "I Agree"; +"Scene.ServerRules.Prompt" = "By continuing, you're subject to the terms of service and privacy policy for %@."; +"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."; \ No newline at end of file diff --git a/Localization/StringsConvertor/output/en.lproj/infoPlist.strings b/Localization/StringsConvertor/output/en.lproj/infoPlist.strings new file mode 100644 index 00000000..972e1a7a --- /dev/null +++ b/Localization/StringsConvertor/output/en.lproj/infoPlist.strings @@ -0,0 +1,2 @@ +"NSCameraUsageDescription" = "Used to take photo for toot"; +"NSPhotoLibraryAddUsageDescription" = "Used to save photo into the Photo Library"; \ No newline at end of file diff --git a/Localization/StringsConvertor/scripts/build.sh b/Localization/StringsConvertor/scripts/build.sh new file mode 100755 index 00000000..81e17745 --- /dev/null +++ b/Localization/StringsConvertor/scripts/build.sh @@ -0,0 +1,28 @@ +#!/bin/zsh + +set -ev + +# Crowin_Latest_Build="https://crowdin.com/backend/download/project/.zip" + +if [[ -d input ]]; then + rm -rf input +fi + +if [[ -d output ]]; then + rm -rf output +fi +mkdir output + + +# FIXME: temporary use local json for i18n +# replace by the Crowdin remote template later + +mkdir -p input/en_US +cp ../app.json ./input/en_US +cp ../ios-infoPlist.json ./input/en_US + +# curl -o .zip -L ${Crowin_Latest_Build} +# unzip -o -q .zip -d input +# rm -rf .zip + +swift run diff --git a/Localization/app.json b/Localization/app.json new file mode 100644 index 00000000..0c3f16c7 --- /dev/null +++ b/Localization/app.json @@ -0,0 +1,78 @@ +{ + "common": { + "alerts": {}, + "controls": { + "actions": { + "add": "Add", + "remove": "Remove", + "edit": "Edit", + "save": "Save", + "ok": "OK", + "confirm": "Confirm", + "continue": "Continue", + "cancel": "Cancel", + "take_photo": "Take photo", + "save_photo": "Save photo", + "sign_in": "Sign in", + "sign_up": "Sign up", + "see_more": "See More", + "preview": "Preview", + "open_in_safari": "Open in Safari" + }, + "timeline": { + "load_more": "Load More" + } + }, + "countable": { + "photo": { + "single": "photo", + "multiple": "photos" + } + } + }, + "scene": { + "welcome": { + "slogan": "Social networking\nback in your hands." + }, + "server_picker": { + "title": "Pick a Server,\nany server.", + "input": { + "placeholder": "Find a server or join your own..." + } + }, + "register": { + "title": "Tell us about you.", + "input": { + "username": { + "placeholder": "username", + "duplicate_prompt": "This username is taken." + }, + "display_name": { + "placeholder": "display name" + }, + "email": { + "placeholder": "email" + }, + "password": { + "placeholder": "password", + "prompt": "Your password needs at least:", + "prompt_eight_characters": "Eight characters" + } + } + }, + "server_rules": { + "title": "Some ground rules.", + "subtitle": "These rules are set by the admins of %s.", + "prompt": "By continuing, you're subject to the terms of service and privacy policy for %s.", + "button": { + "confirm": "I Agree" + } + }, + "home_timeline": { + "title": "Home" + }, + "public_timeline": { + "title": "Public" + } + } +} \ No newline at end of file diff --git a/Localization/ios-infoPlist.json b/Localization/ios-infoPlist.json new file mode 100644 index 00000000..0a260c27 --- /dev/null +++ b/Localization/ios-infoPlist.json @@ -0,0 +1,4 @@ +{ + "NSCameraUsageDescription": "Used to take photo for toot", + "NSPhotoLibraryAddUsageDescription": "Used to save photo into the Photo Library" +} diff --git a/Mastodon/Info.plist b/Mastodon/Info.plist index 47f90b99..7450b654 100644 --- a/Mastodon/Info.plist +++ b/Mastodon/Info.plist @@ -2,8 +2,6 @@ - UIUserInterfaceStyle - Dark CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable From 9a79b9a5458d9fb9e65c4e48cc0c45a6c189ad70 Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 22 Feb 2021 16:20:44 +0800 Subject: [PATCH 09/12] feat: add MastodonServerRules scene --- Mastodon.xcodeproj/project.pbxproj | 44 ++++- Mastodon/Coordinator/SceneCoordinator.swift | 5 + Mastodon/Generated/Assets.swift | 2 +- Mastodon/Generated/Strings.swift | 107 +++++++++++ .../Contents.json | 0 Mastodon/Resources/en.lproj/InfoPlist.strings | 2 + .../Resources/en.lproj/Localizable.strings | 43 ++++- .../AuthenticationViewController.swift | 48 +++-- .../MastodonRegisterViewController.swift | 110 +++++++---- .../Register/MastodonRegisterViewModel.swift | 3 + .../MastodonServerRulesViewController.swift | 174 ++++++++++++++++++ .../MastodonServerRulesViewModel.swift | 41 +++++ .../HomeTimelineViewController.swift | 2 +- Mastodon/Vender/UIViewPreview.swift | 49 +++++ 14 files changed, 561 insertions(+), 69 deletions(-) rename Mastodon/Resources/Assets.xcassets/Colors/Background/{signUp.system.background.colorset => onboarding.background.colorset}/Contents.json (100%) create mode 100644 Mastodon/Resources/en.lproj/InfoPlist.strings create mode 100644 Mastodon/Scene/Authentication/ServerRules/MastodonServerRulesViewController.swift create mode 100644 Mastodon/Scene/Authentication/ServerRules/MastodonServerRulesViewModel.swift create mode 100644 Mastodon/Vender/UIViewPreview.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 3eac67d4..c7180d3b 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -74,6 +74,8 @@ DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140CE25C42AEE00F9F3CF /* OSLog.swift */; }; DB084B5725CBC56C00F898ED /* Toot.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB084B5625CBC56C00F898ED /* Toot.swift */; }; DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */; }; + DB2B3ABC25E37E15007045F9 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = DB2B3ABE25E37E15007045F9 /* InfoPlist.strings */; }; + DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2B3AE825E38850007045F9 /* UIViewPreview.swift */; }; DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB3D0FF225BAA61700EAA174 /* AlamofireImage */; }; DB3D100D25BAA75E00EAA174 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = DB3D100F25BAA75E00EAA174 /* Localizable.strings */; }; DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD525BAA00100D1B89D /* AppDelegate.swift */; }; @@ -96,6 +98,8 @@ DB5086AB25CC0BBB00C2C187 /* AvatarConfigurableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */; }; DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = DB5086B725CC0D6400C2C187 /* Kingfisher */; }; DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */; }; + DB72601C25E36A2100235243 /* MastodonServerRulesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */; }; + DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */; }; DB89B9F725C10FD0008580ED /* CoreDataStack.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */; }; DB89B9FE25C10FD0008580ED /* CoreDataStackTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89B9FD25C10FD0008580ED /* CoreDataStackTests.swift */; }; DB89BA0025C10FD0008580ED /* CoreDataStack.h in Headers */ = {isa = PBXBuildFile; fileRef = DB89B9F025C10FD0008580ED /* CoreDataStack.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -252,6 +256,8 @@ DB0140CE25C42AEE00F9F3CF /* OSLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSLog.swift; sourceTree = ""; }; DB084B5625CBC56C00F898ED /* Toot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toot.swift; sourceTree = ""; }; DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Instance.swift"; sourceTree = ""; }; + DB2B3ABD25E37E15007045F9 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + DB2B3AE825E38850007045F9 /* UIViewPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewPreview.swift; sourceTree = ""; }; DB3D0FED25BAA42200EAA174 /* MastodonSDK */ = {isa = PBXFileReference; lastKnownFileType = folder; path = MastodonSDK; sourceTree = ""; }; DB3D100E25BAA75E00EAA174 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; DB427DD225BAA00100D1B89D /* Mastodon.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Mastodon.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -279,6 +285,8 @@ DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarBarButtonItem.swift; sourceTree = ""; }; DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarConfigurableView.swift; sourceTree = ""; }; DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SplashPreference.swift; sourceTree = ""; }; + DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewController.swift; sourceTree = ""; }; + DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewModel.swift; sourceTree = ""; }; DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CoreDataStack.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DB89B9F025C10FD0008580ED /* CoreDataStack.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CoreDataStack.h; sourceTree = ""; }; DB89B9F125C10FD0008580ED /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -437,6 +445,7 @@ isa = PBXGroup; children = ( 2D5A3D0225CF8742002347D6 /* ControlContainableScrollViews.swift */, + DB2B3AE825E38850007045F9 /* UIViewPreview.swift */, ); path = Vender; sourceTree = ""; @@ -564,6 +573,7 @@ children = ( DB0140A625C40C0900F9F3CF /* PinBased */, DBE0821A25CD382900FD6BBD /* Register */, + DB72602125E36A2500235243 /* ServerRules */, DB01409525C40B6700F9F3CF /* AuthenticationViewController.swift */, DB98334625C8056600AD9700 /* AuthenticationViewModel.swift */, ); @@ -605,6 +615,7 @@ children = ( DB427DDE25BAA00100D1B89D /* Assets.xcassets */, DB3D100F25BAA75E00EAA174 /* Localizable.strings */, + DB2B3ABE25E37E15007045F9 /* InfoPlist.strings */, ); path = Resources; sourceTree = ""; @@ -644,13 +655,13 @@ children = ( DB427DE325BAA00100D1B89D /* Info.plist */, DB89BA1025C10FF5008580ED /* Mastodon.entitlements */, - 2D5A3D0125CF8640002347D6 /* Vender */, 2D76319C25C151DE00929FB9 /* Diffiable */, DB8AF52A25C13561002E6C99 /* State */, 2D61335525C1886800CAE157 /* Service */, DB8AF55525C1379F002E6C99 /* Scene */, DB8AF54125C13647002E6C99 /* Coordinator */, DB8AF56225C138BC002E6C99 /* Extension */, + 2D5A3D0125CF8640002347D6 /* Vender */, DB5086CB25CC0DB400C2C187 /* Preference */, 2D69CFF225CA9E2200C3A1B2 /* Protocol */, DB98338425C945ED00AD9700 /* Generated */, @@ -715,6 +726,15 @@ path = Preference; sourceTree = ""; }; + DB72602125E36A2500235243 /* ServerRules */ = { + isa = PBXGroup; + children = ( + DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */, + DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */, + ); + path = ServerRules; + sourceTree = ""; + }; DB89B9EF25C10FD0008580ED /* CoreDataStack */ = { isa = PBXGroup; children = ( @@ -805,12 +825,11 @@ DB8AF55525C1379F002E6C99 /* Scene */ = { isa = PBXGroup; children = ( - 2D38F1D325CD463600561493 /* HomeTimeline */, 2D7631A425C1532200929FB9 /* Share */, DB8AF54E25C13703002E6C99 /* MainTab */, DB01409B25C40BB600F9F3CF /* Authentication */, + 2D38F1D325CD463600561493 /* HomeTimeline */, 2D76316325C14BAC00929FB9 /* PublicTimeline */, - DBD4ED0B25CC0FD40041B741 /* HomeTimeline */, ); path = Scene; sourceTree = ""; @@ -852,13 +871,6 @@ path = Generated; sourceTree = ""; }; - DBD4ED0B25CC0FD40041B741 /* HomeTimeline */ = { - isa = PBXGroup; - children = ( - ); - path = HomeTimeline; - sourceTree = ""; - }; DBE0821A25CD382900FD6BBD /* Register */ = { isa = PBXGroup; children = ( @@ -1057,6 +1069,7 @@ DB3D100D25BAA75E00EAA174 /* Localizable.strings in Resources */, DB427DDF25BAA00100D1B89D /* Assets.xcassets in Resources */, DB427DDD25BAA00100D1B89D /* Main.storyboard in Resources */, + DB2B3ABC25E37E15007045F9 /* InfoPlist.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1222,10 +1235,12 @@ 2D38F1F725CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift in Sources */, 2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */, DB8AF53025C13561002E6C99 /* AppContext.swift in Sources */, + DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */, 2D38F1F125CD477D00561493 /* HomeTimelineViewModel+LoadMiddleState.swift in Sources */, DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */, 2D152A8C25C295CC009AA50C /* TimelinePostView.swift in Sources */, 2D42FF8525C8224F004A627A /* HitTestExpandedButton.swift in Sources */, + DB72601C25E36A2100235243 /* MastodonServerRulesViewController.swift in Sources */, 2D42FF8F25C8228A004A627A /* UIButton.swift in Sources */, 2D61335825C188A000CAE157 /* APIService+Persist+Timeline.swift in Sources */, DB45FAE325CA7181005A8AC7 /* MastodonUser.swift in Sources */, @@ -1266,6 +1281,7 @@ 2D76319F25C1521200929FB9 /* TimelineSection.swift in Sources */, DB084B5725CBC56C00F898ED /* Toot.swift in Sources */, DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */, + DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */, DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */, DB98338725C945ED00AD9700 /* Strings.swift in Sources */, DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */, @@ -1380,6 +1396,14 @@ /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ + DB2B3ABE25E37E15007045F9 /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + DB2B3ABD25E37E15007045F9 /* en */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; DB3D100F25BAA75E00EAA174 /* Localizable.strings */ = { isa = PBXVariantGroup; children = ( diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index 6bdf135a..0b412396 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -40,6 +40,7 @@ extension SceneCoordinator { case authentication(viewModel: AuthenticationViewModel) case mastodonPinBasedAuthentication(viewModel: MastodonPinBasedAuthenticationViewModel) case mastodonRegister(viewModel: MastodonRegisterViewModel) + case mastodonServerRules(viewModel: MastodonServerRulesViewModel) case alertController(alertController: UIAlertController) } @@ -125,6 +126,10 @@ private extension SceneCoordinator { let _viewController = MastodonRegisterViewController() _viewController.viewModel = viewModel viewController = _viewController + case .mastodonServerRules(let viewModel): + let _viewController = MastodonServerRulesViewController() + _viewController.viewModel = viewModel + viewController = _viewController case .alertController(let alertController): if let popoverPresentationController = alertController.popoverPresentationController { assert( diff --git a/Mastodon/Generated/Assets.swift b/Mastodon/Generated/Assets.swift index 84de0ae9..4510a686 100644 --- a/Mastodon/Generated/Assets.swift +++ b/Mastodon/Generated/Assets.swift @@ -27,8 +27,8 @@ internal enum Asset { } internal enum Colors { internal enum Background { + internal static let onboardingBackground = ColorAsset(name: "Colors/Background/onboarding.background") internal static let secondarySystemBackground = ColorAsset(name: "Colors/Background/secondary.system.background") - internal static let signUpSystemBackground = ColorAsset(name: "Colors/Background/signUp.system.background") internal static let systemBackground = ColorAsset(name: "Colors/Background/system.background") internal static let tertiarySystemBackground = ColorAsset(name: "Colors/Background/tertiary.system.background") } diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index 3399d735..f8383a82 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -13,11 +13,118 @@ internal enum L10n { internal enum Common { internal enum Controls { + internal enum Actions { + /// Add + internal static let add = L10n.tr("Localizable", "Common.Controls.Actions.Add") + /// Cancel + internal static let cancel = L10n.tr("Localizable", "Common.Controls.Actions.Cancel") + /// Confirm + internal static let confirm = L10n.tr("Localizable", "Common.Controls.Actions.Confirm") + /// Continue + internal static let `continue` = L10n.tr("Localizable", "Common.Controls.Actions.Continue") + /// Edit + internal static let edit = L10n.tr("Localizable", "Common.Controls.Actions.Edit") + /// OK + internal static let ok = L10n.tr("Localizable", "Common.Controls.Actions.Ok") + /// Open in Safari + internal static let openInSafari = L10n.tr("Localizable", "Common.Controls.Actions.OpenInSafari") + /// Preview + internal static let preview = L10n.tr("Localizable", "Common.Controls.Actions.Preview") + /// Remove + internal static let remove = L10n.tr("Localizable", "Common.Controls.Actions.Remove") + /// Save + internal static let save = L10n.tr("Localizable", "Common.Controls.Actions.Save") + /// Save photo + internal static let savePhoto = L10n.tr("Localizable", "Common.Controls.Actions.SavePhoto") + /// See More + internal static let seeMore = L10n.tr("Localizable", "Common.Controls.Actions.SeeMore") + /// Sign in + internal static let signIn = L10n.tr("Localizable", "Common.Controls.Actions.SignIn") + /// Sign up + internal static let signUp = L10n.tr("Localizable", "Common.Controls.Actions.SignUp") + /// Take photo + internal static let takePhoto = L10n.tr("Localizable", "Common.Controls.Actions.TakePhoto") + } internal enum Timeline { /// Load More internal static let loadMore = L10n.tr("Localizable", "Common.Controls.Timeline.LoadMore") } } + internal enum Countable { + internal enum Photo { + /// photos + internal static let multiple = L10n.tr("Localizable", "Common.Countable.Photo.Multiple") + /// photo + internal static let single = L10n.tr("Localizable", "Common.Countable.Photo.Single") + } + } + } + + internal enum Scene { + internal enum HomeTimeline { + /// Home + internal static let title = L10n.tr("Localizable", "Scene.HomeTimeline.Title") + } + internal enum PublicTimeline { + /// Public + internal static let title = L10n.tr("Localizable", "Scene.PublicTimeline.Title") + } + internal enum Register { + /// Tell us about you. + internal static let title = L10n.tr("Localizable", "Scene.Register.Title") + internal enum Input { + internal enum DisplayName { + /// display name + internal static let placeholder = L10n.tr("Localizable", "Scene.Register.Input.DisplayName.Placeholder") + } + internal enum Email { + /// email + internal static let placeholder = L10n.tr("Localizable", "Scene.Register.Input.Email.Placeholder") + } + internal enum Password { + /// password + internal static let placeholder = L10n.tr("Localizable", "Scene.Register.Input.Password.Placeholder") + /// Your password needs at least: + internal static let prompt = L10n.tr("Localizable", "Scene.Register.Input.Password.Prompt") + /// Eight characters + internal static let promptEightCharacters = L10n.tr("Localizable", "Scene.Register.Input.Password.PromptEightCharacters") + } + internal enum Username { + /// This username is taken. + internal static let duplicatePrompt = L10n.tr("Localizable", "Scene.Register.Input.Username.DuplicatePrompt") + /// username + internal static let placeholder = L10n.tr("Localizable", "Scene.Register.Input.Username.Placeholder") + } + } + } + internal enum ServerPicker { + /// Pick a Server,\nany server. + internal static let title = L10n.tr("Localizable", "Scene.ServerPicker.Title") + internal enum Input { + /// Find a server or join your own... + internal static let placeholder = L10n.tr("Localizable", "Scene.ServerPicker.Input.Placeholder") + } + } + internal enum ServerRules { + /// By continuing, you're subject to the terms of service and privacy policy for %@. + internal static func prompt(_ p1: Any) -> String { + return L10n.tr("Localizable", "Scene.ServerRules.Prompt", String(describing: p1)) + } + /// These rules are set by the admins of %@. + internal static func subtitle(_ p1: Any) -> String { + return L10n.tr("Localizable", "Scene.ServerRules.Subtitle", String(describing: p1)) + } + /// Some ground rules. + internal static let title = L10n.tr("Localizable", "Scene.ServerRules.Title") + internal enum Button { + /// I Agree + internal static let confirm = L10n.tr("Localizable", "Scene.ServerRules.Button.Confirm") + } + } + internal enum Welcome { + /// Social networking\nback in your hands. + internal static let slogan = L10n.tr("Localizable", "Scene.Welcome.Slogan") + } } } // swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Background/signUp.system.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Background/onboarding.background.colorset/Contents.json similarity index 100% rename from Mastodon/Resources/Assets.xcassets/Colors/Background/signUp.system.background.colorset/Contents.json rename to Mastodon/Resources/Assets.xcassets/Colors/Background/onboarding.background.colorset/Contents.json diff --git a/Mastodon/Resources/en.lproj/InfoPlist.strings b/Mastodon/Resources/en.lproj/InfoPlist.strings new file mode 100644 index 00000000..972e1a7a --- /dev/null +++ b/Mastodon/Resources/en.lproj/InfoPlist.strings @@ -0,0 +1,2 @@ +"NSCameraUsageDescription" = "Used to take photo for toot"; +"NSPhotoLibraryAddUsageDescription" = "Used to save photo into the Photo Library"; \ No newline at end of file diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index caa87e95..707ef3cc 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -1,8 +1,37 @@ -/* - Localizable.strings - Mastodon - - Created by MainasuK Cirno on 2021/1/22. - -*/ +"Common.Controls.Actions.Add" = "Add"; +"Common.Controls.Actions.Cancel" = "Cancel"; +"Common.Controls.Actions.Confirm" = "Confirm"; +"Common.Controls.Actions.Continue" = "Continue"; +"Common.Controls.Actions.Edit" = "Edit"; +"Common.Controls.Actions.Ok" = "OK"; +"Common.Controls.Actions.OpenInSafari" = "Open in Safari"; +"Common.Controls.Actions.Preview" = "Preview"; +"Common.Controls.Actions.Remove" = "Remove"; +"Common.Controls.Actions.Save" = "Save"; +"Common.Controls.Actions.SavePhoto" = "Save photo"; +"Common.Controls.Actions.SeeMore" = "See More"; +"Common.Controls.Actions.SignIn" = "Sign in"; +"Common.Controls.Actions.SignUp" = "Sign up"; +"Common.Controls.Actions.TakePhoto" = "Take photo"; "Common.Controls.Timeline.LoadMore" = "Load More"; +"Common.Countable.Photo.Multiple" = "photos"; +"Common.Countable.Photo.Single" = "photo"; +"Scene.HomeTimeline.Title" = "Home"; +"Scene.PublicTimeline.Title" = "Public"; +"Scene.Register.Input.DisplayName.Placeholder" = "display name"; +"Scene.Register.Input.Email.Placeholder" = "email"; +"Scene.Register.Input.Password.Placeholder" = "password"; +"Scene.Register.Input.Password.Prompt" = "Your password needs at least:"; +"Scene.Register.Input.Password.PromptEightCharacters" = "Eight characters"; +"Scene.Register.Input.Username.DuplicatePrompt" = "This username is taken."; +"Scene.Register.Input.Username.Placeholder" = "username"; +"Scene.Register.Title" = "Tell us about you."; +"Scene.ServerPicker.Input.Placeholder" = "Find a server or join your own..."; +"Scene.ServerPicker.Title" = "Pick a Server, +any server."; +"Scene.ServerRules.Button.Confirm" = "I Agree"; +"Scene.ServerRules.Prompt" = "By continuing, you're subject to the terms of service and privacy policy for %@."; +"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."; \ No newline at end of file diff --git a/Mastodon/Scene/Authentication/AuthenticationViewController.swift b/Mastodon/Scene/Authentication/AuthenticationViewController.swift index b02cf3c4..5ab500bc 100644 --- a/Mastodon/Scene/Authentication/AuthenticationViewController.swift +++ b/Mastodon/Scene/Authentication/AuthenticationViewController.swift @@ -77,6 +77,7 @@ extension AuthenticationViewController { override func viewDidLoad() { super.viewDidLoad() + overrideUserInterfaceStyle = .dark // FIXME: title = "Authentication" view.backgroundColor = Asset.Colors.Background.systemBackground.color @@ -265,6 +266,22 @@ extension AuthenticationViewController { .store(in: &disposeBag) } + private struct SignUpResponseFirst { + let instance: Mastodon.Response.Content + let application: Mastodon.Response.Content + } + + private struct SignUpResponseSecond { + let instance: Mastodon.Response.Content + let authenticateInfo: AuthenticationViewModel.AuthenticateInfo + } + + private struct SignUpResponseThird { + let instance: Mastodon.Response.Content + let authenticateInfo: AuthenticationViewModel.AuthenticateInfo + let applicationToken: Mastodon.Response.Content + } + @objc private func signUpButtonPressed(_ sender: UIButton) { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) guard viewModel.isDomainValid.value, let domain = viewModel.domain.value else { @@ -275,26 +292,34 @@ extension AuthenticationViewController { viewModel.isRegistering.value = true context.apiService.instance(domain: domain) - .compactMap { [weak self] response -> AnyPublisher, Error>? in + .compactMap { [weak self] response -> AnyPublisher? in guard let self = self else { return nil } guard response.value.registrations != false else { return Fail(error: AuthenticationViewModel.AuthenticationError.registrationClosed).eraseToAnyPublisher() } return self.context.apiService.createApplication(domain: domain) + .map { SignUpResponseFirst(instance: response, application: $0) } + .eraseToAnyPublisher() } .switchToLatest() - .tryMap { response -> AuthenticationViewModel.AuthenticateInfo in - let application = response.value + .tryMap { response -> SignUpResponseSecond in + let application = response.application.value guard let authenticateInfo = AuthenticationViewModel.AuthenticateInfo(domain: domain, application: application) else { throw APIService.APIError.explicit(.badResponse) } - return authenticateInfo + return SignUpResponseSecond(instance: response.instance, authenticateInfo: authenticateInfo) } - .compactMap { [weak self] authenticateInfo -> AnyPublisher<(Mastodon.Response.Content, AuthenticationViewModel.AuthenticateInfo), Error>? in + .compactMap { [weak self] response -> AnyPublisher? in guard let self = self else { return nil } - return self.context.apiService.applicationAccessToken(domain: domain, clientID: authenticateInfo.clientID, clientSecret: authenticateInfo.clientSecret) - .map { ($0, authenticateInfo) } - .eraseToAnyPublisher() + let instance = response.instance + let authenticateInfo = response.authenticateInfo + return self.context.apiService.applicationAccessToken( + domain: domain, + clientID: authenticateInfo.clientID, + clientSecret: authenticateInfo.clientSecret + ) + .map { SignUpResponseThird(instance: instance, authenticateInfo: authenticateInfo, applicationToken: $0) } + .eraseToAnyPublisher() } .switchToLatest() .receive(on: DispatchQueue.main) @@ -308,12 +333,13 @@ extension AuthenticationViewController { case .finished: break } - } receiveValue: { [weak self] response, authenticateInfo in + } receiveValue: { [weak self] response in guard let self = self else { return } let mastodonRegisterViewModel = MastodonRegisterViewModel( domain: domain, - authenticateInfo: authenticateInfo, - applicationToken: response.value + authenticateInfo: response.authenticateInfo, + instance: response.instance.value, + applicationToken: response.applicationToken.value ) self.coordinator.present(scene: .mastodonRegister(viewModel: mastodonRegisterViewModel), from: self, transition: .show) } diff --git a/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift b/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift index 5361b311..de5b5c68 100644 --- a/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift +++ b/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift @@ -20,13 +20,19 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency { var viewModel: MastodonRegisterViewModel! let tapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer - let stackViewTopDistance: CGFloat = 16 - var scrollview: UIScrollView = { + let statusBarBackground: UIView = { + let view = UIView() + view.backgroundColor = Asset.Colors.Background.onboardingBackground.color + return view + }() + + let scrollView: UIScrollView = { let scrollview = UIScrollView() scrollview.showsVerticalScrollIndicator = false scrollview.translatesAutoresizingMaskIntoConstraints = false scrollview.keyboardDismissMode = .interactive + scrollview.clipsToBounds = false // make content could display over bleeding return scrollview }() @@ -34,7 +40,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency { let label = UILabel() label.font = UIFontMetrics(forTextStyle: .largeTitle).scaledFont(for: UIFont.boldSystemFont(ofSize: 34)) label.textColor = Asset.Colors.Label.black.color - label.text = "Tell us about you." + label.text = L10n.Scene.Register.title return label }() @@ -92,7 +98,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency { textField.autocorrectionType = .no textField.backgroundColor = .white textField.textColor = .black - textField.attributedPlaceholder = NSAttributedString(string: "username", + textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.Username.placeholder, attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.lightSecondaryText.color, NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .headline)]) textField.borderStyle = UITextField.BorderStyle.roundedRect @@ -113,7 +119,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency { textField.autocorrectionType = .no textField.backgroundColor = .white textField.textColor = .black - textField.attributedPlaceholder = NSAttributedString(string: "display name", + textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.DisplayName.placeholder, attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.lightSecondaryText.color, NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .headline)]) textField.borderStyle = UITextField.BorderStyle.roundedRect @@ -130,7 +136,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency { textField.keyboardType = .emailAddress textField.backgroundColor = .white textField.textColor = .black - textField.attributedPlaceholder = NSAttributedString(string: "email", + textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.Email.placeholder, attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.lightSecondaryText.color, NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .headline)]) textField.borderStyle = UITextField.BorderStyle.roundedRect @@ -154,7 +160,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency { textField.isSecureTextEntry = true textField.backgroundColor = .white textField.textColor = .black - textField.attributedPlaceholder = NSAttributedString(string: "password", + textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.Password.placeholder, attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.lightSecondaryText.color, NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .headline)]) textField.borderStyle = UITextField.BorderStyle.roundedRect @@ -171,7 +177,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency { button.setBackgroundImage(UIImage.placeholder(color: Asset.Colors.lightDisabled.color), for: .disabled) button.isEnabled = false button.setTitleColor(Asset.Colors.Label.primary.color, for: .normal) - button.setTitle("Continue", for: .normal) + button.setTitle(L10n.Common.Controls.Actions.continue, for: .normal) button.layer.masksToBounds = true button.layer.cornerRadius = 8 button.layer.cornerCurve = .continuous @@ -186,11 +192,12 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency { } extension MastodonRegisterViewController { + override func viewDidLoad() { super.viewDidLoad() - navigationController?.navigationBar.isHidden = true - view.backgroundColor = Asset.Colors.Background.signUpSystemBackground.color + overrideUserInterfaceStyle = .light + view.backgroundColor = Asset.Colors.Background.onboardingBackground.color domainLabel.text = "@" + viewModel.domain + " " domainLabel.sizeToFit() passwordCheckLabel.attributedText = viewModel.attributeStringForPassword() @@ -210,7 +217,7 @@ extension MastodonRegisterViewController { stackView.axis = .vertical stackView.distribution = .fill stackView.spacing = 40 - stackView.layoutMargins = UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 4) + stackView.layoutMargins = UIEdgeInsets(top: 20, left: 0, bottom: 0, right: 0) stackView.isLayoutMarginsRelativeArrangement = true stackView.addArrangedSubview(largeTitleLabel) stackView.addArrangedSubview(photoView) @@ -220,25 +227,34 @@ extension MastodonRegisterViewController { stackView.addArrangedSubview(passwordTextField) stackView.addArrangedSubview(passwordCheckLabel) - // scrollview - view.addSubview(scrollview) + // scrollView + view.addSubview(scrollView) NSLayoutConstraint.activate([ - scrollview.frameLayoutGuide.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor), - scrollview.frameLayoutGuide.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor), - view.readableContentGuide.trailingAnchor.constraint(equalTo: scrollview.frameLayoutGuide.trailingAnchor), - scrollview.frameLayoutGuide.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor), - scrollview.frameLayoutGuide.widthAnchor.constraint(equalTo: scrollview.contentLayoutGuide.widthAnchor), + scrollView.frameLayoutGuide.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor), + scrollView.frameLayoutGuide.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor), + view.readableContentGuide.trailingAnchor.constraint(equalTo: scrollView.frameLayoutGuide.trailingAnchor), + scrollView.frameLayoutGuide.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor), + scrollView.frameLayoutGuide.widthAnchor.constraint(equalTo: scrollView.contentLayoutGuide.widthAnchor), ]) // stackview - scrollview.addSubview(stackView) + scrollView.addSubview(stackView) stackView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - stackView.topAnchor.constraint(equalTo: scrollview.contentLayoutGuide.topAnchor, constant: stackViewTopDistance), - stackView.leadingAnchor.constraint(equalTo: scrollview.contentLayoutGuide.leadingAnchor), - stackView.trailingAnchor.constraint(equalTo: scrollview.contentLayoutGuide.trailingAnchor), - stackView.widthAnchor.constraint(equalTo: scrollview.frameLayoutGuide.widthAnchor), - scrollview.contentLayoutGuide.bottomAnchor.constraint(equalTo: stackView.bottomAnchor), + stackView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor), + stackView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor), + stackView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor), + stackView.widthAnchor.constraint(equalTo: scrollView.contentLayoutGuide.widthAnchor), + scrollView.contentLayoutGuide.bottomAnchor.constraint(equalTo: stackView.bottomAnchor), + ]) + + statusBarBackground.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(statusBarBackground) + NSLayoutConstraint.activate([ + statusBarBackground.topAnchor.constraint(equalTo: view.topAnchor), + statusBarBackground.leadingAnchor.constraint(equalTo: view.leadingAnchor), + statusBarBackground.trailingAnchor.constraint(equalTo: view.trailingAnchor), + statusBarBackground.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor), ]) // photoview @@ -283,11 +299,11 @@ extension MastodonRegisterViewController { signUpButton.translatesAutoresizingMaskIntoConstraints = false stackView.addArrangedSubview(signUpButton) NSLayoutConstraint.activate([ - signUpButton.heightAnchor.constraint(equalToConstant: 44).priority(.defaultHigh), + signUpButton.heightAnchor.constraint(equalToConstant: 46).priority(.defaultHigh), ]) signUpActivityIndicatorView.translatesAutoresizingMaskIntoConstraints = false - scrollview.addSubview(signUpActivityIndicatorView) + scrollView.addSubview(signUpActivityIndicatorView) NSLayoutConstraint.activate([ signUpActivityIndicatorView.centerXAnchor.constraint(equalTo: signUpButton.centerXAnchor), signUpActivityIndicatorView.centerYAnchor.constraint(equalTo: signUpButton.centerYAnchor), @@ -302,22 +318,22 @@ extension MastodonRegisterViewController { guard let self = self else { return } guard isShow, state == .dock else { - self.scrollview.contentInset.bottom = 0.0 - self.scrollview.verticalScrollIndicatorInsets.bottom = 0.0 + self.scrollView.contentInset.bottom = 0.0 + self.scrollView.verticalScrollIndicatorInsets.bottom = 0.0 return } // isShow AND dock state - let contentFrame = self.view.convert(self.scrollview.frame, to: nil) + let contentFrame = self.view.convert(self.scrollView.frame, to: nil) let padding = contentFrame.maxY - endFrame.minY guard padding > 0 else { - self.scrollview.contentInset.bottom = 0.0 - self.scrollview.verticalScrollIndicatorInsets.bottom = 0.0 + self.scrollView.contentInset.bottom = 0.0 + self.scrollView.verticalScrollIndicatorInsets.bottom = 0.0 return } - self.scrollview.contentInset.bottom = padding + 16 - self.scrollview.verticalScrollIndicatorInsets.bottom = padding + 16 + self.scrollView.contentInset.bottom = padding + 16 + self.scrollView.verticalScrollIndicatorInsets.bottom = padding + 16 }) .store(in: &disposeBag) @@ -326,7 +342,7 @@ extension MastodonRegisterViewController { .sink { [weak self] isRegistering in guard let self = self else { return } isRegistering ? self.signUpActivityIndicatorView.startAnimating() : self.signUpActivityIndicatorView.stopAnimating() - self.signUpButton.setTitle(isRegistering ? "" : "Continue", for: .normal) + self.signUpButton.setTitle(isRegistering ? "" : L10n.Common.Controls.Actions.continue, for: .normal) self.signUpButton.isEnabled = !isRegistering } .store(in: &disposeBag) @@ -379,7 +395,7 @@ extension MastodonRegisterViewController { .sink { [weak self] error in guard let self = self else { return } let alertController = UIAlertController(error, preferredStyle: .alert) - let okAction = UIAlertAction(title: "OK", style: .default, handler: nil) + let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil) alertController.addAction(okAction) self.coordinator.present( scene: .alertController(alertController: alertController), @@ -402,6 +418,12 @@ extension MastodonRegisterViewController { signUpButton.addTarget(self, action: #selector(MastodonRegisterViewController.signUpButtonPressed(_:)), for: .touchUpInside) } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + navigationController?.setNavigationBarHidden(true, animated: false) + } } extension MastodonRegisterViewController: UITextFieldDelegate { @@ -411,12 +433,12 @@ extension MastodonRegisterViewController: UITextFieldDelegate { KeyboardResponderService.shared.isShow.value, KeyboardResponderService.shared.state.value == .dock { let endFrame = KeyboardResponderService.shared.endFrame.value - let contentFrame = self.scrollview.convert(self.passwordCheckLabel.frame, to: nil) + let contentFrame = self.scrollView.convert(self.passwordCheckLabel.frame, to: nil) let padding = contentFrame.maxY - endFrame.minY if padding > 0 { - let contentOffsetY = scrollview.contentOffset.y + let contentOffsetY = scrollView.contentOffset.y DispatchQueue.main.async { - self.scrollview.setContentOffset(CGPoint(x: 0, y: contentOffsetY + padding + 16.0), animated: true) + self.scrollView.setContentOffset(CGPoint(x: 0, y: contentOffsetY + padding + 16.0), animated: true) } } } @@ -484,6 +506,16 @@ extension MastodonRegisterViewController { guard !viewModel.isRegistering.value else { return } viewModel.isRegistering.value = true + if let rules = viewModel.instance.rules, !rules.isEmpty { + let mastodonServerRulesViewModel = MastodonServerRulesViewModel( + context: context, + domain: viewModel.domain, + rules: rules + ) + coordinator.present(scene: .mastodonServerRules(viewModel: mastodonServerRulesViewModel), from: self, transition: .show) + return + } + let query = Mastodon.API.Account.RegisterQuery( reason: nil, username: username, @@ -513,7 +545,7 @@ extension MastodonRegisterViewController { _ = response.value // TODO: let alertController = UIAlertController(title: "Success", message: "Regsiter request sent. Please check your email.\n(Auto sign in not implement yet.)", preferredStyle: .alert) - let okAction = UIAlertAction(title: "OK", style: .default) { [weak self] _ in + let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default) { [weak self] _ in guard let self = self else { return } self.navigationController?.popViewController(animated: true) } diff --git a/Mastodon/Scene/Authentication/Register/MastodonRegisterViewModel.swift b/Mastodon/Scene/Authentication/Register/MastodonRegisterViewModel.swift index 944cb9b0..daea2f53 100644 --- a/Mastodon/Scene/Authentication/Register/MastodonRegisterViewModel.swift +++ b/Mastodon/Scene/Authentication/Register/MastodonRegisterViewModel.swift @@ -16,6 +16,7 @@ final class MastodonRegisterViewModel { // input let domain: String let authenticateInfo: AuthenticationViewModel.AuthenticateInfo + let instance: Mastodon.Entity.Instance let applicationToken: Mastodon.Entity.Token let isRegistering = CurrentValueSubject(false) @@ -37,10 +38,12 @@ final class MastodonRegisterViewModel { init( domain: String, authenticateInfo: AuthenticationViewModel.AuthenticateInfo, + instance: Mastodon.Entity.Instance, applicationToken: Mastodon.Entity.Token ) { self.domain = domain self.authenticateInfo = authenticateInfo + self.instance = instance self.applicationToken = applicationToken self.applicationAuthorization = Mastodon.API.OAuth.Authorization(accessToken: applicationToken.accessToken) diff --git a/Mastodon/Scene/Authentication/ServerRules/MastodonServerRulesViewController.swift b/Mastodon/Scene/Authentication/ServerRules/MastodonServerRulesViewController.swift new file mode 100644 index 00000000..affd69d4 --- /dev/null +++ b/Mastodon/Scene/Authentication/ServerRules/MastodonServerRulesViewController.swift @@ -0,0 +1,174 @@ +// +// MastodonServerRulesViewController.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-2-22. +// + +import os.log +import UIKit + +final class MastodonServerRulesViewController: UIViewController, NeedsDependency { + + weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } + weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } + + var viewModel: MastodonServerRulesViewModel! + + let largeTitleLabel: UILabel = { + let label = UILabel() + label.font = UIFontMetrics(forTextStyle: .largeTitle).scaledFont(for: UIFont.boldSystemFont(ofSize: 34)) + label.textColor = .label + label.text = L10n.Scene.ServerRules.title + return label + }() + + private(set) lazy var subtitleLabel: UILabel = { + let label = UILabel() + label.font = UIFontMetrics(forTextStyle: .title1).scaledFont(for: UIFont.systemFont(ofSize: 20)) + label.textColor = .secondaryLabel + label.text = L10n.Scene.ServerRules.subtitle(viewModel.domain) + label.numberOfLines = 0 + return label + }() + + let rulesLabel: UILabel = { + let label = UILabel() + label.font = .preferredFont(forTextStyle: .body) + label.textColor = Asset.Colors.Label.black.color + label.text = "Rules" + label.numberOfLines = 0 + return label + }() + + let bottonContainerView: UIView = { + let view = UIView() + view.backgroundColor = Asset.Colors.Background.onboardingBackground.color + return view + }() + + private(set) lazy var bottomPromptLabel: UILabel = { + let label = UILabel() + label.font = .preferredFont(forTextStyle: .body) + label.textColor = .label + label.text = L10n.Scene.ServerRules.prompt(viewModel.domain) + label.numberOfLines = 0 + return label + }() + + let confirmButton: UIButton = { + let button = UIButton(type: .system) + button.titleLabel?.font = .preferredFont(forTextStyle: .headline) + button.setBackgroundImage(UIImage.placeholder(color: Asset.Colors.lightBrandBlue.color), for: .normal) + button.setBackgroundImage(UIImage.placeholder(color: Asset.Colors.lightDisabled.color), for: .disabled) + button.setTitleColor(Asset.Colors.Label.primary.color, for: .normal) + button.setTitle(L10n.Scene.ServerRules.Button.confirm, for: .normal) + button.layer.masksToBounds = true + button.layer.cornerRadius = 8 + button.layer.cornerCurve = .continuous + return button + }() + + let scrollView = UIScrollView() + +} + +extension MastodonServerRulesViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + overrideUserInterfaceStyle = .light + view.backgroundColor = Asset.Colors.Background.onboardingBackground.color + + bottonContainerView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(bottonContainerView) + NSLayoutConstraint.activate([ + view.bottomAnchor.constraint(equalTo: bottonContainerView.bottomAnchor), + bottonContainerView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + bottonContainerView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + ]) + bottonContainerView.preservesSuperviewLayoutMargins = true + + confirmButton.translatesAutoresizingMaskIntoConstraints = false + bottonContainerView.addSubview(confirmButton) + NSLayoutConstraint.activate([ + bottonContainerView.bottomAnchor.constraint(greaterThanOrEqualTo: confirmButton.bottomAnchor, constant: 16), + bottonContainerView.layoutMarginsGuide.bottomAnchor.constraint(equalTo: confirmButton.bottomAnchor).priority(.defaultHigh), + confirmButton.leadingAnchor.constraint(equalTo: bottonContainerView.readableContentGuide.leadingAnchor), + confirmButton.trailingAnchor.constraint(equalTo: bottonContainerView.readableContentGuide.trailingAnchor), + confirmButton.heightAnchor.constraint(equalToConstant: 46).priority(.defaultHigh), + ]) + + bottomPromptLabel.translatesAutoresizingMaskIntoConstraints = false + bottonContainerView.addSubview(bottomPromptLabel) + NSLayoutConstraint.activate([ + bottomPromptLabel.topAnchor.constraint(equalTo: bottonContainerView.topAnchor, constant: 20), + bottomPromptLabel.leadingAnchor.constraint(equalTo: bottonContainerView.readableContentGuide.leadingAnchor), + bottomPromptLabel.trailingAnchor.constraint(equalTo: bottonContainerView.readableContentGuide.trailingAnchor), + confirmButton.topAnchor.constraint(equalTo: bottomPromptLabel.bottomAnchor, constant: 20), + ]) + + scrollView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(scrollView) + NSLayoutConstraint.activate([ + scrollView.frameLayoutGuide.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor), + scrollView.frameLayoutGuide.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor), + view.readableContentGuide.trailingAnchor.constraint(equalTo: scrollView.frameLayoutGuide.trailingAnchor), + scrollView.frameLayoutGuide.widthAnchor.constraint(equalTo: scrollView.contentLayoutGuide.widthAnchor), + bottonContainerView.topAnchor.constraint(equalTo: scrollView.frameLayoutGuide.bottomAnchor), + ]) + + let stackView = UIStackView() + stackView.axis = .vertical + stackView.distribution = .fill + stackView.spacing = 10 + stackView.layoutMargins = UIEdgeInsets(top: 20, left: 0, bottom: 0, right: 0) + stackView.addArrangedSubview(largeTitleLabel) + stackView.addArrangedSubview(subtitleLabel) + stackView.addArrangedSubview(rulesLabel) + + stackView.translatesAutoresizingMaskIntoConstraints = false + scrollView.addSubview(stackView) + NSLayoutConstraint.activate([ + stackView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor), + stackView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor), + stackView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor), + scrollView.contentLayoutGuide.bottomAnchor.constraint(equalTo: stackView.bottomAnchor), + ]) + + rulesLabel.attributedText = viewModel.rulesAttributedString + confirmButton.addTarget(self, action: #selector(MastodonServerRulesViewController.confirmButtonPressed(_:)), for: .touchUpInside) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + navigationController?.setNavigationBarHidden(true, animated: false) + } + +} + +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) + + } +} + +#if canImport(SwiftUI) && DEBUG +import SwiftUI + +struct ServerRulesViewController_Previews: PreviewProvider { + + static var previews: some View { + UIViewControllerPreview { + let viewController = MastodonServerRulesViewController() + return viewController + } + .previewLayout(.fixed(width: 375, height: 800)) + } + +} + +#endif diff --git a/Mastodon/Scene/Authentication/ServerRules/MastodonServerRulesViewModel.swift b/Mastodon/Scene/Authentication/ServerRules/MastodonServerRulesViewModel.swift new file mode 100644 index 00000000..0c43392f --- /dev/null +++ b/Mastodon/Scene/Authentication/ServerRules/MastodonServerRulesViewModel.swift @@ -0,0 +1,41 @@ +// +// MastodonServerRulesViewModel.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-2-22. +// + +import UIKit +import MastodonSDK + +final class MastodonServerRulesViewModel { + + // input + let context: AppContext + let domain: String + let rules: [Mastodon.Entity.Instance.Rule] + + init(context: AppContext, domain: String, rules: [Mastodon.Entity.Instance.Rule]) { + self.context = context + self.domain = domain + self.rules = rules + } + + var rulesAttributedString: NSAttributedString { + let attributedString = NSMutableAttributedString(string: "\n") + for (i, rule) in rules.enumerated() { + let index = String(i + 1) + let indexString = NSAttributedString(string: index + ". ", attributes: [ + NSAttributedString.Key.foregroundColor: UIColor.secondaryLabel + ]) + let ruleString = NSAttributedString(string: rule.text + "\n\n") + attributedString.append(indexString) + attributedString.append(ruleString) + } + // let paragraphStyle = NSMutableParagraphStyle() + // paragraphStyle.lineSpacing = 20 + // attributedString.addAttribute(NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: NSRange(location: 0, length: attributedString.length)) + return attributedString + } + +} diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index 21e3d6fe..cbd3fa9b 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -50,7 +50,7 @@ extension HomeTimelineViewController { override func viewDidLoad() { super.viewDidLoad() - title = "Home" + title = L10n.Scene.HomeTimeline.title view.backgroundColor = Asset.Colors.Background.systemBackground.color navigationItem.leftBarButtonItem = avatarBarButtonItem avatarBarButtonItem.avatarButton.addTarget(self, action: #selector(HomeTimelineViewController.avatarButtonPressed(_:)), for: .touchUpInside) diff --git a/Mastodon/Vender/UIViewPreview.swift b/Mastodon/Vender/UIViewPreview.swift new file mode 100644 index 00000000..6494354e --- /dev/null +++ b/Mastodon/Vender/UIViewPreview.swift @@ -0,0 +1,49 @@ +// https://github.com/bielikb/UIViewPreview/blob/master/Sources/UIViewPreview/UIViewPreview.swift + +#if canImport(SwiftUI) && DEBUG +import SwiftUI + +public struct UIViewPreview: UIViewRepresentable { + public let view: View + public let width: CGFloat? + public init(width: CGFloat? = nil, _ builder: @escaping () -> View) { + self.view = builder() + self.width = width + } + // MARK: - UIViewRepresentable + public func makeUIView(context: Context) -> UIView { + return view + } + public func updateUIView(_ view: UIView, context: Context) { + view.translatesAutoresizingMaskIntoConstraints = false + view.setContentHuggingPriority(.defaultHigh, for: .horizontal) + view.setContentHuggingPriority(.defaultHigh, for: .vertical) + + if let width = width { + NSLayoutConstraint.activate([ + view.widthAnchor.constraint(equalToConstant: width), + ]) + } + } +} + +public struct UIViewControllerPreview: UIViewControllerRepresentable { + public let viewController: ViewController + + public init(_ builder: @escaping () -> ViewController) { + viewController = builder() + } + + // MARK: - UIViewControllerRepresentable + public func makeUIViewController(context: Context) -> ViewController { + viewController + } + + @available(iOS 13.0, tvOS 13.0, *) + @available(OSX, unavailable) + @available(watchOS, unavailable) + public func updateUIViewController(_ uiViewController: ViewController, context: UIViewControllerRepresentableContext>) { + return + } +} +#endif From 832432f42f448694d74e6450e6a8c5d611ecc530 Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 22 Feb 2021 16:37:13 +0800 Subject: [PATCH 10/12] fix: make button display on the top of the keyboard --- .../MastodonRegisterViewController.swift | 47 ++++++++++++------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift b/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift index b8bfc64b..8b95538b 100644 --- a/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift +++ b/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift @@ -329,9 +329,18 @@ extension MastodonRegisterViewController { self.scrollView.verticalScrollIndicatorInsets.bottom = 0.0 return } - + self.scrollView.contentInset.bottom = padding + 16 self.scrollView.verticalScrollIndicatorInsets.bottom = padding + 16 + + if self.passwordTextField.isFirstResponder { + let contentFrame = self.scrollView.convert(self.signUpButton.frame, to: nil) + let labelPadding = contentFrame.maxY - endFrame.minY + let contentOffsetY = self.scrollView.contentOffset.y + DispatchQueue.main.async { + self.scrollView.setContentOffset(CGPoint(x: 0, y: contentOffsetY + labelPadding + 16.0), animated: true) + } + } }) .store(in: &disposeBag) @@ -425,23 +434,25 @@ extension MastodonRegisterViewController { } extension MastodonRegisterViewController: UITextFieldDelegate { - func textFieldDidBeginEditing(_ textField: UITextField) { - // align to password label when overlap - if textField === passwordTextField, - KeyboardResponderService.shared.isShow.value, - KeyboardResponderService.shared.state.value == .dock - { - let endFrame = KeyboardResponderService.shared.willEndFrame.value - let contentFrame = scrollView.convert(passwordCheckLabel.frame, to: nil) - let padding = contentFrame.maxY - endFrame.minY - if padding > 0 { - let contentOffsetY = scrollView.contentOffset.y - DispatchQueue.main.async { - self.scrollView.setContentOffset(CGPoint(x: 0, y: contentOffsetY + padding + 16.0), animated: true) - } - } - } - } + + // FIXME: keyboard listener trigger when switch between text fields. Maybe could remove it + // func textFieldDidBeginEditing(_ textField: UITextField) { + // // align to password label when overlap + // if textField === passwordTextField, + // KeyboardResponderService.shared.isShow.value, + // KeyboardResponderService.shared.state.value == .dock + // { + // let endFrame = KeyboardResponderService.shared.willEndFrame.value + // let contentFrame = scrollView.convert(signUpButton.frame, to: nil) + // let padding = contentFrame.maxY - endFrame.minY + // if padding > 0 { + // let contentOffsetY = scrollView.contentOffset.y + // DispatchQueue.main.async { + // self.scrollView.setContentOffset(CGPoint(x: 0, y: contentOffsetY + padding + 16.0), animated: true) + // } + // } + // } + // } func textFieldDidEndEditing(_ textField: UITextField) { switch textField { From 40a21a3a9fba8ddc8aeacd42e1f1b7f61e5d3d9e Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 22 Feb 2021 17:48:44 +0800 Subject: [PATCH 11/12] chore: make input responsible --- Mastodon/Generated/Assets.swift | 4 +- .../highlight.colorset/Contents.json | 20 + .../TextField/invalid.colorset/Contents.json | 20 + .../Contents.json | 0 .../MastodonRegisterViewController.swift | 130 ++-- .../MastodonRegisterViewController.swift.orig | 577 ++++++++++++++++++ .../Register/MastodonRegisterViewModel.swift | 118 ++-- 7 files changed, 759 insertions(+), 110 deletions(-) create mode 100644 Mastodon/Resources/Assets.xcassets/Colors/TextField/highlight.colorset/Contents.json create mode 100644 Mastodon/Resources/Assets.xcassets/Colors/TextField/invalid.colorset/Contents.json rename Mastodon/Resources/Assets.xcassets/Colors/TextField/{successGreen.colorset => valid.colorset}/Contents.json (100%) create mode 100644 Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift.orig diff --git a/Mastodon/Generated/Assets.swift b/Mastodon/Generated/Assets.swift index 4510a686..18f25f92 100644 --- a/Mastodon/Generated/Assets.swift +++ b/Mastodon/Generated/Assets.swift @@ -45,7 +45,9 @@ internal enum Asset { internal static let secondary = ColorAsset(name: "Colors/Label/secondary") } internal enum TextField { - internal static let successGreen = ColorAsset(name: "Colors/TextField/successGreen") + internal static let highlight = ColorAsset(name: "Colors/TextField/highlight") + internal static let invalid = ColorAsset(name: "Colors/TextField/invalid") + internal static let valid = ColorAsset(name: "Colors/TextField/valid") } internal static let lightAlertYellow = ColorAsset(name: "Colors/lightAlertYellow") internal static let lightBackground = ColorAsset(name: "Colors/lightBackground") diff --git a/Mastodon/Resources/Assets.xcassets/Colors/TextField/highlight.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/TextField/highlight.colorset/Contents.json new file mode 100644 index 00000000..d853a71a --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Colors/TextField/highlight.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "217", + "green" : "144", + "red" : "43" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Resources/Assets.xcassets/Colors/TextField/invalid.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/TextField/invalid.colorset/Contents.json new file mode 100644 index 00000000..dabccc33 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Colors/TextField/invalid.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.353", + "green" : "0.251", + "red" : "0.875" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Resources/Assets.xcassets/Colors/TextField/successGreen.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/TextField/valid.colorset/Contents.json similarity index 100% rename from Mastodon/Resources/Assets.xcassets/Colors/TextField/successGreen.colorset/Contents.json rename to Mastodon/Resources/Assets.xcassets/Colors/TextField/valid.colorset/Contents.json diff --git a/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift b/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift index 8b95538b..98f876c3 100644 --- a/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift +++ b/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift @@ -210,7 +210,7 @@ extension MastodonRegisterViewController { // gesture view.addGestureRecognizer(tapGestureRecognizer) - tapGestureRecognizer.addTarget(self, action: #selector(_resignFirstResponder)) + tapGestureRecognizer.addTarget(self, action: #selector(tapGestureRecognizerHandler)) // stackview let stackView = UIStackView() @@ -354,45 +354,42 @@ extension MastodonRegisterViewController { } .store(in: &disposeBag) - viewModel.isUsernameValid + viewModel.usernameValidateState .receive(on: DispatchQueue.main) - .sink { [weak self] isValid in + .sink { [weak self] validateState in guard let self = self else { return } - self.setTextFieldValidAppearance(self.usernameTextField, isValid: isValid) + self.setTextFieldValidAppearance(self.usernameTextField, validateState: validateState) } .store(in: &disposeBag) - viewModel.isDisplaynameValid + viewModel.displayNameValidateState .receive(on: DispatchQueue.main) - .sink { [weak self] isValid in + .sink { [weak self] validateState in guard let self = self else { return } - self.setTextFieldValidAppearance(self.displayNameTextField, isValid: isValid) + self.setTextFieldValidAppearance(self.displayNameTextField, validateState: validateState) } .store(in: &disposeBag) - viewModel.isEmailValid + viewModel.emailValidateState .receive(on: DispatchQueue.main) - .sink { [weak self] isValid in + .sink { [weak self] validateState in guard let self = self else { return } - self.setTextFieldValidAppearance(self.emailTextField, isValid: isValid) + self.setTextFieldValidAppearance(self.emailTextField, validateState: validateState) } .store(in: &disposeBag) - viewModel.isPasswordValid + viewModel.passwordValidateState .receive(on: DispatchQueue.main) - .sink { [weak self] isValid in + .sink { [weak self] validateState in guard let self = self else { return } - self.setTextFieldValidAppearance(self.passwordTextField, isValid: isValid) + self.setTextFieldValidAppearance(self.passwordTextField, validateState: validateState) + self.passwordCheckLabel.attributedText = self.viewModel.attributeStringForPassword(eightCharacters: validateState == .valid) + } .store(in: &disposeBag) - Publishers.CombineLatest4( - viewModel.isUsernameValid, - viewModel.isDisplaynameValid, - viewModel.isEmailValid, - viewModel.isPasswordValid - ) + viewModel.isAllValid .receive(on: DispatchQueue.main) - .sink { [weak self] isUsernameValid, isDisplaynameValid, isEmailValid, isPasswordValid in + .sink { [weak self] isAllValid in guard let self = self else { return } - self.signUpButton.isEnabled = isUsernameValid ?? false && isDisplaynameValid ?? false && isEmailValid ?? false && isPasswordValid ?? false + self.signUpButton.isEnabled = isAllValid } .store(in: &disposeBag) @@ -412,14 +409,39 @@ extension MastodonRegisterViewController { } .store(in: &disposeBag) + NotificationCenter.default + .publisher(for: UITextField.textDidChangeNotification, object: usernameTextField) + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + guard let self = self else { return } + self.viewModel.username.value = self.usernameTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + } + .store(in: &disposeBag) + + NotificationCenter.default + .publisher(for: UITextField.textDidChangeNotification, object: displayNameTextField) + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + guard let self = self else { return } + self.viewModel.displayName.value = self.displayNameTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + } + .store(in: &disposeBag) + + NotificationCenter.default + .publisher(for: UITextField.textDidChangeNotification, object: emailTextField) + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + guard let self = self else { return } + self.viewModel.email.value = self.emailTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + } + .store(in: &disposeBag) + NotificationCenter.default .publisher(for: UITextField.textDidChangeNotification, object: passwordTextField) .receive(on: DispatchQueue.main) .sink { [weak self] _ in guard let self = self else { return } - guard let text = self.passwordTextField.text else { return } - let validations = self.viewModel.validatePassword(text: text) - self.passwordCheckLabel.attributedText = self.viewModel.attributeStringForPassword(eightCharacters: validations.0, oneNumber: validations.1, oneSpecialCharacter: validations.2) + self.viewModel.password.value = self.passwordTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" } .store(in: &disposeBag) @@ -453,18 +475,21 @@ extension MastodonRegisterViewController: UITextFieldDelegate { // } // } // } - - func textFieldDidEndEditing(_ textField: UITextField) { + + func textFieldDidBeginEditing(_ textField: UITextField) { + let text = textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + switch textField { case usernameTextField: - viewModel.username.value = textField.text + viewModel.username.value = text case displayNameTextField: - viewModel.displayname.value = textField.text + viewModel.displayName.value = text case emailTextField: - viewModel.email.value = textField.text + viewModel.email.value = text case passwordTextField: - viewModel.password.value = textField.text - default: break + viewModel.password.value = text + default: + break } } @@ -477,44 +502,34 @@ extension MastodonRegisterViewController: UITextFieldDelegate { textField.layer.shadowPath = UIBezierPath(roundedRect: textField.bounds, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: 2.0, height: 2.0)).cgPath } - func validateAllTextField() -> Bool { - return viewModel.isUsernameValid.value ?? false && viewModel.isDisplaynameValid.value ?? false && viewModel.isEmailValid.value ?? false && viewModel.isPasswordValid.value ?? false - } - - private func setTextFieldValidAppearance(_ textField: UITextField, isValid: Bool?) { - guard let isValid = isValid else { - showShadowWithColor(color: .clear, textField: textField) - return - } - - if isValid { - showShadowWithColor(color: Asset.Colors.TextField.successGreen.color, textField: textField) - } else { - textField.shake() - showShadowWithColor(color: Asset.Colors.lightDangerRed.color, textField: textField) + private func setTextFieldValidAppearance(_ textField: UITextField, validateState: MastodonRegisterViewModel.ValidateState) { + switch validateState { + case .empty: + showShadowWithColor(color: textField.isFirstResponder ? Asset.Colors.TextField.highlight.color : .clear, textField: textField) + case .valid: + showShadowWithColor(color: Asset.Colors.TextField.valid.color, textField: textField) + case .invalid: + showShadowWithColor(color: Asset.Colors.TextField.invalid.color, textField: textField) } } } extension MastodonRegisterViewController { - @objc private func _resignFirstResponder() { - usernameTextField.resignFirstResponder() - displayNameTextField.resignFirstResponder() - emailTextField.resignFirstResponder() - passwordTextField.resignFirstResponder() + + @objc private func tapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) { + view.endEditing(true) } @objc private func signUpButtonPressed(_ sender: UIButton) { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", (#file as NSString).lastPathComponent, #line, #function) - guard validateAllTextField(), - let username = viewModel.username.value, - let email = viewModel.email.value, - let password = viewModel.password.value else { - return - } + guard viewModel.isAllValid.value else { return } guard !viewModel.isRegistering.value else { return } viewModel.isRegistering.value = true + + let username = viewModel.username.value + let email = viewModel.email.value + let password = viewModel.password.value if let rules = viewModel.instance.rules, !rules.isEmpty { let mastodonServerRulesViewModel = MastodonServerRulesViewModel( @@ -564,4 +579,5 @@ extension MastodonRegisterViewController { } .store(in: &disposeBag) } + } diff --git a/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift.orig b/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift.orig new file mode 100644 index 00000000..df2d7d9f --- /dev/null +++ b/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift.orig @@ -0,0 +1,577 @@ +// +// MastodonRegisterViewController.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-2-5. +// + +import Combine +import MastodonSDK +import os.log +import UIKit +import UITextField_Shake + +final class MastodonRegisterViewController: UIViewController, NeedsDependency { + var disposeBag = Set() + + weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } + weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } + + var viewModel: MastodonRegisterViewModel! + + let tapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer + + let statusBarBackground: UIView = { + let view = UIView() + view.backgroundColor = Asset.Colors.Background.onboardingBackground.color + return view + }() + + let scrollView: UIScrollView = { + let scrollview = UIScrollView() + scrollview.showsVerticalScrollIndicator = false + scrollview.translatesAutoresizingMaskIntoConstraints = false + scrollview.keyboardDismissMode = .interactive + scrollview.clipsToBounds = false // make content could display over bleeding + return scrollview + }() + + let largeTitleLabel: UILabel = { + let label = UILabel() + label.font = UIFontMetrics(forTextStyle: .largeTitle).scaledFont(for: UIFont.boldSystemFont(ofSize: 34)) + label.textColor = Asset.Colors.Label.black.color + label.text = L10n.Scene.Register.title + return label + }() + + let photoView: UIView = { + let view = UIView() + view.backgroundColor = .clear + return view + }() + + let photoButton: UIButton = { + let button = UIButton(type: .custom) + let boldFont = UIFont.systemFont(ofSize: 42) + let configuration = UIImage.SymbolConfiguration(font: boldFont) + let image = UIImage(systemName: "person.fill.viewfinder", withConfiguration: configuration) + + button.setImage(image?.withRenderingMode(UIImage.RenderingMode.alwaysTemplate), for: UIControl.State.normal) + button.imageView?.tintColor = Asset.Colors.Icon.photo.color + button.backgroundColor = .white + button.layer.cornerRadius = 45 + button.clipsToBounds = true + return button + }() + + let plusIconBackground: UIImageView = { + let icon = UIImageView() + let boldFont = UIFont.systemFont(ofSize: 24) + let configuration = UIImage.SymbolConfiguration(font: boldFont) + let image = UIImage(systemName: "plus.circle", withConfiguration: configuration) + icon.image = image + icon.tintColor = .white + return icon + }() + + let plusIcon: UIImageView = { + let icon = UIImageView() + let boldFont = UIFont.systemFont(ofSize: 24) + let configuration = UIImage.SymbolConfiguration(font: boldFont) + let image = UIImage(systemName: "plus.circle.fill", withConfiguration: configuration) + icon.image = image + icon.tintColor = Asset.Colors.Icon.plus.color + return icon + }() + + let domainLabel: UILabel = { + let label = UILabel() + label.font = .preferredFont(forTextStyle: .headline) + label.textColor = Asset.Colors.Label.black.color + return label + }() + + let usernameTextField: UITextField = { + let textField = UITextField() + + textField.autocapitalizationType = .none + textField.autocorrectionType = .no + textField.backgroundColor = .white + textField.textColor = .black + textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.Username.placeholder, + attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.lightSecondaryText.color, + NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .headline)]) + textField.borderStyle = UITextField.BorderStyle.roundedRect + let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height)) + textField.leftView = paddingView + textField.leftViewMode = .always + return textField + }() + + let usernameIsTakenLabel: UILabel = { + let label = UILabel() + return label + }() + + let displayNameTextField: UITextField = { + let textField = UITextField() + textField.autocapitalizationType = .none + textField.autocorrectionType = .no + textField.backgroundColor = .white + textField.textColor = .black + textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.DisplayName.placeholder, + attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.lightSecondaryText.color, + NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .headline)]) + textField.borderStyle = UITextField.BorderStyle.roundedRect + let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height)) + textField.leftView = paddingView + textField.leftViewMode = .always + return textField + }() + + let emailTextField: UITextField = { + let textField = UITextField() + textField.autocapitalizationType = .none + textField.autocorrectionType = .no + textField.keyboardType = .emailAddress + textField.backgroundColor = .white + textField.textColor = .black + textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.Email.placeholder, + attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.lightSecondaryText.color, + NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .headline)]) + textField.borderStyle = UITextField.BorderStyle.roundedRect + let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height)) + textField.leftView = paddingView + textField.leftViewMode = .always + return textField + }() + + let passwordCheckLabel: UILabel = { + let label = UILabel() + label.numberOfLines = 0 + return label + }() + + let passwordTextField: UITextField = { + let textField = UITextField() + textField.autocapitalizationType = .none + textField.autocorrectionType = .no + textField.keyboardType = .asciiCapable + textField.isSecureTextEntry = true + textField.backgroundColor = .white + textField.textColor = .black + textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.Password.placeholder, + attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.lightSecondaryText.color, + NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .headline)]) + textField.borderStyle = UITextField.BorderStyle.roundedRect + let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height)) + textField.leftView = paddingView + textField.leftViewMode = .always + return textField + }() + + let signUpButton: UIButton = { + let button = UIButton(type: .system) + button.titleLabel?.font = .preferredFont(forTextStyle: .headline) + button.setBackgroundImage(UIImage.placeholder(color: Asset.Colors.lightBrandBlue.color), for: .normal) + button.setBackgroundImage(UIImage.placeholder(color: Asset.Colors.lightDisabled.color), for: .disabled) + button.isEnabled = false + button.setTitleColor(Asset.Colors.Label.primary.color, for: .normal) + button.setTitle(L10n.Common.Controls.Actions.continue, for: .normal) + button.layer.masksToBounds = true + button.layer.cornerRadius = 8 + button.layer.cornerCurve = .continuous + return button + }() + + let signUpActivityIndicatorView: UIActivityIndicatorView = { + let activityIndicatorView = UIActivityIndicatorView(style: .medium) + activityIndicatorView.hidesWhenStopped = true + return activityIndicatorView + }() +} + +extension MastodonRegisterViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + overrideUserInterfaceStyle = .light + view.backgroundColor = Asset.Colors.Background.onboardingBackground.color + domainLabel.text = "@" + viewModel.domain + " " + domainLabel.sizeToFit() + passwordCheckLabel.attributedText = viewModel.attributeStringForPassword() + usernameTextField.rightView = domainLabel + usernameTextField.rightViewMode = .always + usernameTextField.delegate = self + displayNameTextField.delegate = self + emailTextField.delegate = self + passwordTextField.delegate = self + + // gesture + view.addGestureRecognizer(tapGestureRecognizer) + tapGestureRecognizer.addTarget(self, action: #selector(_resignFirstResponder)) + + // stackview + let stackView = UIStackView() + stackView.axis = .vertical + stackView.distribution = .fill + stackView.spacing = 40 +<<<<<<< HEAD + stackView.layoutMargins = UIEdgeInsets(top: 20, left: 0, bottom: 0, right: 0) +======= + stackView.layoutMargins = UIEdgeInsets(top: 0, left: 4, bottom: 26, right: 4) +>>>>>>> feature/signup + stackView.isLayoutMarginsRelativeArrangement = true + stackView.addArrangedSubview(largeTitleLabel) + stackView.addArrangedSubview(photoView) + stackView.addArrangedSubview(usernameTextField) + stackView.addArrangedSubview(displayNameTextField) + stackView.addArrangedSubview(emailTextField) + stackView.addArrangedSubview(passwordTextField) + stackView.addArrangedSubview(passwordCheckLabel) + + // scrollView + view.addSubview(scrollView) + NSLayoutConstraint.activate([ + scrollView.frameLayoutGuide.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor), + scrollView.frameLayoutGuide.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor), + view.readableContentGuide.trailingAnchor.constraint(equalTo: scrollView.frameLayoutGuide.trailingAnchor), + scrollView.frameLayoutGuide.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor), + scrollView.frameLayoutGuide.widthAnchor.constraint(equalTo: scrollView.contentLayoutGuide.widthAnchor), + ]) + + // stackview + scrollView.addSubview(stackView) + stackView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + stackView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor), + stackView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor), + stackView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor), + stackView.widthAnchor.constraint(equalTo: scrollView.contentLayoutGuide.widthAnchor), + scrollView.contentLayoutGuide.bottomAnchor.constraint(equalTo: stackView.bottomAnchor), + ]) + + statusBarBackground.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(statusBarBackground) + NSLayoutConstraint.activate([ + statusBarBackground.topAnchor.constraint(equalTo: view.topAnchor), + statusBarBackground.leadingAnchor.constraint(equalTo: view.leadingAnchor), + statusBarBackground.trailingAnchor.constraint(equalTo: view.trailingAnchor), + statusBarBackground.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor), + ]) + + // photoview + photoView.translatesAutoresizingMaskIntoConstraints = false + photoView.addSubview(photoButton) + NSLayoutConstraint.activate([ + photoView.heightAnchor.constraint(equalToConstant: 90).priority(.defaultHigh), + ]) + photoButton.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + photoButton.heightAnchor.constraint(equalToConstant: 90).priority(.defaultHigh), + photoButton.widthAnchor.constraint(equalToConstant: 90).priority(.defaultHigh), + photoButton.centerXAnchor.constraint(equalTo: photoView.centerXAnchor), + photoButton.centerYAnchor.constraint(equalTo: photoView.centerYAnchor), + ]) + plusIconBackground.translatesAutoresizingMaskIntoConstraints = false + photoView.addSubview(plusIconBackground) + NSLayoutConstraint.activate([ + plusIconBackground.trailingAnchor.constraint(equalTo: photoButton.trailingAnchor), + plusIconBackground.bottomAnchor.constraint(equalTo: photoButton.bottomAnchor), + ]) + plusIcon.translatesAutoresizingMaskIntoConstraints = false + photoView.addSubview(plusIcon) + NSLayoutConstraint.activate([ + plusIcon.trailingAnchor.constraint(equalTo: photoButton.trailingAnchor), + plusIcon.bottomAnchor.constraint(equalTo: photoButton.bottomAnchor), + ]) + + // textfield + NSLayoutConstraint.activate([ + usernameTextField.heightAnchor.constraint(equalToConstant: 50).priority(.defaultHigh), + displayNameTextField.heightAnchor.constraint(equalToConstant: 50).priority(.defaultHigh), + emailTextField.heightAnchor.constraint(equalToConstant: 50).priority(.defaultHigh), + passwordTextField.heightAnchor.constraint(equalToConstant: 50).priority(.defaultHigh), + ]) + + // password + stackView.setCustomSpacing(6, after: passwordTextField) + stackView.setCustomSpacing(32, after: passwordCheckLabel) + + // button + signUpButton.translatesAutoresizingMaskIntoConstraints = false + stackView.addArrangedSubview(signUpButton) + NSLayoutConstraint.activate([ + signUpButton.heightAnchor.constraint(equalToConstant: 46).priority(.defaultHigh), + ]) + + signUpActivityIndicatorView.translatesAutoresizingMaskIntoConstraints = false + scrollView.addSubview(signUpActivityIndicatorView) + NSLayoutConstraint.activate([ + signUpActivityIndicatorView.centerXAnchor.constraint(equalTo: signUpButton.centerXAnchor), + signUpActivityIndicatorView.centerYAnchor.constraint(equalTo: signUpButton.centerYAnchor), + ]) + + Publishers.CombineLatest( + KeyboardResponderService.shared.state.eraseToAnyPublisher(), + KeyboardResponderService.shared.willEndFrame.eraseToAnyPublisher() + ) + .sink(receiveValue: { [weak self] state, endFrame in + guard let self = self else { return } + +<<<<<<< HEAD + guard isShow, state == .dock else { + self.scrollView.contentInset.bottom = 0.0 + self.scrollView.verticalScrollIndicatorInsets.bottom = 0.0 + return + } + + // isShow AND dock state + let contentFrame = self.view.convert(self.scrollView.frame, to: nil) +======= + guard state == .dock else { + self.scrollview.contentInset.bottom = 0.0 + self.scrollview.verticalScrollIndicatorInsets.bottom = 0.0 + return + } + + let contentFrame = self.view.convert(self.scrollview.frame, to: nil) +>>>>>>> feature/signup + let padding = contentFrame.maxY - endFrame.minY + guard padding > 0 else { + self.scrollView.contentInset.bottom = 0.0 + self.scrollView.verticalScrollIndicatorInsets.bottom = 0.0 + return + } + + self.scrollView.contentInset.bottom = padding + 16 + self.scrollView.verticalScrollIndicatorInsets.bottom = padding + 16 + }) + .store(in: &disposeBag) + + viewModel.isRegistering + .receive(on: DispatchQueue.main) + .sink { [weak self] isRegistering in + guard let self = self else { return } + isRegistering ? self.signUpActivityIndicatorView.startAnimating() : self.signUpActivityIndicatorView.stopAnimating() + self.signUpButton.setTitle(isRegistering ? "" : L10n.Common.Controls.Actions.continue, for: .normal) + self.signUpButton.isEnabled = !isRegistering + } + .store(in: &disposeBag) + + viewModel.isUsernameValid + .receive(on: DispatchQueue.main) + .sink { [weak self] isValid in + guard let self = self else { return } + self.setTextFieldValidAppearance(self.usernameTextField, isValid: isValid) + } + .store(in: &disposeBag) + viewModel.isDisplaynameValid + .receive(on: DispatchQueue.main) + .sink { [weak self] isValid in + guard let self = self else { return } + self.setTextFieldValidAppearance(self.displayNameTextField, isValid: isValid) + } + .store(in: &disposeBag) + viewModel.isEmailValid + .receive(on: DispatchQueue.main) + .sink { [weak self] isValid in + guard let self = self else { return } + self.setTextFieldValidAppearance(self.emailTextField, isValid: isValid) + } + .store(in: &disposeBag) + viewModel.isPasswordValid + .receive(on: DispatchQueue.main) + .sink { [weak self] isValid in + guard let self = self else { return } + self.setTextFieldValidAppearance(self.passwordTextField, isValid: isValid) + } + .store(in: &disposeBag) + + Publishers.CombineLatest4( + viewModel.isUsernameValid, + viewModel.isDisplaynameValid, + viewModel.isEmailValid, + viewModel.isPasswordValid + ) + .receive(on: DispatchQueue.main) + .sink { [weak self] isUsernameValid, isDisplaynameValid, isEmailValid, isPasswordValid in + guard let self = self else { return } + self.signUpButton.isEnabled = isUsernameValid ?? false && isDisplaynameValid ?? false && isEmailValid ?? false && isPasswordValid ?? false + } + .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(error, 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) + + NotificationCenter.default + .publisher(for: UITextField.textDidChangeNotification, object: passwordTextField) + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + guard let self = self else { return } + guard let text = self.passwordTextField.text else { return } + let validations = self.viewModel.validatePassword(text: text) + self.passwordCheckLabel.attributedText = self.viewModel.attributeStringForPassword(eightCharacters: validations.0, oneNumber: validations.1, oneSpecialCharacter: validations.2) + } + .store(in: &disposeBag) + + signUpButton.addTarget(self, action: #selector(MastodonRegisterViewController.signUpButtonPressed(_:)), for: .touchUpInside) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + navigationController?.setNavigationBarHidden(true, animated: false) + } +} + +extension MastodonRegisterViewController: UITextFieldDelegate { + func textFieldDidBeginEditing(_ textField: UITextField) { + // align to password label when overlap + if textField === passwordTextField, + KeyboardResponderService.shared.isShow.value, +<<<<<<< HEAD + KeyboardResponderService.shared.state.value == .dock { + let endFrame = KeyboardResponderService.shared.endFrame.value + let contentFrame = self.scrollView.convert(self.passwordCheckLabel.frame, to: nil) +======= + KeyboardResponderService.shared.state.value == .dock + { + let endFrame = KeyboardResponderService.shared.willEndFrame.value + let contentFrame = scrollview.convert(passwordCheckLabel.frame, to: nil) +>>>>>>> feature/signup + let padding = contentFrame.maxY - endFrame.minY + if padding > 0 { + let contentOffsetY = scrollView.contentOffset.y + DispatchQueue.main.async { + self.scrollView.setContentOffset(CGPoint(x: 0, y: contentOffsetY + padding + 16.0), animated: true) + } + } + } + } + + func textFieldDidEndEditing(_ textField: UITextField) { + switch textField { + case usernameTextField: + viewModel.username.value = textField.text + case displayNameTextField: + viewModel.displayname.value = textField.text + case emailTextField: + viewModel.email.value = textField.text + case passwordTextField: + viewModel.password.value = textField.text + default: break + } + } + + func showShadowWithColor(color: UIColor, textField: UITextField) { + // To apply Shadow + textField.layer.shadowOpacity = 1 + textField.layer.shadowRadius = 2.0 + textField.layer.shadowOffset = CGSize.zero + textField.layer.shadowColor = color.cgColor + textField.layer.shadowPath = UIBezierPath(roundedRect: textField.bounds, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: 2.0, height: 2.0)).cgPath + } + + func validateAllTextField() -> Bool { + return viewModel.isUsernameValid.value ?? false && viewModel.isDisplaynameValid.value ?? false && viewModel.isEmailValid.value ?? false && viewModel.isPasswordValid.value ?? false + } + + private func setTextFieldValidAppearance(_ textField: UITextField, isValid: Bool?) { + guard let isValid = isValid else { + showShadowWithColor(color: .clear, textField: textField) + return + } + + if isValid { + showShadowWithColor(color: Asset.Colors.TextField.successGreen.color, textField: textField) + } else { + textField.shake() + showShadowWithColor(color: Asset.Colors.lightDangerRed.color, textField: textField) + } + } +} + +extension MastodonRegisterViewController { + @objc private func _resignFirstResponder() { + usernameTextField.resignFirstResponder() + displayNameTextField.resignFirstResponder() + emailTextField.resignFirstResponder() + passwordTextField.resignFirstResponder() + } + + @objc private func signUpButtonPressed(_ sender: UIButton) { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", (#file as NSString).lastPathComponent, #line, #function) + guard validateAllTextField(), + let username = viewModel.username.value, + let email = viewModel.email.value, + let password = viewModel.password.value else { + return + } + + guard !viewModel.isRegistering.value else { return } + viewModel.isRegistering.value = true + + if let rules = viewModel.instance.rules, !rules.isEmpty { + let mastodonServerRulesViewModel = MastodonServerRulesViewModel( + context: context, + domain: viewModel.domain, + rules: rules + ) + coordinator.present(scene: .mastodonServerRules(viewModel: mastodonServerRulesViewModel), from: self, transition: .show) + return + } + + let query = Mastodon.API.Account.RegisterQuery( + reason: nil, + username: username, + email: email, + password: password, + agreement: true, // TODO: + locale: "en" // TODO: + ) + + 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 } + _ = response.value + // TODO: + let alertController = UIAlertController(title: "Success", message: "Regsiter request sent. Please check your email.\n(Auto sign in not implement yet.)", preferredStyle: .alert) + let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default) { [weak self] _ in + guard let self = self else { return } + self.navigationController?.popViewController(animated: true) + } + alertController.addAction(okAction) + self.coordinator.present(scene: .alertController(alertController: alertController), from: self, transition: .alertController(animated: true, completion: nil)) + } + .store(in: &disposeBag) + } +} diff --git a/Mastodon/Scene/Authentication/Register/MastodonRegisterViewModel.swift b/Mastodon/Scene/Authentication/Register/MastodonRegisterViewModel.swift index daea2f53..a4d79195 100644 --- a/Mastodon/Scene/Authentication/Register/MastodonRegisterViewModel.swift +++ b/Mastodon/Scene/Authentication/Register/MastodonRegisterViewModel.swift @@ -11,6 +11,7 @@ import MastodonSDK import UIKit final class MastodonRegisterViewModel { + var disposeBag = Set() // input @@ -19,19 +20,26 @@ final class MastodonRegisterViewModel { let instance: Mastodon.Entity.Instance let applicationToken: Mastodon.Entity.Token + let username = CurrentValueSubject("") + let displayName = CurrentValueSubject("") + let email = CurrentValueSubject("") + let password = CurrentValueSubject("") + let isUsernameValidateDalay = CurrentValueSubject(true) + let isDisplayNameValidateDalay = CurrentValueSubject(true) + let isEmailValidateDalay = CurrentValueSubject(true) + let isPasswordValidateDalay = CurrentValueSubject(true) let isRegistering = CurrentValueSubject(false) - let username = CurrentValueSubject(nil) - let displayname = CurrentValueSubject(nil) - let email = CurrentValueSubject(nil) - let password = CurrentValueSubject(nil) + // output let applicationAuthorization: Mastodon.API.OAuth.Authorization - let isUsernameValid = CurrentValueSubject(nil) - let isDisplaynameValid = CurrentValueSubject(nil) - let isEmailValid = CurrentValueSubject(nil) - let isPasswordValid = CurrentValueSubject(nil) + let usernameValidateState = CurrentValueSubject(.empty) + let displayNameValidateState = CurrentValueSubject(.empty) + let emailValidateState = CurrentValueSubject(.empty) + let passwordValidateState = CurrentValueSubject(.empty) + + let isAllValid = CurrentValueSubject(false) let error = CurrentValueSubject(nil) @@ -49,61 +57,75 @@ final class MastodonRegisterViewModel { username .map { username in - guard let username = username else { - return nil + guard !username.isEmpty else { return .empty } + var isValid = true + + // regex opt-out way to check validation + // allowed: + // a-z (isASCII && isLetter) + // A-Z (isASCII && isLetter) + // 0-9 (isASCII && isNumber) + // _ ("_") + for char in username { + guard char.isASCII, (char.isLetter || char.isNumber || char == "_") else { + isValid = false + break + } } - return !username.isEmpty + return isValid ? .valid : .invalid } - .assign(to: \.value, on: isUsernameValid) + .assign(to: \.value, on: usernameValidateState) .store(in: &disposeBag) - displayname + displayName .map { displayname in - guard let displayname = displayname else { - return nil - } - return !displayname.isEmpty + guard !displayname.isEmpty else { return .empty } + return .valid } - .assign(to: \.value, on: isDisplaynameValid) + .assign(to: \.value, on: displayNameValidateState) .store(in: &disposeBag) email - .map { [weak self] email in - guard let self = self else { return nil } - guard let email = email else { - return nil - } - return !email.isEmpty && self.isValidEmail(email) + .map { email in + guard !email.isEmpty else { return .empty } + return MastodonRegisterViewModel.isValidEmail(email) ? .valid : .invalid } - .assign(to: \.value, on: isEmailValid) + .assign(to: \.value, on: emailValidateState) .store(in: &disposeBag) password - .map { [weak self] password in - guard let self = self else { return nil } - guard let password = password else { - return nil - } - let result = self.validatePassword(text: password) - return !password.isEmpty && result.0 && result.1 && result.2 + .map { password in + guard !password.isEmpty else { return .empty } + return password.count >= 8 ? .valid : .invalid } - .assign(to: \.value, on: isPasswordValid) + .assign(to: \.value, on: passwordValidateState) .store(in: &disposeBag) + + Publishers.CombineLatest4( + usernameValidateState.eraseToAnyPublisher(), + displayNameValidateState.eraseToAnyPublisher(), + emailValidateState.eraseToAnyPublisher(), + passwordValidateState.eraseToAnyPublisher() + ) + .map { $0.0 == .valid && $0.1 == .valid && $0.2 == .valid && $0.3 == .valid } + .assign(to: \.value, on: isAllValid) + .store(in: &disposeBag) + } + +} + +extension MastodonRegisterViewModel { + enum ValidateState { + case empty + case invalid + case valid } } extension MastodonRegisterViewModel { - func isValidEmail(_ email: String) -> Bool { + static func isValidEmail(_ email: String) -> Bool { let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" let emailPred = NSPredicate(format: "SELF MATCHES %@", emailRegEx) return emailPred.evaluate(with: email) } - - func validatePassword(text: String) -> (Bool, Bool, Bool) { - let trimmedText = text.trimmingCharacters(in: .whitespaces) - let isEightCharacters = trimmedText.count >= 8 - let isOneNumber = trimmedText.range(of: ".*[0-9]", options: .regularExpression) != nil - let isOneSpecialCharacter = trimmedText.trimmingCharacters(in: .decimalDigits).trimmingCharacters(in: .letters).count > 0 - return (isEightCharacters, isOneNumber, isOneSpecialCharacter) - } func attributeStringForUsername() -> NSAttributedString { let resultAttributeString = NSMutableAttributedString() @@ -118,7 +140,7 @@ extension MastodonRegisterViewModel { return resultAttributeString } - func attributeStringForPassword(eightCharacters: Bool = false, oneNumber: Bool = false, oneSpecialCharacter: Bool = false) -> NSAttributedString { + func attributeStringForPassword(eightCharacters: Bool = false) -> NSAttributedString { let font = UIFont.preferredFont(forTextStyle: .caption1) let color = UIColor.black let falseColor = UIColor.clear @@ -128,17 +150,9 @@ extension MastodonRegisterViewModel { attributeString.append(start) attributeString.append(checkmarkImage(color: eightCharacters ? color : falseColor)) - let eightCharactersDescription = NSAttributedString(string: "Eight characters\n", attributes: [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: color]) + let eightCharactersDescription = NSAttributedString(string: " Eight characters\n", attributes: [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: color]) attributeString.append(eightCharactersDescription) - attributeString.append(checkmarkImage(color: oneNumber ? color : falseColor)) - let oneNumberDescription = NSAttributedString(string: "One number\n", attributes: [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: color]) - attributeString.append(oneNumberDescription) - - attributeString.append(checkmarkImage(color: oneSpecialCharacter ? color : falseColor)) - let oneSpecialCharacterDescription = NSAttributedString(string: "One special character\n", attributes: [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: color]) - attributeString.append(oneSpecialCharacterDescription) - return attributeString } From db2761447b74787fec6766d532f939b0d6144abf Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 22 Feb 2021 17:55:43 +0800 Subject: [PATCH 12/12] chore: remove ignore file --- .../MastodonRegisterViewController.swift.orig | 577 ------------------ 1 file changed, 577 deletions(-) delete mode 100644 Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift.orig diff --git a/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift.orig b/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift.orig deleted file mode 100644 index df2d7d9f..00000000 --- a/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift.orig +++ /dev/null @@ -1,577 +0,0 @@ -// -// MastodonRegisterViewController.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021-2-5. -// - -import Combine -import MastodonSDK -import os.log -import UIKit -import UITextField_Shake - -final class MastodonRegisterViewController: UIViewController, NeedsDependency { - var disposeBag = Set() - - weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } - weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } - - var viewModel: MastodonRegisterViewModel! - - let tapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer - - let statusBarBackground: UIView = { - let view = UIView() - view.backgroundColor = Asset.Colors.Background.onboardingBackground.color - return view - }() - - let scrollView: UIScrollView = { - let scrollview = UIScrollView() - scrollview.showsVerticalScrollIndicator = false - scrollview.translatesAutoresizingMaskIntoConstraints = false - scrollview.keyboardDismissMode = .interactive - scrollview.clipsToBounds = false // make content could display over bleeding - return scrollview - }() - - let largeTitleLabel: UILabel = { - let label = UILabel() - label.font = UIFontMetrics(forTextStyle: .largeTitle).scaledFont(for: UIFont.boldSystemFont(ofSize: 34)) - label.textColor = Asset.Colors.Label.black.color - label.text = L10n.Scene.Register.title - return label - }() - - let photoView: UIView = { - let view = UIView() - view.backgroundColor = .clear - return view - }() - - let photoButton: UIButton = { - let button = UIButton(type: .custom) - let boldFont = UIFont.systemFont(ofSize: 42) - let configuration = UIImage.SymbolConfiguration(font: boldFont) - let image = UIImage(systemName: "person.fill.viewfinder", withConfiguration: configuration) - - button.setImage(image?.withRenderingMode(UIImage.RenderingMode.alwaysTemplate), for: UIControl.State.normal) - button.imageView?.tintColor = Asset.Colors.Icon.photo.color - button.backgroundColor = .white - button.layer.cornerRadius = 45 - button.clipsToBounds = true - return button - }() - - let plusIconBackground: UIImageView = { - let icon = UIImageView() - let boldFont = UIFont.systemFont(ofSize: 24) - let configuration = UIImage.SymbolConfiguration(font: boldFont) - let image = UIImage(systemName: "plus.circle", withConfiguration: configuration) - icon.image = image - icon.tintColor = .white - return icon - }() - - let plusIcon: UIImageView = { - let icon = UIImageView() - let boldFont = UIFont.systemFont(ofSize: 24) - let configuration = UIImage.SymbolConfiguration(font: boldFont) - let image = UIImage(systemName: "plus.circle.fill", withConfiguration: configuration) - icon.image = image - icon.tintColor = Asset.Colors.Icon.plus.color - return icon - }() - - let domainLabel: UILabel = { - let label = UILabel() - label.font = .preferredFont(forTextStyle: .headline) - label.textColor = Asset.Colors.Label.black.color - return label - }() - - let usernameTextField: UITextField = { - let textField = UITextField() - - textField.autocapitalizationType = .none - textField.autocorrectionType = .no - textField.backgroundColor = .white - textField.textColor = .black - textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.Username.placeholder, - attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.lightSecondaryText.color, - NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .headline)]) - textField.borderStyle = UITextField.BorderStyle.roundedRect - let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height)) - textField.leftView = paddingView - textField.leftViewMode = .always - return textField - }() - - let usernameIsTakenLabel: UILabel = { - let label = UILabel() - return label - }() - - let displayNameTextField: UITextField = { - let textField = UITextField() - textField.autocapitalizationType = .none - textField.autocorrectionType = .no - textField.backgroundColor = .white - textField.textColor = .black - textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.DisplayName.placeholder, - attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.lightSecondaryText.color, - NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .headline)]) - textField.borderStyle = UITextField.BorderStyle.roundedRect - let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height)) - textField.leftView = paddingView - textField.leftViewMode = .always - return textField - }() - - let emailTextField: UITextField = { - let textField = UITextField() - textField.autocapitalizationType = .none - textField.autocorrectionType = .no - textField.keyboardType = .emailAddress - textField.backgroundColor = .white - textField.textColor = .black - textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.Email.placeholder, - attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.lightSecondaryText.color, - NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .headline)]) - textField.borderStyle = UITextField.BorderStyle.roundedRect - let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height)) - textField.leftView = paddingView - textField.leftViewMode = .always - return textField - }() - - let passwordCheckLabel: UILabel = { - let label = UILabel() - label.numberOfLines = 0 - return label - }() - - let passwordTextField: UITextField = { - let textField = UITextField() - textField.autocapitalizationType = .none - textField.autocorrectionType = .no - textField.keyboardType = .asciiCapable - textField.isSecureTextEntry = true - textField.backgroundColor = .white - textField.textColor = .black - textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.Password.placeholder, - attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.lightSecondaryText.color, - NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .headline)]) - textField.borderStyle = UITextField.BorderStyle.roundedRect - let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height)) - textField.leftView = paddingView - textField.leftViewMode = .always - return textField - }() - - let signUpButton: UIButton = { - let button = UIButton(type: .system) - button.titleLabel?.font = .preferredFont(forTextStyle: .headline) - button.setBackgroundImage(UIImage.placeholder(color: Asset.Colors.lightBrandBlue.color), for: .normal) - button.setBackgroundImage(UIImage.placeholder(color: Asset.Colors.lightDisabled.color), for: .disabled) - button.isEnabled = false - button.setTitleColor(Asset.Colors.Label.primary.color, for: .normal) - button.setTitle(L10n.Common.Controls.Actions.continue, for: .normal) - button.layer.masksToBounds = true - button.layer.cornerRadius = 8 - button.layer.cornerCurve = .continuous - return button - }() - - let signUpActivityIndicatorView: UIActivityIndicatorView = { - let activityIndicatorView = UIActivityIndicatorView(style: .medium) - activityIndicatorView.hidesWhenStopped = true - return activityIndicatorView - }() -} - -extension MastodonRegisterViewController { - - override func viewDidLoad() { - super.viewDidLoad() - - overrideUserInterfaceStyle = .light - view.backgroundColor = Asset.Colors.Background.onboardingBackground.color - domainLabel.text = "@" + viewModel.domain + " " - domainLabel.sizeToFit() - passwordCheckLabel.attributedText = viewModel.attributeStringForPassword() - usernameTextField.rightView = domainLabel - usernameTextField.rightViewMode = .always - usernameTextField.delegate = self - displayNameTextField.delegate = self - emailTextField.delegate = self - passwordTextField.delegate = self - - // gesture - view.addGestureRecognizer(tapGestureRecognizer) - tapGestureRecognizer.addTarget(self, action: #selector(_resignFirstResponder)) - - // stackview - let stackView = UIStackView() - stackView.axis = .vertical - stackView.distribution = .fill - stackView.spacing = 40 -<<<<<<< HEAD - stackView.layoutMargins = UIEdgeInsets(top: 20, left: 0, bottom: 0, right: 0) -======= - stackView.layoutMargins = UIEdgeInsets(top: 0, left: 4, bottom: 26, right: 4) ->>>>>>> feature/signup - stackView.isLayoutMarginsRelativeArrangement = true - stackView.addArrangedSubview(largeTitleLabel) - stackView.addArrangedSubview(photoView) - stackView.addArrangedSubview(usernameTextField) - stackView.addArrangedSubview(displayNameTextField) - stackView.addArrangedSubview(emailTextField) - stackView.addArrangedSubview(passwordTextField) - stackView.addArrangedSubview(passwordCheckLabel) - - // scrollView - view.addSubview(scrollView) - NSLayoutConstraint.activate([ - scrollView.frameLayoutGuide.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor), - scrollView.frameLayoutGuide.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor), - view.readableContentGuide.trailingAnchor.constraint(equalTo: scrollView.frameLayoutGuide.trailingAnchor), - scrollView.frameLayoutGuide.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor), - scrollView.frameLayoutGuide.widthAnchor.constraint(equalTo: scrollView.contentLayoutGuide.widthAnchor), - ]) - - // stackview - scrollView.addSubview(stackView) - stackView.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - stackView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor), - stackView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor), - stackView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor), - stackView.widthAnchor.constraint(equalTo: scrollView.contentLayoutGuide.widthAnchor), - scrollView.contentLayoutGuide.bottomAnchor.constraint(equalTo: stackView.bottomAnchor), - ]) - - statusBarBackground.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(statusBarBackground) - NSLayoutConstraint.activate([ - statusBarBackground.topAnchor.constraint(equalTo: view.topAnchor), - statusBarBackground.leadingAnchor.constraint(equalTo: view.leadingAnchor), - statusBarBackground.trailingAnchor.constraint(equalTo: view.trailingAnchor), - statusBarBackground.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor), - ]) - - // photoview - photoView.translatesAutoresizingMaskIntoConstraints = false - photoView.addSubview(photoButton) - NSLayoutConstraint.activate([ - photoView.heightAnchor.constraint(equalToConstant: 90).priority(.defaultHigh), - ]) - photoButton.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - photoButton.heightAnchor.constraint(equalToConstant: 90).priority(.defaultHigh), - photoButton.widthAnchor.constraint(equalToConstant: 90).priority(.defaultHigh), - photoButton.centerXAnchor.constraint(equalTo: photoView.centerXAnchor), - photoButton.centerYAnchor.constraint(equalTo: photoView.centerYAnchor), - ]) - plusIconBackground.translatesAutoresizingMaskIntoConstraints = false - photoView.addSubview(plusIconBackground) - NSLayoutConstraint.activate([ - plusIconBackground.trailingAnchor.constraint(equalTo: photoButton.trailingAnchor), - plusIconBackground.bottomAnchor.constraint(equalTo: photoButton.bottomAnchor), - ]) - plusIcon.translatesAutoresizingMaskIntoConstraints = false - photoView.addSubview(plusIcon) - NSLayoutConstraint.activate([ - plusIcon.trailingAnchor.constraint(equalTo: photoButton.trailingAnchor), - plusIcon.bottomAnchor.constraint(equalTo: photoButton.bottomAnchor), - ]) - - // textfield - NSLayoutConstraint.activate([ - usernameTextField.heightAnchor.constraint(equalToConstant: 50).priority(.defaultHigh), - displayNameTextField.heightAnchor.constraint(equalToConstant: 50).priority(.defaultHigh), - emailTextField.heightAnchor.constraint(equalToConstant: 50).priority(.defaultHigh), - passwordTextField.heightAnchor.constraint(equalToConstant: 50).priority(.defaultHigh), - ]) - - // password - stackView.setCustomSpacing(6, after: passwordTextField) - stackView.setCustomSpacing(32, after: passwordCheckLabel) - - // button - signUpButton.translatesAutoresizingMaskIntoConstraints = false - stackView.addArrangedSubview(signUpButton) - NSLayoutConstraint.activate([ - signUpButton.heightAnchor.constraint(equalToConstant: 46).priority(.defaultHigh), - ]) - - signUpActivityIndicatorView.translatesAutoresizingMaskIntoConstraints = false - scrollView.addSubview(signUpActivityIndicatorView) - NSLayoutConstraint.activate([ - signUpActivityIndicatorView.centerXAnchor.constraint(equalTo: signUpButton.centerXAnchor), - signUpActivityIndicatorView.centerYAnchor.constraint(equalTo: signUpButton.centerYAnchor), - ]) - - Publishers.CombineLatest( - KeyboardResponderService.shared.state.eraseToAnyPublisher(), - KeyboardResponderService.shared.willEndFrame.eraseToAnyPublisher() - ) - .sink(receiveValue: { [weak self] state, endFrame in - guard let self = self else { return } - -<<<<<<< HEAD - guard isShow, state == .dock else { - self.scrollView.contentInset.bottom = 0.0 - self.scrollView.verticalScrollIndicatorInsets.bottom = 0.0 - return - } - - // isShow AND dock state - let contentFrame = self.view.convert(self.scrollView.frame, to: nil) -======= - guard state == .dock else { - self.scrollview.contentInset.bottom = 0.0 - self.scrollview.verticalScrollIndicatorInsets.bottom = 0.0 - return - } - - let contentFrame = self.view.convert(self.scrollview.frame, to: nil) ->>>>>>> feature/signup - let padding = contentFrame.maxY - endFrame.minY - guard padding > 0 else { - self.scrollView.contentInset.bottom = 0.0 - self.scrollView.verticalScrollIndicatorInsets.bottom = 0.0 - return - } - - self.scrollView.contentInset.bottom = padding + 16 - self.scrollView.verticalScrollIndicatorInsets.bottom = padding + 16 - }) - .store(in: &disposeBag) - - viewModel.isRegistering - .receive(on: DispatchQueue.main) - .sink { [weak self] isRegistering in - guard let self = self else { return } - isRegistering ? self.signUpActivityIndicatorView.startAnimating() : self.signUpActivityIndicatorView.stopAnimating() - self.signUpButton.setTitle(isRegistering ? "" : L10n.Common.Controls.Actions.continue, for: .normal) - self.signUpButton.isEnabled = !isRegistering - } - .store(in: &disposeBag) - - viewModel.isUsernameValid - .receive(on: DispatchQueue.main) - .sink { [weak self] isValid in - guard let self = self else { return } - self.setTextFieldValidAppearance(self.usernameTextField, isValid: isValid) - } - .store(in: &disposeBag) - viewModel.isDisplaynameValid - .receive(on: DispatchQueue.main) - .sink { [weak self] isValid in - guard let self = self else { return } - self.setTextFieldValidAppearance(self.displayNameTextField, isValid: isValid) - } - .store(in: &disposeBag) - viewModel.isEmailValid - .receive(on: DispatchQueue.main) - .sink { [weak self] isValid in - guard let self = self else { return } - self.setTextFieldValidAppearance(self.emailTextField, isValid: isValid) - } - .store(in: &disposeBag) - viewModel.isPasswordValid - .receive(on: DispatchQueue.main) - .sink { [weak self] isValid in - guard let self = self else { return } - self.setTextFieldValidAppearance(self.passwordTextField, isValid: isValid) - } - .store(in: &disposeBag) - - Publishers.CombineLatest4( - viewModel.isUsernameValid, - viewModel.isDisplaynameValid, - viewModel.isEmailValid, - viewModel.isPasswordValid - ) - .receive(on: DispatchQueue.main) - .sink { [weak self] isUsernameValid, isDisplaynameValid, isEmailValid, isPasswordValid in - guard let self = self else { return } - self.signUpButton.isEnabled = isUsernameValid ?? false && isDisplaynameValid ?? false && isEmailValid ?? false && isPasswordValid ?? false - } - .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(error, 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) - - NotificationCenter.default - .publisher(for: UITextField.textDidChangeNotification, object: passwordTextField) - .receive(on: DispatchQueue.main) - .sink { [weak self] _ in - guard let self = self else { return } - guard let text = self.passwordTextField.text else { return } - let validations = self.viewModel.validatePassword(text: text) - self.passwordCheckLabel.attributedText = self.viewModel.attributeStringForPassword(eightCharacters: validations.0, oneNumber: validations.1, oneSpecialCharacter: validations.2) - } - .store(in: &disposeBag) - - signUpButton.addTarget(self, action: #selector(MastodonRegisterViewController.signUpButtonPressed(_:)), for: .touchUpInside) - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - navigationController?.setNavigationBarHidden(true, animated: false) - } -} - -extension MastodonRegisterViewController: UITextFieldDelegate { - func textFieldDidBeginEditing(_ textField: UITextField) { - // align to password label when overlap - if textField === passwordTextField, - KeyboardResponderService.shared.isShow.value, -<<<<<<< HEAD - KeyboardResponderService.shared.state.value == .dock { - let endFrame = KeyboardResponderService.shared.endFrame.value - let contentFrame = self.scrollView.convert(self.passwordCheckLabel.frame, to: nil) -======= - KeyboardResponderService.shared.state.value == .dock - { - let endFrame = KeyboardResponderService.shared.willEndFrame.value - let contentFrame = scrollview.convert(passwordCheckLabel.frame, to: nil) ->>>>>>> feature/signup - let padding = contentFrame.maxY - endFrame.minY - if padding > 0 { - let contentOffsetY = scrollView.contentOffset.y - DispatchQueue.main.async { - self.scrollView.setContentOffset(CGPoint(x: 0, y: contentOffsetY + padding + 16.0), animated: true) - } - } - } - } - - func textFieldDidEndEditing(_ textField: UITextField) { - switch textField { - case usernameTextField: - viewModel.username.value = textField.text - case displayNameTextField: - viewModel.displayname.value = textField.text - case emailTextField: - viewModel.email.value = textField.text - case passwordTextField: - viewModel.password.value = textField.text - default: break - } - } - - func showShadowWithColor(color: UIColor, textField: UITextField) { - // To apply Shadow - textField.layer.shadowOpacity = 1 - textField.layer.shadowRadius = 2.0 - textField.layer.shadowOffset = CGSize.zero - textField.layer.shadowColor = color.cgColor - textField.layer.shadowPath = UIBezierPath(roundedRect: textField.bounds, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: 2.0, height: 2.0)).cgPath - } - - func validateAllTextField() -> Bool { - return viewModel.isUsernameValid.value ?? false && viewModel.isDisplaynameValid.value ?? false && viewModel.isEmailValid.value ?? false && viewModel.isPasswordValid.value ?? false - } - - private func setTextFieldValidAppearance(_ textField: UITextField, isValid: Bool?) { - guard let isValid = isValid else { - showShadowWithColor(color: .clear, textField: textField) - return - } - - if isValid { - showShadowWithColor(color: Asset.Colors.TextField.successGreen.color, textField: textField) - } else { - textField.shake() - showShadowWithColor(color: Asset.Colors.lightDangerRed.color, textField: textField) - } - } -} - -extension MastodonRegisterViewController { - @objc private func _resignFirstResponder() { - usernameTextField.resignFirstResponder() - displayNameTextField.resignFirstResponder() - emailTextField.resignFirstResponder() - passwordTextField.resignFirstResponder() - } - - @objc private func signUpButtonPressed(_ sender: UIButton) { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", (#file as NSString).lastPathComponent, #line, #function) - guard validateAllTextField(), - let username = viewModel.username.value, - let email = viewModel.email.value, - let password = viewModel.password.value else { - return - } - - guard !viewModel.isRegistering.value else { return } - viewModel.isRegistering.value = true - - if let rules = viewModel.instance.rules, !rules.isEmpty { - let mastodonServerRulesViewModel = MastodonServerRulesViewModel( - context: context, - domain: viewModel.domain, - rules: rules - ) - coordinator.present(scene: .mastodonServerRules(viewModel: mastodonServerRulesViewModel), from: self, transition: .show) - return - } - - let query = Mastodon.API.Account.RegisterQuery( - reason: nil, - username: username, - email: email, - password: password, - agreement: true, // TODO: - locale: "en" // TODO: - ) - - 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 } - _ = response.value - // TODO: - let alertController = UIAlertController(title: "Success", message: "Regsiter request sent. Please check your email.\n(Auto sign in not implement yet.)", preferredStyle: .alert) - let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default) { [weak self] _ in - guard let self = self else { return } - self.navigationController?.popViewController(animated: true) - } - alertController.addAction(okAction) - self.coordinator.present(scene: .alertController(alertController: alertController), from: self, transition: .alertController(animated: true, completion: nil)) - } - .store(in: &disposeBag) - } -}