Signal-iOS/Signal/src/ViewControllers/UsernameViewController.swift
2019-08-14 21:44:14 -07:00

262 lines
9.6 KiB
Swift

//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
import Foundation
@objc
class UsernameViewController: OWSViewController {
private static let minimumUsernameLength = 4
private static let maximumUsernameLength: UInt = 26
private let usernameTextField = OWSTextField()
private var hasPendingChanges: Bool {
return pendingUsername != (OWSProfileManager.shared().localUsername() ?? "")
}
private var pendingUsername: String {
return usernameTextField.text?.stripped.lowercased() ?? ""
}
private var errorLabel = UILabel()
private var errorRow = UIView()
private enum ValidationState {
case valid
case tooShort
case invalidCharacters
case inUse
}
private var validationState: ValidationState = .valid {
didSet { updateValidationError() }
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = Theme.backgroundColor
title = NSLocalizedString("USERNAME_TITLE", comment: "The title for the username view.")
navigationItem.leftBarButtonItem = UIBarButtonItem(title: CommonStrings.cancelButton,
style: .plain,
target: self,
action: #selector(didTapCancel))
let stackView = UIStackView()
stackView.axis = .vertical
stackView.spacing = 0
view.addSubview(stackView)
stackView.autoPinWidthToSuperview()
stackView.autoPinTopToSuperviewMargin(withInset: 35)
let topLine = UIView()
let bottomLine = UIView()
for line in [topLine, bottomLine] {
stackView.addArrangedSubview(line)
line.backgroundColor = Theme.cellSeparatorColor
line.autoSetDimension(.height, toSize: CGHairlineWidth())
}
// Username
let usernameRow = UIView()
stackView.insertArrangedSubview(usernameRow, at: 1)
usernameRow.layoutMargins = UIEdgeInsets(top: 10, leading: 18, bottom: 10, trailing: 18)
let titleLabel = UILabel()
titleLabel.text = NSLocalizedString("USERNAME_FIELD", comment: "Label for the username field in the username view.")
titleLabel.textColor = Theme.primaryColor
titleLabel.font = UIFont.ows_dynamicTypeBodyClamped.ows_mediumWeight()
usernameRow.addSubview(titleLabel)
titleLabel.autoPinLeadingToSuperviewMargin()
titleLabel.autoPinHeightToSuperviewMargins()
usernameTextField.font = .ows_dynamicTypeBodyClamped
usernameTextField.textColor = Theme.primaryColor
usernameTextField.autocorrectionType = .no
usernameTextField.autocapitalizationType = .none
usernameTextField.placeholder = NSLocalizedString("USERNAME_PLACEHOLDER",
comment: "The placeholder for the username text entry in the username view.")
usernameTextField.text = OWSProfileManager.shared().localUsername()
usernameTextField.textAlignment = .right
usernameTextField.delegate = self
usernameTextField.returnKeyType = .done
usernameTextField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
usernameTextField.becomeFirstResponder()
usernameRow.addSubview(usernameTextField)
usernameTextField.autoPinLeading(toTrailingEdgeOf: titleLabel, offset: 10)
usernameTextField.autoPinTrailingToSuperviewMargin()
usernameTextField.autoVCenterInSuperview()
// Error
stackView.addArrangedSubview(errorRow)
errorRow.isHidden = true
errorLabel.textColor = .ows_red
errorLabel.textAlignment = .center
errorLabel.numberOfLines = 0
errorLabel.lineBreakMode = .byWordWrapping
errorLabel.font = .ows_dynamicTypeCaption1Clamped
errorRow.addSubview(errorLabel)
errorLabel.autoPinWidthToSuperview(withMargin: 18)
errorLabel.autoPinHeightToSuperview(withMargin: 5)
// Info
let infoRow = UIView()
stackView.addArrangedSubview(infoRow)
let infoLabel = UILabel()
infoLabel.textColor = Theme.secondaryColor
infoLabel.text = NSLocalizedString("USERNAME_DESCRIPTION", comment: "An explanation of how usernames work on the username view.")
infoLabel.font = .ows_dynamicTypeCaption1Clamped
infoLabel.numberOfLines = 0
infoLabel.lineBreakMode = .byWordWrapping
infoRow.addSubview(infoLabel)
infoLabel.autoPinWidthToSuperview(withMargin: 18)
infoLabel.autoPinHeightToSuperview(withMargin: 10)
}
func updateSaveButton() {
guard hasPendingChanges else {
navigationItem.rightBarButtonItem = nil
return
}
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .save, target: self, action: #selector(didTapSave))
}
func updateValidationError() {
switch validationState {
case .valid:
errorRow.isHidden = true
case .tooShort:
errorRow.isHidden = false
errorLabel.text = NSLocalizedString("USERNAME_TOO_SHORT_ERROR",
comment: "An error indicating that the supplied username is too short.")
case .invalidCharacters:
errorRow.isHidden = false
errorLabel.text = NSLocalizedString("USERNAME_INVALID_CHARACTERS_ERROR",
comment: "An error indicating that the supplied username contains disallowed characters.")
case .inUse:
errorRow.isHidden = false
let unavailableErrorFormat = NSLocalizedString("USERNAME_UNAVAIALBE_ERROR_FORMAT",
comment: "An error indicating that the supplied username is in use by another user. Embeds {{requested username}}.")
errorLabel.text = String(format: unavailableErrorFormat, pendingUsername)
}
}
func saveUsername() {
// If we're trying to save, but we have nothing to save, just dismiss immediately.
guard hasPendingChanges else { return usernameSavedOrCanceled() }
guard usernameIsValid() else { return }
var usernameToUse: String? = pendingUsername
if usernameToUse?.isEmpty == true {
usernameToUse = nil
}
ModalActivityIndicatorViewController.present(fromViewController: self, canCancel: false) { modalView in
let usernameRequest: TSRequest
if let usernameToUse = usernameToUse {
usernameRequest = OWSRequestFactory.usernameSetRequest(usernameToUse)
} else {
usernameRequest = OWSRequestFactory.usernameDeleteRequest()
}
SSKEnvironment.shared.networkManager.makePromise(request: usernameRequest).done { _ in
modalView.dismiss {
OWSProfileManager.shared().updateLocalUsername(usernameToUse)
self.usernameSavedOrCanceled()
}
}.catch { error in
if case .taskError(let task, _)? = error as? NetworkManagerError, task.statusCode() == 409 {
modalView.dismiss { self.validationState = .inUse }
return
}
owsFailDebug("Unexpected username update error \(error)")
modalView.dismiss {
OWSAlerts.showErrorAlert(message: NSLocalizedString("USERNAME_VIEW_ERROR_UPDATE_FAILED",
comment: "Error moessage shown when a username update fails."))
}
}
}
}
func usernameIsValid() -> Bool {
// We allow empty usernames, as this is how you delete your username
guard !pendingUsername.isEmpty else {
return true
}
guard pendingUsername.count >= UsernameViewController.minimumUsernameLength else {
validationState = .tooShort
return false
}
// Usernames only allow a-z, 0-9, and underscore
let validUsernameRegex = try! NSRegularExpression(pattern: "^[a-z0-9_]+$", options: [])
guard validUsernameRegex.hasMatch(input: pendingUsername) else {
validationState = .invalidCharacters
return false
}
return true
}
func usernameSavedOrCanceled() {
usernameTextField.resignFirstResponder()
dismiss(animated: true, completion: nil)
}
// MARK: -
@objc func didTapCancel() {
guard hasPendingChanges else {
return usernameSavedOrCanceled()
}
OWSAlerts.showPendingChangesAlert { [weak self] in self?.usernameSavedOrCanceled() }
}
@objc func didTapSave() {
saveUsername()
}
@objc func textFieldDidChange() {
// Clear validation errors when the user starts typing again.
validationState = .valid
updateSaveButton()
}
}
// MARK: -
extension UsernameViewController: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
usernameTextField.resignFirstResponder()
return false
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
return TextFieldHelper.textField(
textField,
shouldChangeCharactersInRange: range,
replacementString: string,
byteLimit: UsernameViewController.maximumUsernameLength
)
}
}