Signal-iOS/SignalMessaging/ViewControllers/FindByPhoneNumberViewController.swift
2019-09-26 12:20:42 -07:00

268 lines
11 KiB
Swift

//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
import Foundation
@objc
public protocol FindByPhoneNumberDelegate: class {
func findByPhoneNumber(_ findByPhoneNumber: FindByPhoneNumberViewController,
didSelectAddress address: SignalServiceAddress)
}
@objc
public class FindByPhoneNumberViewController: OWSViewController {
weak var delegate: FindByPhoneNumberDelegate?
let buttonText: String?
let requiresRegisteredNumber: Bool
var callingCode: String = "+1"
let countryCodeLabel = UILabel()
let phoneNumberTextField = OWSTextField()
let exampleLabel = UILabel()
let button = OWSFlatButton()
let countryRowTitleLabel = UILabel()
let phoneNumberRowTitleLabel = UILabel()
@objc
init(delegate: FindByPhoneNumberDelegate, buttonText: String?, requiresRegisteredNumber: Bool) {
self.delegate = delegate
self.buttonText = buttonText
self.requiresRegisteredNumber = requiresRegisteredNumber
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func viewDidLoad() {
super.viewDidLoad()
title = NSLocalizedString("NEW_NONCONTACT_CONVERSATION_VIEW_TITLE",
comment: "Title for the 'new non-contact conversation' view.")
let stackView = UIStackView()
stackView.axis = .vertical
stackView.isLayoutMarginsRelativeArrangement = true
stackView.layoutMargins = UIEdgeInsets(top: 10, leading: 18, bottom: 10, trailing: 18)
stackView.spacing = 15
view.addSubview(stackView)
stackView.autoPinWidthToSuperviewMargins()
stackView.autoPin(toTopLayoutGuideOf: self, withInset: 0)
// Country Row
let countryRow = UIView.container()
countryRow.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTapCountryRow)))
stackView.addArrangedSubview(countryRow)
countryRowTitleLabel.text = NSLocalizedString("REGISTRATION_DEFAULT_COUNTRY_NAME", comment: "Label for the country code field")
countryRowTitleLabel.font = UIFont.ows_dynamicTypeBodyClamped.ows_semibold()
countryRow.addSubview(countryRowTitleLabel)
countryRowTitleLabel.autoPinLeadingToSuperviewMargin()
countryRowTitleLabel.autoPinHeightToSuperviewMargins()
countryCodeLabel.textColor = .ows_signalBlue
countryCodeLabel.font = UIFont.ows_dynamicTypeBodyClamped.ows_semibold()
countryCodeLabel.textAlignment = .right
countryRow.addSubview(countryCodeLabel)
countryCodeLabel.autoPinLeading(toTrailingEdgeOf: countryRowTitleLabel, offset: 10)
countryCodeLabel.autoPinTrailingToSuperviewMargin()
countryCodeLabel.autoVCenterInSuperview()
// Phone Number row
let phoneNumberRow = UIView.container()
stackView.addArrangedSubview(phoneNumberRow)
phoneNumberRowTitleLabel.text = NSLocalizedString("REGISTRATION_PHONENUMBER_BUTTON",
comment: "Label for the phone number textfield")
phoneNumberRowTitleLabel.font = UIFont.ows_dynamicTypeBodyClamped.ows_semibold()
phoneNumberRow.addSubview(phoneNumberRowTitleLabel)
phoneNumberRowTitleLabel.autoPinLeadingToSuperviewMargin()
phoneNumberRowTitleLabel.autoPinHeightToSuperviewMargins()
phoneNumberTextField.font = .ows_dynamicTypeBodyClamped
phoneNumberTextField.textColor = .ows_signalBlue
phoneNumberTextField.autocorrectionType = .no
phoneNumberTextField.autocapitalizationType = .none
phoneNumberTextField.placeholder = NSLocalizedString("REGISTRATION_ENTERNUMBER_DEFAULT_TEXT",
comment: "Placeholder text for the phone number textfield")
phoneNumberTextField.textAlignment = .right
phoneNumberTextField.delegate = self
phoneNumberTextField.returnKeyType = .done
phoneNumberTextField.becomeFirstResponder()
phoneNumberRow.addSubview(phoneNumberTextField)
phoneNumberTextField.autoPinLeading(toTrailingEdgeOf: phoneNumberRowTitleLabel, offset: 10)
phoneNumberTextField.autoPinTrailingToSuperviewMargin()
phoneNumberTextField.autoVCenterInSuperview()
// Example row
stackView.addArrangedSubview(exampleLabel)
exampleLabel.font = .ows_dynamicTypeFootnoteClamped
exampleLabel.textAlignment = .right
populateDefaultCountryCode()
// Button row
let buttonHeight: CGFloat = 47
let buttonTitle = buttonText ?? NSLocalizedString("NEW_NONCONTACT_CONVERSATION_VIEW_BUTTON",
comment: "A label for the 'add by phone number' button in the 'new non-contact conversation' view")
stackView.addArrangedSubview(button)
button.useDefaultCornerRadius()
button.autoSetDimension(.height, toSize: buttonHeight)
button.setTitle(title: buttonTitle, font: OWSFlatButton.fontForHeight(buttonHeight), titleColor: .white)
button.setBackgroundColors(upColor: .ows_signalBlue)
button.addTarget(target: self, selector: #selector(tryToSelectPhoneNumber))
button.setEnabled(false)
}
public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
view.backgroundColor = Theme.backgroundColor
countryRowTitleLabel.textColor = Theme.primaryTextColor
phoneNumberRowTitleLabel.textColor = Theme.primaryTextColor
exampleLabel.textColor = Theme.secondaryTextAndIconColor
}
func updateButtonState() {
button.setEnabled(hasValidPhoneNumber())
}
func possiblePhoneNumbers() -> [PhoneNumber] {
guard let localNumber = TSAccountManager.localNumber else {
owsFailDebug("local number unexpectedly nil")
return []
}
guard let phoneNumberText = phoneNumberTextField.text else { return [] }
let possiblePhoneNumber = callingCode + phoneNumberText
return PhoneNumber.tryParsePhoneNumbersFromsUserSpecifiedText(possiblePhoneNumber, clientPhoneNumber: localNumber)
}
func hasValidPhoneNumber() -> Bool {
let phoneNumbers = possiblePhoneNumbers()
guard phoneNumbers.count > 0 else {
return false
}
// It'd be nice to use [PhoneNumber isValid] but it always returns false for some countries
// (like afghanistan) and there doesn't seem to be a good way to determine beforehand
// which countries it can validate for without forking libPhoneNumber.
return !phoneNumbers[0].toE164().isEmpty
}
@objc func tryToSelectPhoneNumber() {
guard hasValidPhoneNumber() else { return }
let phoneNumbers = possiblePhoneNumbers()
guard phoneNumbers.count > 0 else { return owsFailDebug("unexpectedly found no numbers") }
// There should only be one phone number, since we're explicitly specifying
// a country code and therefore parsing a number in e164 format.
assert(phoneNumbers.count == 1)
phoneNumberTextField.resignFirstResponder()
if requiresRegisteredNumber {
ModalActivityIndicatorViewController.present(fromViewController: self, canCancel: true) { [weak self] modal in
ContactsUpdater.shared().lookupIdentifiers(phoneNumbers.map { $0.toE164() }, success: { recipients in
AssertIsOnMainThread()
guard !modal.wasCancelled else { return }
guard let self = self else { return }
modal.dismiss {
guard let recipient = recipients.first else {
return OWSAlerts.showErrorAlert(message: OWSErrorMakeNoSuchSignalRecipientError().localizedDescription)
}
self.delegate?.findByPhoneNumber(self, didSelectAddress: recipient.address)
}
}, failure: { error in
AssertIsOnMainThread()
guard !modal.wasCancelled else { return }
modal.dismiss {
OWSAlerts.showErrorAlert(message: error.localizedDescription)
}
})
}
} else {
delegate?.findByPhoneNumber(self, didSelectAddress: SignalServiceAddress(phoneNumber: phoneNumbers[0].toE164()))
}
}
}
// MARK: - Country
extension FindByPhoneNumberViewController: CountryCodeViewControllerDelegate {
public func countryCodeViewController(_ vc: CountryCodeViewController, didSelectCountryCode countryCode: String, countryName: String, callingCode: String) {
updateCountry(callingCode: callingCode, countryCode: countryCode)
}
@objc func didTapCountryRow() {
let countryCodeController = CountryCodeViewController()
countryCodeController.countryCodeDelegate = self
present(OWSNavigationController(rootViewController: countryCodeController), animated: true)
}
func populateDefaultCountryCode() {
guard let localNumber = TSAccountManager.localNumber else {
return owsFailDebug("Local number unexpectedly nil")
}
var callingCodeInt: Int?
var countryCode: String?
if let localE164 = PhoneNumber(fromE164: localNumber), let localCountryCode = localE164.getCountryCode()?.intValue {
callingCodeInt = localCountryCode
} else {
callingCodeInt = PhoneNumberUtil.sharedThreadLocal().nbPhoneNumberUtil.getCountryCode(
forRegion: PhoneNumber.defaultCountryCode()
)?.intValue
}
var callingCode: String?
if let callingCodeInt = callingCodeInt {
callingCode = COUNTRY_CODE_PREFIX + "\(callingCodeInt)"
countryCode = PhoneNumberUtil.sharedThreadLocal().probableCountryCode(forCallingCode: callingCode!)
}
updateCountry(callingCode: callingCode, countryCode: countryCode)
}
func updateCountry(callingCode: String?, countryCode: String?) {
guard let callingCode = callingCode, !callingCode.isEmpty, let countryCode = countryCode, !countryCode.isEmpty else {
return owsFailDebug("missing calling code for selected country")
}
self.callingCode = callingCode
let labelFormat = CurrentAppContext().isRTL ? "(%2$@) %1$@" : "%1$@ (%2$@)"
countryCodeLabel.text = String(format: labelFormat, callingCode, countryCode.localizedUppercase)
exampleLabel.text = ViewControllerUtils.examplePhoneNumber(forCountryCode: countryCode, callingCode: callingCode)
}
}
extension FindByPhoneNumberViewController: UITextFieldDelegate {
public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
ViewControllerUtils.phoneNumber(textField, shouldChangeCharactersIn: range, replacementString: string, callingCode: callingCode)
updateButtonState()
return false
}
public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
tryToSelectPhoneNumber()
return false
}
}