diff --git a/submodules/CountrySelectionUI/Sources/AuthorizationSequenceCountrySelectionController.swift b/submodules/CountrySelectionUI/Sources/AuthorizationSequenceCountrySelectionController.swift index 79b65b718f..bc1e10b488 100644 --- a/submodules/CountrySelectionUI/Sources/AuthorizationSequenceCountrySelectionController.swift +++ b/submodules/CountrySelectionUI/Sources/AuthorizationSequenceCountrySelectionController.swift @@ -25,6 +25,7 @@ private func loadCountryCodes() -> [Country] { let endOfLine = "\n" var result: [Country] = [] + var countriesByPrefix: [String: (Country, Country.CountryCode)] = [:] var currentLocation = data.startIndex @@ -46,8 +47,11 @@ private func loadCountryCodes() -> [Country] { let maybeNameRange = data.range(of: endOfLine, options: [], range: idRange.upperBound ..< data.endIndex) let countryName = locale.localizedString(forIdentifier: countryId) ?? "" - if let countryCodeInt = Int(countryCode) { - result.append(Country(code: countryId, name: countryName, localizedName: nil, countryCodes: [Country.CountryCode(code: countryCode, prefixes: [], patterns: [])], hidden: false)) + if let _ = Int(countryCode) { + let code = Country.CountryCode(code: countryCode, prefixes: [], patterns: []) + let country = Country(id: countryId, name: countryName, localizedName: nil, countryCodes: [code], hidden: false) + result.append(country) + countriesByPrefix["\(code.code)"] = (country, code) } if let maybeNameRange = maybeNameRange { @@ -57,15 +61,35 @@ private func loadCountryCodes() -> [Country] { } } + countryCodesByPrefix = countriesByPrefix + return result } private var countryCodes: [Country] = loadCountryCodes() +private var countryCodesByPrefix: [String: (Country, Country.CountryCode)] = [:] -public func loadServerCountryCodes(accountManager: AccountManager, network: Network) { +public func loadServerCountryCodes(accountManager: AccountManager, network: Network, completion: @escaping () -> Void) { let _ = (getCountriesList(accountManager: accountManager, network: network, langCode: nil) |> deliverOnMainQueue).start(next: { countries in countryCodes = countries + + var countriesByPrefix: [String: (Country, Country.CountryCode)] = [:] + for country in countries { + for code in country.countryCodes { + if !code.prefixes.isEmpty { + for prefix in code.prefixes { + countriesByPrefix["\(code.code)\(prefix)"] = (country, code) + } + } else { + countriesByPrefix[code.code] = (country, code) + } + } + } + countryCodesByPrefix = countriesByPrefix + Queue.mainQueue().async { + completion() + } }) } @@ -129,9 +153,13 @@ private final class AuthorizationSequenceCountrySelectionNavigationContentNode: } public final class AuthorizationSequenceCountrySelectionController: ViewController { + static func countries() -> [Country] { + return countryCodes + } + public static func lookupCountryNameById(_ id: String, strings: PresentationStrings) -> String? { - for country in countryCodes { - if id == country.code { + for country in self.countries() { + if id == country.id { let locale = localeWithStrings(strings) if let countryName = locale.localizedString(forRegionCode: id) { return countryName @@ -142,25 +170,73 @@ public final class AuthorizationSequenceCountrySelectionController: ViewControll } 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)? { + var results: [(Country, Country.CountryCode)]? = nil + if number.count == 1, let preferredCountryId = preferredCountries[number], let country = lookupCountryById(preferredCountryId), let code = country.countryCodes.first { + return (country, code) + } + + for i in 0.. country.1.code.count { + break + } else { + currentResults.append(country) + } + } else { + results = [country] + } + } + } + if let results = results { + 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 country in self.countries() { for countryCode in country.countryCodes { if countryCode.code == "\(code)" { - return country.code + return country.id } } } return nil } - public static func lookupPatternByCode(_ code: Int) -> String? { - for country in countryCodes { - for countryCode in country.countryCodes { - if countryCode.code == "\(code)" { - return countryCode.patterns.first + public static func lookupPatternByNumber(_ number: String, preferredCountries: [String: String]) -> String? { + 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.. [(String, Int)] { private let countryCodes: [(String, Int)] = loadCountryCodes() -func localizedContryNamesAndCodes(strings: PresentationStrings) -> [((String, String), String, Int)] { +func localizedCountryNamesAndCodes(strings: PresentationStrings) -> [((String, String), String, Int)] { let locale = localeWithStrings(strings) var result: [((String, String), String, Int)] = [] - for (id, code) in countryCodes { - if let englishCountryName = usEnglishLocale.localizedString(forRegionCode: id), let countryName = locale.localizedString(forRegionCode: id) { - result.append(((englishCountryName, countryName), id, code)) + for country in AuthorizationSequenceCountrySelectionController.countries() { + if country.hidden { + continue + } + if let englishCountryName = usEnglishLocale.localizedString(forRegionCode: country.id), let countryName = locale.localizedString(forRegionCode: country.id), let codeValue = country.countryCodes.first?.code, let code = Int(codeValue) { + result.append(((englishCountryName, countryName), country.id, code)) } else { assertionFailure() } @@ -103,7 +106,7 @@ final class AuthorizationSequenceCountrySelectionControllerNode: ASDisplayNode, self.searchTableView.contentInsetAdjustmentBehavior = .never } - let countryNamesAndCodes = localizedContryNamesAndCodes(strings: strings) + let countryNamesAndCodes = localizedCountryNamesAndCodes(strings: strings) var sections: [(String, [((String, String), String, Int)])] = [] for (names, id, code) in countryNamesAndCodes.sorted(by: { lhs, rhs in @@ -116,8 +119,7 @@ final class AuthorizationSequenceCountrySelectionControllerNode: ASDisplayNode, sections[sections.count - 1].1.append((names, id, code)) } self.sections = sections - var sectionTitles = sections.map { $0.0 } - self.sectionTitles = sectionTitles + self.sectionTitles = sections.map { $0.0 } super.init() diff --git a/submodules/PhoneInputNode/Sources/PhoneInputNode.swift b/submodules/PhoneInputNode/Sources/PhoneInputNode.swift index f68af967e8..56c8d7fa58 100644 --- a/submodules/PhoneInputNode/Sources/PhoneInputNode.swift +++ b/submodules/PhoneInputNode/Sources/PhoneInputNode.swift @@ -78,6 +78,7 @@ private func cleanSuffix(_ text: String) -> String { extension String { func applyPatternOnNumbers(pattern: String, replacementCharacter: Character) -> String { + let pattern = pattern.replacingOccurrences( of: "[0-9]", with: "X", options: .regularExpression) var pureNumber = self.replacingOccurrences( of: "[^0-9]", with: "", options: .regularExpression) for index in 0 ..< pattern.count { guard index < pureNumber.count else { return pureNumber } @@ -175,8 +176,12 @@ public final class PhoneInputNode: ASDisplayNode, UITextFieldDelegate { private func updatePlaceholder() { if let mask = self.mask { let mutableMask = NSMutableAttributedString(attributedString: mask) - mutableMask.replaceCharacters(in: NSRange(location: 0, length: mask.string.count), with: mask.string.replacingOccurrences(of: "X", with: "-")) + mutableMask.replaceCharacters(in: NSRange(location: 0, length: mask.string.count), with: mask.string.replacingOccurrences(of: "X", with: "–")) + if let text = self.numberField.textField.text { + mutableMask.replaceCharacters(in: NSRange(location: 0, length: min(text.count, mask.string.count)), with: text) + } mutableMask.addAttribute(.foregroundColor, value: UIColor.clear, range: NSRange(location: 0, length: min(self.numberField.textField.text?.count ?? 0, mask.string.count))) + mutableMask.addAttribute(.kern, value: 1.6, range: NSRange(location: 0, length: mask.string.count)) self.placeholderNode.attributedText = mutableMask } else { self.placeholderNode.attributedText = NSAttributedString(string: "") @@ -212,7 +217,7 @@ public final class PhoneInputNode: ASDisplayNode, UITextFieldDelegate { } else { self.numberField.textField.keyboardType = .numberPad } - self.numberField.textField.defaultTextAttributes = [NSAttributedString.Key.font: font, NSAttributedString.Key.kern: 2.0] + self.numberField.textField.defaultTextAttributes = [NSAttributedString.Key.font: font, NSAttributedString.Key.kern: 1.6] super.init() self.addSubnode(self.countryCodeField) diff --git a/submodules/TelegramCore/Sources/AccountManager.swift b/submodules/TelegramCore/Sources/AccountManager.swift index 9877a921ae..d2a4eea491 100644 --- a/submodules/TelegramCore/Sources/AccountManager.swift +++ b/submodules/TelegramCore/Sources/AccountManager.swift @@ -165,6 +165,7 @@ private var declaredEncodables: Void = { declareEncodable(TelegramMediaImage.VideoRepresentation.self, f: { TelegramMediaImage.VideoRepresentation(decoder: $0) }) declareEncodable(Country.self, f: { Country(decoder: $0) }) declareEncodable(Country.CountryCode.self, f: { Country.CountryCode(decoder: $0) }) + declareEncodable(CountriesList.self, f: { CountriesList(decoder: $0) }) return }() diff --git a/submodules/TelegramCore/Sources/Countries.swift b/submodules/TelegramCore/Sources/Countries.swift index 15ebe870cb..6ba3de2611 100644 --- a/submodules/TelegramCore/Sources/Countries.swift +++ b/submodules/TelegramCore/Sources/Countries.swift @@ -7,7 +7,7 @@ import SyncCore public struct Country: PostboxCoding, Equatable { public static func == (lhs: Country, rhs: Country) -> Bool { - return lhs.code == rhs.code && lhs.name == rhs.name && lhs.localizedName == rhs.localizedName && lhs.countryCodes == rhs.countryCodes && lhs.hidden == rhs.hidden + return lhs.id == rhs.id && lhs.name == rhs.name && lhs.localizedName == rhs.localizedName && lhs.countryCodes == rhs.countryCodes && lhs.hidden == rhs.hidden } public struct CountryCode: PostboxCoding, Equatable { @@ -34,14 +34,14 @@ public struct Country: PostboxCoding, Equatable { } } - public let code: String + public let id: String public let name: String public let localizedName: String? public let countryCodes: [CountryCode] public let hidden: Bool - public init(code: String, name: String, localizedName: String?, countryCodes: [CountryCode], hidden: Bool) { - self.code = code + public init(id: String, name: String, localizedName: String?, countryCodes: [CountryCode], hidden: Bool) { + self.id = id self.name = name self.localizedName = localizedName self.countryCodes = countryCodes @@ -49,7 +49,7 @@ public struct Country: PostboxCoding, Equatable { } public init(decoder: PostboxDecoder) { - self.code = decoder.decodeStringForKey("c", orElse: "") + self.id = decoder.decodeStringForKey("c", orElse: "") self.name = decoder.decodeStringForKey("n", orElse: "") self.localizedName = decoder.decodeOptionalStringForKey("ln") self.countryCodes = decoder.decodeObjectArrayForKey("cc").map { $0 as! CountryCode } @@ -57,7 +57,7 @@ public struct Country: PostboxCoding, Equatable { } public func encode(_ encoder: PostboxEncoder) { - encoder.encodeString(self.code, forKey: "c") + encoder.encodeString(self.id, forKey: "c") encoder.encodeString(self.name, forKey: "n") if let localizedName = self.localizedName { encoder.encodeString(localizedName, forKey: "ln") @@ -130,6 +130,7 @@ public func getCountriesList(accountManager: AccountManager, network: Network, l return fetch(nil, nil) } else { return accountManager.sharedData(keys: [SharedDataKeys.countriesList]) + |> take(1) |> map { sharedData -> ([Country], Int32) in if let countriesList = sharedData.entries[SharedDataKeys.countriesList] as? CountriesList { return (countriesList.countries, countriesList.hash) @@ -158,7 +159,7 @@ extension Country { init(apiCountry: Api.help.Country) { switch apiCountry { case let .country(flags, iso2, defaultName, name, countryCodes): - self.init(code: iso2, name: defaultName, localizedName: name, countryCodes: countryCodes.map { Country.CountryCode(apiCountryCode: $0) }, hidden: (flags & 1 << 0) != 0) + self.init(id: iso2, name: defaultName, localizedName: name, countryCodes: countryCodes.map { Country.CountryCode(apiCountryCode: $0) }, hidden: (flags & 1 << 0) != 0) } } } diff --git a/submodules/TelegramUI/Sources/AuthorizationSequencePhoneEntryController.swift b/submodules/TelegramUI/Sources/AuthorizationSequencePhoneEntryController.swift index 1ff91532a6..9ad2b45410 100644 --- a/submodules/TelegramUI/Sources/AuthorizationSequencePhoneEntryController.swift +++ b/submodules/TelegramUI/Sources/AuthorizationSequencePhoneEntryController.swift @@ -57,9 +57,7 @@ final class AuthorizationSequencePhoneEntryController: ViewController { self.presentationData = presentationData self.openUrl = openUrl self.back = back - - loadServerCountryCodes(accountManager: sharedContext.accountManager, network: account.network) - + super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: AuthorizationSequenceController.navigationBarTheme(presentationData.theme), strings: NavigationBarStrings(presentationStrings: presentationData.strings))) self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) @@ -140,6 +138,12 @@ final class AuthorizationSequencePhoneEntryController: ViewController { self.controllerNode.checkPhone = { [weak self] in self?.nextPressed() } + + loadServerCountryCodes(accountManager: sharedContext.accountManager, network: account.network, completion: { [weak self] in + if let strongSelf = self { + strongSelf.controllerNode.updateCountryCode() + } + }) } override func viewWillAppear(_ animated: Bool) { @@ -185,7 +189,7 @@ final class AuthorizationSequencePhoneEntryController: ViewController { self.loginWithNumber?(self.controllerNode.currentNumber, self.controllerNode.syncContacts) } } else { - hapticFeedback.error() + self.hapticFeedback.error() self.controllerNode.animateError() } } diff --git a/submodules/TelegramUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift b/submodules/TelegramUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift index 0f514b7657..0c261fbc8e 100644 --- a/submodules/TelegramUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift +++ b/submodules/TelegramUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift @@ -40,6 +40,8 @@ private final class PhoneAndCountryNode: ASDisplayNode { var selectCountryCode: (() -> Void)? var checkPhone: (() -> Void)? + var preferredCountryIdForCode: [String: String] = [:] + init(strings: PresentationStrings, theme: PresentationTheme) { self.strings = strings @@ -124,29 +126,71 @@ private final class PhoneAndCountryNode: ASDisplayNode { self.countryButton.contentEdgeInsets = UIEdgeInsets(top: 0.0, left: 15.0, bottom: 10.0, right: 0.0) self.countryButton.contentHorizontalAlignment = .left -// self.phoneInputNode.numberField.textField.attributedPlaceholder = NSAttributedString(string: strings.Login_PhonePlaceholder, font: Font.regular(20.0), textColor: theme.list.itemPlaceholderTextColor) - self.countryButton.addTarget(self, action: #selector(self.countryPressed), forControlEvents: .touchUpInside) - self.phoneInputNode.countryCodeUpdated = { [weak self] code, name in - let font = Font.with(size: 20.0, design: .monospace, traits: []) + func removePlus(_ text: String?) -> String { + var result = "" + if let text = text { + for c in text { + if c != "+" { + result += String(c) + } + } + } + return result + } + + let processNumberChange: (String) -> Bool = { [weak self] number in + guard let strongSelf = self else { + return false + } + let number = removePlus(number) + if let (country, _) = AuthorizationSequenceCountrySelectionController.lookupCountryIdByNumber(number, preferredCountries: strongSelf.preferredCountryIdForCode) { + let flagString = emojiFlagForISOCountryCode(country.id as NSString) + let localizedName: String = AuthorizationSequenceCountrySelectionController.lookupCountryNameById(country.id, strings: strongSelf.strings) ?? country.name + strongSelf.countryButton.setTitle("\(flagString) \(localizedName)", with: Font.regular(20.0), with: theme.list.itemPrimaryTextColor, for: []) + + let maskFont = Font.with(size: 20.0, design: .regular, traits: [.monospacedNumbers]) + if let mask = AuthorizationSequenceCountrySelectionController.lookupPatternByNumber(number, preferredCountries: strongSelf.preferredCountryIdForCode).flatMap({ NSAttributedString(string: $0, font: maskFont, textColor: theme.list.itemPlaceholderTextColor) }) { + strongSelf.phoneInputNode.numberField.textField.attributedPlaceholder = nil + strongSelf.phoneInputNode.mask = mask + } else { + strongSelf.phoneInputNode.mask = nil + strongSelf.phoneInputNode.numberField.textField.attributedPlaceholder = NSAttributedString(string: strings.Login_PhonePlaceholder, font: Font.regular(20.0), textColor: theme.list.itemPlaceholderTextColor) + } + return true + } else { + return false + } + } + + self.phoneInputNode.numberTextUpdated = { [weak self] number in if let strongSelf = self { - if let code = Int(code), let name = name, let countryName = countryCodeAndIdToName[CountryCodeAndId(code: code, id: name)] { + let _ = processNumberChange(strongSelf.phoneInputNode.number) + } + } + + self.phoneInputNode.countryCodeUpdated = { [weak self] code, name in + if let strongSelf = self { + if let name = name { + strongSelf.preferredCountryIdForCode[code] = name + } + + if processNumberChange(strongSelf.phoneInputNode.number) { + } else if let code = Int(code), let name = name, let countryName = countryCodeAndIdToName[CountryCodeAndId(code: code, id: name)] { let flagString = emojiFlagForISOCountryCode(name as NSString) let localizedName: String = AuthorizationSequenceCountrySelectionController.lookupCountryNameById(name, strings: strongSelf.strings) ?? countryName strongSelf.countryButton.setTitle("\(flagString) \(localizedName)", with: Font.regular(20.0), with: theme.list.itemPrimaryTextColor, for: []) - - strongSelf.phoneInputNode.mask = AuthorizationSequenceCountrySelectionController.lookupPatternByCode(code).flatMap { NSAttributedString(string: $0, font: font, textColor: theme.list.itemPlaceholderTextColor) } + strongSelf.phoneInputNode.numberField.textField.attributedPlaceholder = NSAttributedString(string: strings.Login_PhonePlaceholder, font: Font.regular(20.0), textColor: theme.list.itemPlaceholderTextColor) } else if let code = Int(code), let (countryId, countryName) = countryCodeToIdAndName[code] { let flagString = emojiFlagForISOCountryCode(countryId as NSString) let localizedName: String = AuthorizationSequenceCountrySelectionController.lookupCountryNameById(countryId, strings: strongSelf.strings) ?? countryName strongSelf.countryButton.setTitle("\(flagString) \(localizedName)", with: Font.regular(20.0), with: theme.list.itemPrimaryTextColor, for: []) - - strongSelf.phoneInputNode.mask = AuthorizationSequenceCountrySelectionController.lookupPatternByCode(code).flatMap { NSAttributedString(string: $0, font: font, textColor: theme.list.itemPlaceholderTextColor) } + strongSelf.phoneInputNode.numberField.textField.attributedPlaceholder = NSAttributedString(string: strings.Login_PhonePlaceholder, font: Font.regular(20.0), textColor: theme.list.itemPlaceholderTextColor) } else { strongSelf.countryButton.setTitle(strings.Login_SelectCountry_Title, with: Font.regular(20.0), with: theme.list.itemPlaceholderTextColor, for: []) - strongSelf.phoneInputNode.mask = nil + strongSelf.phoneInputNode.numberField.textField.attributedPlaceholder = NSAttributedString(string: strings.Login_PhonePlaceholder, font: Font.regular(20.0), textColor: theme.list.itemPlaceholderTextColor) } } } @@ -322,6 +366,10 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { self.tokenEventsDisposable.dispose() } + func updateCountryCode() { + self.phoneAndCountryNode.phoneInputNode.codeAndNumber = self.codeAndNumber + } + override func didLoad() { super.didLoad()