WinterGram/submodules/CountrySelectionUI/Sources/AuthorizationSequenceCountrySelectionController.swift

510 lines
19 KiB
Swift

import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import TelegramStringFormatting
import SearchBarNode
import AppBundle
import TelegramCore
import ComponentFlow
import BundleIconComponent
import GlassBarButtonComponent
import libphonenumber
private let countryPhoneNumberUtil = NBPhoneNumberUtil()
private func loadCountryCodes() -> [Country] {
guard let filePath = getAppBundle().path(forResource: "PhoneCountries", ofType: "txt") else {
return []
}
guard let stringData = try? Data(contentsOf: URL(fileURLWithPath: filePath)) else {
return []
}
guard let data = String(data: stringData, encoding: .utf8) else {
return []
}
let delimiter = ";"
let endOfLine = "\r\n"
var result: [Country] = []
var countriesByPrefix: [String: (Country, Country.CountryCode)] = [:]
var currentLocation = data.startIndex
let locale = Locale(identifier: "en-US")
while true {
guard let codeRange = data.range(of: delimiter, options: [], range: currentLocation ..< data.endIndex) else {
break
}
let countryCode = String(data[currentLocation ..< codeRange.lowerBound])
guard let idRange = data.range(of: delimiter, options: [], range: codeRange.upperBound ..< data.endIndex) else {
break
}
let countryId = String(data[codeRange.upperBound ..< idRange.lowerBound])
guard let patternRange = data.range(of: delimiter, options: [], range: idRange.upperBound ..< data.endIndex) else {
break
}
let pattern = String(data[idRange.upperBound ..< patternRange.lowerBound])
let maybeNameRange = data.range(of: endOfLine, options: [], range: patternRange.upperBound ..< data.endIndex)
let countryName = locale.localizedString(forIdentifier: countryId) ?? ""
if let _ = Int(countryCode) {
let prefixes: [String]
if countryCode == "7" {
switch countryId {
case "RU":
prefixes = ["3", "4", "8", "9"]
case "KZ":
prefixes = ["6", "7"]
default:
prefixes = []
}
} else if countryCode == "599" {
switch countryId {
case "BQ":
prefixes = ["3", "4", "7"]
case "CW":
prefixes = ["1", "6", "9"]
default:
prefixes = []
}
} else {
prefixes = []
}
let code = Country.CountryCode(code: countryCode, prefixes: prefixes, patterns: !pattern.isEmpty ? [pattern] : [])
let country = Country(id: countryId, name: countryName, localizedName: nil, countryCodes: [code], hidden: false)
result.append(country)
if !prefixes.isEmpty {
for prefix in prefixes {
countriesByPrefix["\(code.code)\(prefix)"] = (country, code)
}
} else if countriesByPrefix[code.code] == nil {
countriesByPrefix[code.code] = (country, code)
}
}
if let maybeNameRange = maybeNameRange {
currentLocation = maybeNameRange.upperBound
} else {
break
}
}
countryCodesByPrefix = countriesByPrefix
return result
}
private var countryCodesByPrefix: [String: (Country, Country.CountryCode)] = [:]
private var countryCodes: [Country] = loadCountryCodes()
private func updateCountryCodes(_ countries: [Country]) {
guard !countries.isEmpty else {
return
}
countryCodes = countries
var updatedCodesByPrefix: [String: (Country, Country.CountryCode)] = [:]
for country in countries {
for code in country.countryCodes {
if !code.prefixes.isEmpty {
for prefix in code.prefixes {
updatedCodesByPrefix["\(code.code)\(prefix)"] = (country, code)
}
} else {
updatedCodesByPrefix[code.code] = (country, code)
}
}
}
countryCodesByPrefix = updatedCodesByPrefix
}
public func loadServerCountryCodes(accountManager: AccountManager<TelegramAccountManagerTypes>, engine: TelegramEngineUnauthorized, completion: @escaping () -> Void) {
let _ = (engine.localization.getCountriesList(accountManager: accountManager, langCode: nil)
|> deliverOnMainQueue).start(next: { countries in
updateCountryCodes(countries)
Queue.mainQueue().async {
completion()
}
})
}
public func loadServerCountryCodes(accountManager: AccountManager<TelegramAccountManagerTypes>, engine: TelegramEngine, completion: @escaping () -> Void) {
let _ = (engine.localization.getCountriesList(accountManager: accountManager, langCode: nil)
|> deliverOnMainQueue).start(next: { countries in
updateCountryCodes(countries)
Queue.mainQueue().async {
completion()
}
})
}
private final class AuthorizationSequenceCountrySelectionNavigationContentNode: NavigationBarContentNode {
private let theme: PresentationTheme
private let strings: PresentationStrings
private let cancel: () -> Void
private let searchBar: SearchBarNode
private var queryUpdated: ((String) -> Void)?
init(theme: PresentationTheme, strings: PresentationStrings, cancel: @escaping () -> Void) {
self.theme = theme
self.strings = strings
self.cancel = cancel
self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: theme), presentationTheme: theme, strings: strings, fieldStyle: .modern)
let placeholderText = strings.Common_Search
let searchBarFont = Font.regular(17.0)
self.searchBar.placeholderString = NSAttributedString(string: placeholderText, font: searchBarFont, textColor: theme.rootController.navigationSearchBar.inputPlaceholderTextColor)
super.init()
self.addSubnode(self.searchBar)
self.searchBar.cancel = { [weak self] in
self?.cancel()
}
self.searchBar.textUpdated = { [weak self] query, _ in
self?.queryUpdated?(query)
}
}
func setQueryUpdated(_ f: @escaping (String) -> Void) {
self.queryUpdated = f
}
override var nominalHeight: CGFloat {
return 54.0
}
override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize {
let searchBarFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - self.nominalHeight), size: CGSize(width: size.width, height: 54.0))
self.searchBar.frame = searchBarFrame
self.searchBar.updateLayout(boundingSize: searchBarFrame.size, leftInset: leftInset, rightInset: rightInset, transition: transition)
return size
}
func activate() {
self.searchBar.activate()
}
func deactivate() {
self.searchBar.deactivate(clear: false)
}
}
private func removePlus(_ text: String?) -> String {
var result = ""
if let text = text {
for c in text {
if c != "+" {
result += String(c)
}
}
}
return result
}
public final class AuthorizationSequenceCountrySelectionController: ViewController {
static func countries() -> [Country] {
return countryCodes
}
public static func setupCountryCodes(countries: [Country], codesByPrefix: [String: (Country, Country.CountryCode)]) {
guard !countries.isEmpty else {
return
}
countryCodes = countries
countryCodesByPrefix = codesByPrefix
}
public static func lookupCountryNameById(_ id: String, strings: PresentationStrings) -> String? {
for country in countryCodes {
if id == country.id {
let locale = localeWithStrings(strings)
if let countryName = locale.localizedString(forRegionCode: id) {
return countryName
} else {
return nil
}
}
}
return nil
}
static func lookupCountryById(_ id: String) -> Country? {
return countryCodes.first { $0.id == id }
}
public static func lookupCountryIdByNumber(_ number: String, preferredCountries: [String: String]) -> (Country, Country.CountryCode)? {
let number = removePlus(number)
var results: [(Country, Country.CountryCode)]? = nil
var matchedPrefixLength = 0
if number.count == 1, let preferredCountryId = preferredCountries[number], let country = lookupCountryById(preferredCountryId), let code = country.countryCodes.first {
return (country, code)
}
for i in 0..<number.count {
let prefix = String(number.prefix(number.count - i))
if let country = countryCodesByPrefix[prefix] {
if var currentResults = results {
if let result = currentResults.first, result.1.code.count > country.1.code.count {
break
} else {
currentResults.append(country)
}
} else {
results = [country]
matchedPrefixLength = prefix.count
}
}
}
if let results = results {
if let firstResult = results.first,
matchedPrefixLength <= firstResult.1.code.count,
let parsedNumber = try? countryPhoneNumberUtil.parse("+\(number)", defaultRegion: nil),
let regionCode = countryPhoneNumberUtil.getRegionCode(for: parsedNumber),
let matchedCountry = countryCodes.first(where: { $0.id == regionCode }),
let matchedCode = matchedCountry.countryCodes.first(where: { number.hasPrefix($0.code) }) {
return (matchedCountry, matchedCode)
}
if !preferredCountries.isEmpty, let (_, code) = results.first {
if let preferredCountry = preferredCountries[code.code] {
for (country, code) in results {
if country.id == preferredCountry {
return (country, code)
}
}
}
}
return results.first
} else {
return nil
}
}
public static func lookupCountryIdByCode(_ code: Int) -> String? {
for country in countryCodes {
for countryCode in country.countryCodes {
if countryCode.code == "\(code)" {
return country.id
}
}
}
return nil
}
public static func lookupPatternByNumber(_ number: String, preferredCountries: [String: String]) -> String? {
let number = removePlus(number)
if let (_, code) = lookupCountryIdByNumber(number, preferredCountries: preferredCountries), !code.patterns.isEmpty {
var prefixes: [String: String] = [:]
for pattern in code.patterns {
let cleanPattern = pattern.replacingOccurrences(of: " ", with: "").replacingOccurrences(of: "X", with: "")
let cleanPrefix = "\(code.code)\(cleanPattern)"
prefixes[cleanPrefix] = pattern
}
for i in 0..<number.count {
let prefix = String(number.prefix(number.count - i))
if let pattern = prefixes[prefix] {
return pattern
}
}
return code.patterns.first
}
return nil
}
public static func defaultCountryCode() -> Int32 {
let countryId = (Locale.current as NSLocale).object(forKey: .countryCode) as? String
var countryCode: Int32 = 1
if let countryId = countryId {
let normalizedId = countryId.uppercased()
for (code, idAndName) in countryCodeToIdAndName {
if idAndName.0 == normalizedId {
countryCode = Int32(code)
break
}
}
}
return countryCode
}
private let theme: PresentationTheme
private let strings: PresentationStrings
private let displayCodes: Bool
private let glass: Bool
private var closeButtonNode: BarComponentHostNode?
private var searchButtonNode: BarComponentHostNode?
private var navigationContentNode: AuthorizationSequenceCountrySelectionNavigationContentNode?
private var controllerNode: AuthorizationSequenceCountrySelectionControllerNode {
return self.displayNode as! AuthorizationSequenceCountrySelectionControllerNode
}
public var completeWithCountryCode: ((Int, String) -> Void)?
public var dismissed: (() -> Void)?
public init(strings: PresentationStrings, theme: PresentationTheme, displayCodes: Bool = true, glass: Bool = false) {
self.theme = theme
self.strings = strings
self.displayCodes = displayCodes
self.glass = glass
super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: NavigationBarTheme(rootControllerTheme: theme, hideBackground: glass, hideSeparator: glass, style: glass ? .glass : .legacy), strings: NavigationBarStrings(presentationStrings: strings)))
self._hasGlassStyle = glass
self.navigationPresentation = .modal
self.statusBar.statusBarStyle = theme.rootController.statusBarStyle.style
if glass {
self.title = strings.Login_SelectCountry
} else {
let navigationContentNode = AuthorizationSequenceCountrySelectionNavigationContentNode(theme: theme, strings: strings, cancel: { [weak self] in
self?.dismissed?()
self?.dismiss()
})
self.navigationContentNode = navigationContentNode
navigationContentNode.setQueryUpdated { [weak self] query in
guard let strongSelf = self, strongSelf.isNodeLoaded else {
return
}
strongSelf.controllerNode.updateSearchQuery(query)
}
self.navigationBar?.setContentNode(navigationContentNode, animated: false)
}
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func loadDisplayNode() {
self.displayNode = AuthorizationSequenceCountrySelectionControllerNode(theme: self.theme, strings: self.strings, displayCodes: self.displayCodes, glass: self.glass, itemSelected: { [weak self] args in
let (_, countryId, code) = args
self?.completeWithCountryCode?(code, countryId)
self?.dismiss()
})
self.controllerNode.deactivateSearch = { [weak self] in
self?.controllerNode.isSearching = false
self?.requestLayout(transition: .animated(duration: 0.5, curve: .spring))
}
self.displayNodeDidLoad()
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
Queue.mainQueue().justDispatch {
self.navigationContentNode?.activate()
}
}
private func updateNavigationButtons() {
guard self.glass else {
return
}
let barButtonSize = CGSize(width: 40.0, height: 40.0)
let closeComponent: AnyComponentWithIdentity<Empty> = AnyComponentWithIdentity(
id: "close",
component: AnyComponent(GlassBarButtonComponent(
size: barButtonSize,
backgroundColor: nil,
isDark: self.theme.overallDarkAppearance,
state: .glass,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: self.theme.chat.inputPanel.panelControlColor
)
)),
action: { [weak self] _ in
self?.cancelPressed()
}
))
)
let searchComponent: AnyComponentWithIdentity<Empty>?
if !self.controllerNode.isSearching {
searchComponent = AnyComponentWithIdentity(
id: "search",
component: AnyComponent(GlassBarButtonComponent(
size: barButtonSize,
backgroundColor: nil,
isDark: self.theme.overallDarkAppearance,
state: .glass,
component: AnyComponentWithIdentity(id: "search", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Search",
tintColor: self.theme.chat.inputPanel.panelControlColor
)
)),
action: { [weak self] _ in
self?.controllerNode.isSearching = true
self?.requestLayout(transition: .animated(duration: 0.5, curve: .spring))
}
))
)
} else {
searchComponent = nil
}
let closeButtonNode: BarComponentHostNode
if let current = self.closeButtonNode {
closeButtonNode = current
closeButtonNode.component = closeComponent
} else {
closeButtonNode = BarComponentHostNode(component: closeComponent, size: barButtonSize)
self.closeButtonNode = closeButtonNode
self.navigationItem.leftBarButtonItem = UIBarButtonItem(customDisplayNode: closeButtonNode)
}
if !self.glass {
let searchButtonNode: BarComponentHostNode
if let current = self.searchButtonNode {
searchButtonNode = current
searchButtonNode.component = searchComponent
} else {
searchButtonNode = BarComponentHostNode(component: searchComponent, size: barButtonSize)
self.searchButtonNode = searchButtonNode
self.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: searchButtonNode)
}
}
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.updateNavigationButtons()
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
}
private func cancelPressed() {
self.dismissed?()
self.dismiss(completion: nil)
}
}