Merge pull request #25 from tootsuite/feature/invite

feat: add support for inviteEnabled instance
This commit is contained in:
sxiaojian88 2021-02-26 15:42:46 +08:00 committed by GitHub
commit 56b010a47d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 94 additions and 176 deletions

View File

@ -1,95 +0,0 @@
{
"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"
},
"status": {
"user_boosted": "%s boosted",
"content_warning": "content warning",
"show_post": "Show Post"
},
"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.",
"Button": {
"Category": {
"All": "All"
},
"SeeLess": "See Less",
"SeeMore": "See More"
},
"Label": {
"Language": "LANGUAGE",
"Users": "USERS",
"Category": "CATEGORY"
},
"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"
}
}
}

View File

@ -1,46 +0,0 @@
"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.Status.ContentWarning" = "content warning";
"Common.Controls.Status.ShowPost" = "Show Post";
"Common.Controls.Status.UserBoosted" = "%@ boosted";
"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.Button.Category.All" = "All";
"Scene.ServerPicker.Button.Seeless" = "See Less";
"Scene.ServerPicker.Button.Seemore" = "See More";
"Scene.ServerPicker.Input.Placeholder" = "Find a server or join your own...";
"Scene.ServerPicker.Label.Category" = "CATEGORY";
"Scene.ServerPicker.Label.Language" = "LANGUAGE";
"Scene.ServerPicker.Label.Users" = "USERS";
"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.";

View File

@ -84,6 +84,9 @@
"placeholder": "password", "placeholder": "password",
"prompt": "Your password needs at least:", "prompt": "Your password needs at least:",
"prompt_eight_characters": "Eight characters" "prompt_eight_characters": "Eight characters"
},
"invite": {
"registration_user_invite_request": "Why do you want to join?"
} }
}, },
"success": "Success", "success": "Success",

View File

@ -141,6 +141,10 @@ internal enum L10n {
/// email /// email
internal static let placeholder = L10n.tr("Localizable", "Scene.Register.Input.Email.Placeholder") internal static let placeholder = L10n.tr("Localizable", "Scene.Register.Input.Email.Placeholder")
} }
internal enum Invite {
/// Why do you want to join?
internal static let registrationUserInviteRequest = L10n.tr("Localizable", "Scene.Register.Input.Invite.RegistrationUserInviteRequest")
}
internal enum Password { internal enum Password {
/// password /// password
internal static let placeholder = L10n.tr("Localizable", "Scene.Register.Input.Password.Placeholder") internal static let placeholder = L10n.tr("Localizable", "Scene.Register.Input.Password.Placeholder")

View File

@ -40,6 +40,7 @@ tap the link to confirm your account.";
"Scene.Register.CheckEmail" = "Regsiter request sent. Please check your email."; "Scene.Register.CheckEmail" = "Regsiter request sent. Please check your email.";
"Scene.Register.Input.DisplayName.Placeholder" = "display name"; "Scene.Register.Input.DisplayName.Placeholder" = "display name";
"Scene.Register.Input.Email.Placeholder" = "email"; "Scene.Register.Input.Email.Placeholder" = "email";
"Scene.Register.Input.Invite.RegistrationUserInviteRequest" = "Why do you want to join?";
"Scene.Register.Input.Password.Placeholder" = "password"; "Scene.Register.Input.Password.Placeholder" = "password";
"Scene.Register.Input.Password.Prompt" = "Your password needs at least:"; "Scene.Register.Input.Password.Prompt" = "Your password needs at least:";
"Scene.Register.Input.Password.PromptEightCharacters" = "Eight characters"; "Scene.Register.Input.Password.PromptEightCharacters" = "Eight characters";

View File

@ -97,7 +97,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
textField.autocapitalizationType = .none textField.autocapitalizationType = .none
textField.autocorrectionType = .no textField.autocorrectionType = .no
textField.backgroundColor = .white textField.backgroundColor = .white
textField.textColor = .black textField.textColor = Asset.Colors.Label.secondary.color
textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.Username.placeholder, textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.Username.placeholder,
attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.lightSecondaryText.color, attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.lightSecondaryText.color,
NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .headline)]) NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .headline)])
@ -118,7 +118,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
textField.autocapitalizationType = .none textField.autocapitalizationType = .none
textField.autocorrectionType = .no textField.autocorrectionType = .no
textField.backgroundColor = .white textField.backgroundColor = .white
textField.textColor = .black textField.textColor = Asset.Colors.Label.secondary.color
textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.DisplayName.placeholder, textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.DisplayName.placeholder,
attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.lightSecondaryText.color, attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.lightSecondaryText.color,
NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .headline)]) NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .headline)])
@ -135,7 +135,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
textField.autocorrectionType = .no textField.autocorrectionType = .no
textField.keyboardType = .emailAddress textField.keyboardType = .emailAddress
textField.backgroundColor = .white textField.backgroundColor = .white
textField.textColor = .black textField.textColor = Asset.Colors.Label.secondary.color
textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.Email.placeholder, textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.Email.placeholder,
attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.lightSecondaryText.color, attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.lightSecondaryText.color,
NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .headline)]) NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .headline)])
@ -159,7 +159,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
textField.keyboardType = .asciiCapable textField.keyboardType = .asciiCapable
textField.isSecureTextEntry = true textField.isSecureTextEntry = true
textField.backgroundColor = .white textField.backgroundColor = .white
textField.textColor = .black textField.textColor = Asset.Colors.Label.secondary.color
textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.Password.placeholder, textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.Password.placeholder,
attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.lightSecondaryText.color, attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.lightSecondaryText.color,
NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .headline)]) NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .headline)])
@ -170,6 +170,22 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
return textField return textField
}() }()
lazy var inviteTextField: UITextField = {
let textField = UITextField()
textField.autocapitalizationType = .none
textField.autocorrectionType = .no
textField.backgroundColor = .white
textField.textColor = Asset.Colors.Label.secondary.color
textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.Invite.registrationUserInviteRequest,
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 signUpButton: UIButton = {
let button = UIButton(type: .system) let button = UIButton(type: .system)
button.titleLabel?.font = .preferredFont(forTextStyle: .headline) button.titleLabel?.font = .preferredFont(forTextStyle: .headline)
@ -226,7 +242,9 @@ extension MastodonRegisterViewController {
stackView.addArrangedSubview(emailTextField) stackView.addArrangedSubview(emailTextField)
stackView.addArrangedSubview(passwordTextField) stackView.addArrangedSubview(passwordTextField)
stackView.addArrangedSubview(passwordCheckLabel) stackView.addArrangedSubview(passwordCheckLabel)
if self.viewModel.approvalRequired {
stackView.addArrangedSubview(inviteTextField)
}
// scrollView // scrollView
view.addSubview(scrollView) view.addSubview(scrollView)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
@ -445,6 +463,31 @@ extension MastodonRegisterViewController {
} }
.store(in: &disposeBag) .store(in: &disposeBag)
if self.viewModel.approvalRequired {
inviteTextField.delegate = self
NSLayoutConstraint.activate([
inviteTextField.heightAnchor.constraint(equalToConstant: 50).priority(.defaultHigh)
])
viewModel.inviteValidateState
.receive(on: DispatchQueue.main)
.sink { [weak self] validateState in
guard let self = self else { return }
self.setTextFieldValidAppearance(self.inviteTextField, validateState: validateState)
}
.store(in: &disposeBag)
NotificationCenter.default
.publisher(for: UITextField.textDidChangeNotification, object: inviteTextField)
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
guard let self = self else { return }
self.viewModel.invite.value = self.inviteTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
}
.store(in: &disposeBag)
}
signUpButton.addTarget(self, action: #selector(MastodonRegisterViewController.signUpButtonPressed(_:)), for: .touchUpInside) signUpButton.addTarget(self, action: #selector(MastodonRegisterViewController.signUpButtonPressed(_:)), for: .touchUpInside)
} }
@ -457,25 +500,6 @@ extension MastodonRegisterViewController {
extension MastodonRegisterViewController: UITextFieldDelegate { extension MastodonRegisterViewController: UITextFieldDelegate {
// 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 textFieldDidBeginEditing(_ textField: UITextField) { func textFieldDidBeginEditing(_ textField: UITextField) {
let text = textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" let text = textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
@ -488,6 +512,8 @@ extension MastodonRegisterViewController: UITextFieldDelegate {
viewModel.email.value = text viewModel.email.value = text
case passwordTextField: case passwordTextField:
viewModel.password.value = text viewModel.password.value = text
case inviteTextField:
viewModel.invite.value = text
default: default:
break break
} }
@ -542,7 +568,7 @@ extension MastodonRegisterViewController {
} }
let query = Mastodon.API.Account.RegisterQuery( let query = Mastodon.API.Account.RegisterQuery(
reason: nil, reason: viewModel.invite.value,
username: username, username: username,
email: email, email: email,
password: password, password: password,

View File

@ -11,7 +11,6 @@ import MastodonSDK
import UIKit import UIKit
final class MastodonRegisterViewModel { final class MastodonRegisterViewModel {
var disposeBag = Set<AnyCancellable>() var disposeBag = Set<AnyCancellable>()
// input // input
@ -24,20 +23,30 @@ final class MastodonRegisterViewModel {
let displayName = CurrentValueSubject<String, Never>("") let displayName = CurrentValueSubject<String, Never>("")
let email = CurrentValueSubject<String, Never>("") let email = CurrentValueSubject<String, Never>("")
let password = CurrentValueSubject<String, Never>("") let password = CurrentValueSubject<String, Never>("")
let invite = CurrentValueSubject<String, Never>("")
let isUsernameValidateDalay = CurrentValueSubject<Bool, Never>(true) let isUsernameValidateDalay = CurrentValueSubject<Bool, Never>(true)
let isDisplayNameValidateDalay = CurrentValueSubject<Bool, Never>(true) let isDisplayNameValidateDalay = CurrentValueSubject<Bool, Never>(true)
let isEmailValidateDalay = CurrentValueSubject<Bool, Never>(true) let isEmailValidateDalay = CurrentValueSubject<Bool, Never>(true)
let isPasswordValidateDalay = CurrentValueSubject<Bool, Never>(true) let isPasswordValidateDalay = CurrentValueSubject<Bool, Never>(true)
let isInviteValidateDelay = CurrentValueSubject<Bool, Never>(true)
let isRegistering = CurrentValueSubject<Bool, Never>(false) let isRegistering = CurrentValueSubject<Bool, Never>(false)
// output // output
lazy var approvalRequired: Bool = {
if let approvalRequired = instance.approvalRequired {
return approvalRequired
}
return false
}()
let applicationAuthorization: Mastodon.API.OAuth.Authorization let applicationAuthorization: Mastodon.API.OAuth.Authorization
let usernameValidateState = CurrentValueSubject<ValidateState, Never>(.empty) let usernameValidateState = CurrentValueSubject<ValidateState, Never>(.empty)
let displayNameValidateState = CurrentValueSubject<ValidateState, Never>(.empty) let displayNameValidateState = CurrentValueSubject<ValidateState, Never>(.empty)
let emailValidateState = CurrentValueSubject<ValidateState, Never>(.empty) let emailValidateState = CurrentValueSubject<ValidateState, Never>(.empty)
let passwordValidateState = CurrentValueSubject<ValidateState, Never>(.empty) let passwordValidateState = CurrentValueSubject<ValidateState, Never>(.empty)
let inviteValidateState = CurrentValueSubject<ValidateState, Never>(.empty)
let isAllValid = CurrentValueSubject<Bool, Never>(false) let isAllValid = CurrentValueSubject<Bool, Never>(false)
@ -67,7 +76,7 @@ final class MastodonRegisterViewModel {
// 0-9 (isASCII && isNumber) // 0-9 (isASCII && isNumber)
// _ ("_") // _ ("_")
for char in username { for char in username {
guard char.isASCII, (char.isLetter || char.isNumber || char == "_") else { guard char.isASCII, char.isLetter || char.isNumber || char == "_" else {
isValid = false isValid = false
break break
} }
@ -97,18 +106,34 @@ final class MastodonRegisterViewModel {
} }
.assign(to: \.value, on: passwordValidateState) .assign(to: \.value, on: passwordValidateState)
.store(in: &disposeBag) .store(in: &disposeBag)
if approvalRequired {
Publishers.CombineLatest4( invite
.map { invite in
guard !invite.isEmpty else { return .empty }
return .valid
}
.assign(to: \.value, on: inviteValidateState)
.store(in: &disposeBag)
}
let publisherOne = Publishers.CombineLatest4(
usernameValidateState.eraseToAnyPublisher(), usernameValidateState.eraseToAnyPublisher(),
displayNameValidateState.eraseToAnyPublisher(), displayNameValidateState.eraseToAnyPublisher(),
emailValidateState.eraseToAnyPublisher(), emailValidateState.eraseToAnyPublisher(),
passwordValidateState.eraseToAnyPublisher() passwordValidateState.eraseToAnyPublisher()
).map {
$0.0 == .valid && $0.1 == .valid && $0.2 == .valid && $0.3 == .valid
}
Publishers.CombineLatest(
publisherOne,
approvalRequired ? inviteValidateState.map {$0 == .valid}.eraseToAnyPublisher() : Just(true).eraseToAnyPublisher()
) )
.map { $0.0 == .valid && $0.1 == .valid && $0.2 == .valid && $0.3 == .valid } .map {
return $0 && $1
}
.assign(to: \.value, on: isAllValid) .assign(to: \.value, on: isAllValid)
.store(in: &disposeBag) .store(in: &disposeBag)
} }
} }
extension MastodonRegisterViewModel { extension MastodonRegisterViewModel {

View File

@ -45,7 +45,7 @@ extension Mastodon.Entity {
case languages case languages
case registrations case registrations
case approvalRequired = "approval_required" case approvalRequired = "approval_required"
case invitesEnabled case invitesEnabled = "invites_enabled"
case urls case urls
case statistics case statistics

View File

@ -23,4 +23,4 @@ fi
#task4 clean temp file #task4 clean temp file
rm -rf ${SRCROOT}/Localization/StringsConvertor/output rm -rf ${SRCROOT}/Localization/StringsConvertor/output
rm -rf ${SRCROOT}/Localization/StringsConvertor/intput rm -rf ${SRCROOT}/Localization/StringsConvertor/input