312 lines
11 KiB
Swift
312 lines
11 KiB
Swift
//
|
|
// Copyright 2019 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
public import SignalServiceKit
|
|
|
|
public protocol FindByPhoneNumberDelegate: AnyObject {
|
|
func findByPhoneNumber(_ findByPhoneNumber: FindByPhoneNumberViewController,
|
|
didSelectAddress address: SignalServiceAddress)
|
|
}
|
|
|
|
public class FindByPhoneNumberViewController: OWSTableViewController2 {
|
|
weak var findByPhoneNumberDelegate: FindByPhoneNumberDelegate?
|
|
let buttonText: String?
|
|
let requiresRegisteredNumber: Bool
|
|
|
|
private var country: PhoneNumberCountry!
|
|
let countryCodeLabel = UILabel()
|
|
private lazy var nationalNumberTextField = OWSTextField(
|
|
keyboardType: .numberPad,
|
|
returnKeyType: .done,
|
|
autocorrectionType: .no,
|
|
delegate: self
|
|
)
|
|
let countryRowTitleLabel = UILabel()
|
|
let nationalNumberRowTitleLabel = UILabel()
|
|
|
|
public init(delegate: FindByPhoneNumberDelegate, buttonText: String?, requiresRegisteredNumber: Bool) {
|
|
self.findByPhoneNumberDelegate = delegate
|
|
self.buttonText = buttonText
|
|
self.requiresRegisteredNumber = requiresRegisteredNumber
|
|
super.init()
|
|
}
|
|
|
|
private var titleAttributes: [NSAttributedString.Key: Any] {
|
|
[
|
|
.font: UIFont.dynamicTypeBodyClamped.semibold(),
|
|
.foregroundColor: Theme.primaryTextColor,
|
|
]
|
|
}
|
|
|
|
private var countryTitle: NSAttributedString {
|
|
NSAttributedString(
|
|
string: OWSLocalizedString(
|
|
"REGISTRATION_DEFAULT_COUNTRY_NAME",
|
|
comment: "Label for the country code field"
|
|
),
|
|
attributes: titleAttributes
|
|
)
|
|
}
|
|
|
|
private var nationalNumberTitle: NSAttributedString {
|
|
NSAttributedString(
|
|
string: OWSLocalizedString(
|
|
"REGISTRATION_PHONENUMBER_BUTTON",
|
|
comment: "Label for the phone number textfield"
|
|
),
|
|
attributes: titleAttributes
|
|
)
|
|
}
|
|
|
|
private var phoneNumberPlaceholder: NSAttributedString {
|
|
NSAttributedString(
|
|
string: OWSLocalizedString(
|
|
"REGISTRATION_ENTERNUMBER_DEFAULT_TEXT",
|
|
comment: "Placeholder text for the phone number textfield"
|
|
),
|
|
attributes: [
|
|
.font: UIFont.dynamicTypeBodyClamped,
|
|
]
|
|
)
|
|
}
|
|
|
|
override public func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
populateDefaultCountryCode()
|
|
loadTableContents()
|
|
}
|
|
|
|
private func loadTableContents() {
|
|
let section = OWSTableSection()
|
|
|
|
let content = OWSTableContents(
|
|
title: OWSLocalizedString(
|
|
"NEW_NONCONTACT_CONVERSATION_VIEW_TITLE",
|
|
comment: "Title for the 'new non-contact conversation' view."
|
|
),
|
|
sections: [section]
|
|
)
|
|
|
|
let titleWidth: CGFloat = 138
|
|
let useInlineTitles: Bool = {
|
|
[countryTitle, nationalNumberTitle].allSatisfy { string in
|
|
string.size().width <= titleWidth
|
|
}
|
|
}()
|
|
|
|
let countryCell = OWSTableItem.newCell()
|
|
countryCell.accessoryType = .disclosureIndicator
|
|
|
|
let countryStack = UIStackView()
|
|
countryCell.addSubview(countryStack)
|
|
countryStack.autoPinEdgesToSuperviewMargins()
|
|
countryStack.axis = useInlineTitles ? .horizontal : .vertical
|
|
countryStack.spacing = useInlineTitles ? 10 : 0
|
|
|
|
countryRowTitleLabel.attributedText = countryTitle
|
|
countryRowTitleLabel.accessibilityIdentifier =
|
|
UIView.accessibilityIdentifier(in: self, name: "countryRowTitleLabel")
|
|
|
|
countryStack.addArrangedSubview(countryRowTitleLabel)
|
|
|
|
if useInlineTitles {
|
|
countryRowTitleLabel.autoSetDimension(.width, toSize: titleWidth)
|
|
}
|
|
|
|
countryCodeLabel.font = UIFont.dynamicTypeBodyClamped
|
|
countryCodeLabel.accessibilityIdentifier =
|
|
UIView.accessibilityIdentifier(in: self, name: "countryCodeLabel")
|
|
|
|
countryStack.addArrangedSubview(countryCodeLabel)
|
|
|
|
section.add(.init(customCellBlock: {
|
|
countryCell
|
|
}, actionBlock: { [weak self] in
|
|
self?.didTapCountryRow()
|
|
}))
|
|
|
|
// Phone Number row
|
|
|
|
let phoneNumberCell = OWSTableItem.newCell()
|
|
|
|
let phoneNumberStack = UIStackView()
|
|
phoneNumberCell.contentView.addSubview(phoneNumberStack)
|
|
phoneNumberStack.autoPinEdgesToSuperviewMargins()
|
|
phoneNumberStack.axis = useInlineTitles ? .horizontal : .vertical
|
|
phoneNumberStack.spacing = useInlineTitles ? 10 : 0
|
|
|
|
nationalNumberRowTitleLabel.attributedText = nationalNumberTitle
|
|
|
|
phoneNumberStack.addArrangedSubview(nationalNumberRowTitleLabel)
|
|
|
|
if useInlineTitles {
|
|
nationalNumberRowTitleLabel.autoSetDimension(.width, toSize: titleWidth)
|
|
}
|
|
|
|
nationalNumberTextField.becomeFirstResponder()
|
|
|
|
phoneNumberStack.addArrangedSubview(nationalNumberTextField)
|
|
|
|
section.add(.init(customCellBlock: { phoneNumberCell }))
|
|
|
|
self.contents = content
|
|
|
|
navigationItem.rightBarButtonItem = UIBarButtonItem(
|
|
title: CommonStrings.nextButton,
|
|
style: .done,
|
|
target: self,
|
|
action: #selector(tryToSelectPhoneNumber),
|
|
accessibilityIdentifier: UIView.accessibilityIdentifier(in: self, name: "button")
|
|
)
|
|
navigationItem.rightBarButtonItem?.isEnabled = false
|
|
|
|
applyTheme()
|
|
}
|
|
|
|
public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
|
super.traitCollectionDidChange(previousTraitCollection)
|
|
|
|
applyTheme()
|
|
}
|
|
|
|
public override func contentSizeCategoryDidChange() {
|
|
super.contentSizeCategoryDidChange()
|
|
loadTableContents()
|
|
}
|
|
|
|
public override func themeDidChange() {
|
|
super.themeDidChange()
|
|
applyTheme()
|
|
}
|
|
|
|
private func applyTheme() {
|
|
countryRowTitleLabel.attributedText = countryTitle
|
|
countryCodeLabel.textColor = .placeholderText
|
|
nationalNumberRowTitleLabel.attributedText = nationalNumberTitle
|
|
nationalNumberTextField.attributedPlaceholder = phoneNumberPlaceholder
|
|
nationalNumberTextField.textColor = Theme.primaryTextColor
|
|
}
|
|
|
|
func updateButtonState() {
|
|
navigationItem.rightBarButtonItem?.isEnabled = hasValidPhoneNumber()
|
|
}
|
|
|
|
func validPhoneNumber() -> String? {
|
|
guard let nationalNumber = nationalNumberTextField.text else {
|
|
return nil
|
|
}
|
|
let phoneNumberUtil = SSKEnvironment.shared.phoneNumberUtilRef
|
|
return phoneNumberUtil.parsePhoneNumber(
|
|
countryCode: country.countryCode,
|
|
nationalNumber: nationalNumber
|
|
)?.e164.nilIfEmpty
|
|
}
|
|
|
|
func hasValidPhoneNumber() -> Bool {
|
|
// 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 without
|
|
// forking libPhoneNumber. Instead, we consider it valid if we can convert
|
|
// it to a non-empty e164 (which is the same validation we use when the
|
|
// user submits the number).
|
|
return validPhoneNumber() != nil
|
|
}
|
|
|
|
@objc
|
|
private func tryToSelectPhoneNumber() {
|
|
guard let phoneNumber = validPhoneNumber() else {
|
|
return
|
|
}
|
|
|
|
nationalNumberTextField.resignFirstResponder()
|
|
|
|
if requiresRegisteredNumber {
|
|
ModalActivityIndicatorViewController.present(
|
|
fromViewController: self,
|
|
canCancel: true,
|
|
asyncBlock: { modal in
|
|
do {
|
|
let recipients = try await SSKEnvironment.shared.contactDiscoveryManagerRef.lookUp(phoneNumbers: [phoneNumber], mode: .oneOffUserRequest)
|
|
modal.dismissIfNotCanceled {
|
|
guard let recipient = recipients.first else {
|
|
return OWSActionSheets.showErrorAlert(
|
|
message: MessageSenderNoSuchSignalRecipientError().userErrorDescription,
|
|
dismissalDelegate: self
|
|
)
|
|
}
|
|
self.findByPhoneNumberDelegate?.findByPhoneNumber(self, didSelectAddress: recipient.address)
|
|
}
|
|
} catch {
|
|
modal.dismissIfNotCanceled {
|
|
OWSActionSheets.showErrorAlert(
|
|
message: error.userErrorDescription,
|
|
dismissalDelegate: self
|
|
)
|
|
}
|
|
}
|
|
}
|
|
)
|
|
} else {
|
|
findByPhoneNumberDelegate?.findByPhoneNumber(self, didSelectAddress: SignalServiceAddress(phoneNumber: phoneNumber))
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - SheetDismissalDelegate
|
|
|
|
extension FindByPhoneNumberViewController: SheetDismissalDelegate {
|
|
public func didDismissPresentedSheet() {
|
|
nationalNumberTextField.becomeFirstResponder()
|
|
}
|
|
}
|
|
|
|
// MARK: - Country
|
|
|
|
extension FindByPhoneNumberViewController: CountryCodeViewControllerDelegate {
|
|
public func countryCodeViewController(_ vc: CountryCodeViewController, didSelectCountry country: PhoneNumberCountry) {
|
|
updateCountry(country)
|
|
}
|
|
|
|
private func didTapCountryRow() {
|
|
let countryCodeController = CountryCodeViewController()
|
|
countryCodeController.countryCodeDelegate = self
|
|
presentFormSheet(OWSNavigationController(rootViewController: countryCodeController), animated: true)
|
|
}
|
|
|
|
private func populateDefaultCountryCode() {
|
|
let tsAccountManager = DependenciesBridge.shared.tsAccountManager
|
|
let phoneNumberUtil = SSKEnvironment.shared.phoneNumberUtilRef
|
|
let defaultCountry: PhoneNumberCountry
|
|
if
|
|
let localNumber = tsAccountManager.localIdentifiersWithMaybeSneakyTransaction?.phoneNumber,
|
|
let localCountry = PhoneNumberCountry.buildCountry(forCountryCode: phoneNumberUtil.preferredCountryCode(forLocalNumber: localNumber))
|
|
{
|
|
defaultCountry = localCountry
|
|
} else {
|
|
owsFailDebug("Couldn't determine local country.")
|
|
defaultCountry = .defaultValue
|
|
}
|
|
updateCountry(defaultCountry)
|
|
}
|
|
|
|
private func updateCountry(_ country: PhoneNumberCountry) {
|
|
self.country = country
|
|
let labelFormat = CurrentAppContext().isRTL ? "(%2$@) %1$@" : "%1$@ (%2$@)"
|
|
countryCodeLabel.text = String(format: labelFormat, country.plusPrefixedCallingCode, country.countryCode.localizedUppercase)
|
|
}
|
|
}
|
|
|
|
extension FindByPhoneNumberViewController: UITextFieldDelegate {
|
|
public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
|
TextFieldFormatting.phoneNumberTextField(textField, changeCharactersIn: range, replacementString: string, plusPrefixedCallingCode: country.plusPrefixedCallingCode)
|
|
updateButtonState()
|
|
return false
|
|
}
|
|
|
|
public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
|
tryToSelectPhoneNumber()
|
|
return false
|
|
}
|
|
}
|