import Foundation import UIKit import AsyncDisplayKit import Display import TelegramCore import TelegramPresentationData import TelegramStringFormatting import AppBundle private func loadCountryCodes() -> [(String, Int)] { 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 = "\n" var result: [(String, Int)] = [] var currentLocation = data.startIndex 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 maybeNameRange = data.range(of: endOfLine, options: [], range: patternRange.upperBound ..< data.endIndex) if let countryCodeInt = Int(countryCode) { result.append((countryId, countryCodeInt)) } if let maybeNameRange = maybeNameRange { currentLocation = maybeNameRange.upperBound } else { break } } return result } private let countryCodes: [(String, Int)] = loadCountryCodes() func localizedCountryNamesAndCodes(strings: PresentationStrings) -> [((String, String), String, [Int])] { let locale = localeWithStrings(strings) var result: [((String, String), String, [Int])] = [] for country in AuthorizationSequenceCountrySelectionController.countries() { if country.hidden || country.id == "FT" { continue } if let englishCountryName = usEnglishLocale.localizedString(forRegionCode: country.id), let countryName = locale.localizedString(forRegionCode: country.id) { var codes: [Int] = [] for codeValue in country.countryCodes { if let code = Int(codeValue.code) { codes.append(code) } } result.append(((englishCountryName, countryName), country.id, codes)) } } return result } private func stringTokens(_ string: String) -> [Data] { let nsString = string.replacingOccurrences(of: ".", with: "").folding(options: .diacriticInsensitive, locale: .current).lowercased() as NSString let flag = UInt(kCFStringTokenizerUnitWord) let tokenizer = CFStringTokenizerCreate(kCFAllocatorDefault, nsString, CFRangeMake(0, nsString.length), flag, CFLocaleCopyCurrent()) var tokenType = CFStringTokenizerAdvanceToNextToken(tokenizer) var tokens: [Data] = [] var addedTokens = Set() while tokenType != [] { let currentTokenRange = CFStringTokenizerGetCurrentTokenRange(tokenizer) if currentTokenRange.location >= 0 && currentTokenRange.length != 0 { var token = Data(count: currentTokenRange.length * 2) token.withUnsafeMutableBytes { bytes -> Void in guard let baseAddress = bytes.baseAddress else { return } nsString.getCharacters(baseAddress.assumingMemoryBound(to: unichar.self), range: NSMakeRange(currentTokenRange.location, currentTokenRange.length)) } if !addedTokens.contains(token) { tokens.append(token) addedTokens.insert(token) } } tokenType = CFStringTokenizerAdvanceToNextToken(tokenizer) } return tokens } public func isPrefix(data: Data, to otherData: Data) -> Bool { if data.isEmpty { return true } else if data.count <= otherData.count { return data.withUnsafeBytes { bytes -> Bool in guard let bytesBaseAddress = bytes.baseAddress else { return false } return otherData.withUnsafeBytes { otherBytes -> Bool in guard let otherBytesBaseAddress = otherBytes.baseAddress else { return false } return memcmp(bytesBaseAddress, otherBytesBaseAddress, bytes.count) == 0 } } } else { return false } } private func matchStringTokens(_ tokens: [Data], with other: [Data]) -> Bool { if other.isEmpty { return false } else if other.count == 1 { let otherToken = other[0] for token in tokens { if isPrefix(data: otherToken, to: token) { return true } } } else { for otherToken in other { var found = false for token in tokens { if isPrefix(data: otherToken, to: token) { found = true break } } if !found { return false } } return true } return false } private func searchCountries(items: [((String, String), String, [Int])], query: String) -> [((String, String), String, Int)] { let queryTokens = stringTokens(query.lowercased()) var result: [((String, String), String, Int)] = [] for item in items { let componentsOne = item.0.0.components(separatedBy: " ") let abbrOne = componentsOne.compactMap { $0.first.flatMap { String($0) } }.reduce(into: String(), { $0.append(contentsOf: $1) }).replacingOccurrences(of: "&", with: "") let componentsTwo = item.0.0.components(separatedBy: " ") let abbrTwo = componentsTwo.compactMap { $0.first.flatMap { String($0) } }.reduce(into: String(), { $0.append(contentsOf: $1) }).replacingOccurrences(of: "&", with: "") let string = "\(item.0.0) \((item.0.1)) \(item.1) \(abbrOne) \(abbrTwo)" let tokens = stringTokens(string) if matchStringTokens(tokens, with: queryTokens) { for code in item.2 { result.append((item.0, item.1, code)) } } } return result } final class AuthorizationSequenceCountrySelectionControllerNode: ASDisplayNode, UITableViewDelegate, UITableViewDataSource { let itemSelected: (((String, String), String, Int)) -> Void private let theme: PresentationTheme private let strings: PresentationStrings private let displayCodes: Bool private let needsSubtitle: Bool private let tableView: UITableView private let searchTableView: UITableView private let sections: [(String, [((String, String), String, Int)])] private let sectionTitles: [String] private var searchResults: [((String, String), String, Int)] = [] private let countryNamesAndCodes: [((String, String), String, [Int])] init(theme: PresentationTheme, strings: PresentationStrings, displayCodes: Bool, itemSelected: @escaping (((String, String), String, Int)) -> Void) { self.theme = theme self.strings = strings self.displayCodes = displayCodes self.itemSelected = itemSelected self.needsSubtitle = strings.baseLanguageCode != "en" self.tableView = UITableView(frame: CGRect(), style: .plain) if #available(iOS 15.0, *) { self.tableView.sectionHeaderTopPadding = 0.0 } self.searchTableView = UITableView(frame: CGRect(), style: .plain) self.searchTableView.isHidden = true if #available(iOS 11.0, *) { self.tableView.contentInsetAdjustmentBehavior = .never self.searchTableView.contentInsetAdjustmentBehavior = .never } let countryNamesAndCodes = localizedCountryNamesAndCodes(strings: strings) self.countryNamesAndCodes = countryNamesAndCodes var sections: [(String, [((String, String), String, Int)])] = [] for (names, id, codes) in countryNamesAndCodes.sorted(by: { lhs, rhs in return lhs.0.1 < rhs.0.1 }) { let title = String(names.1[names.1.startIndex ..< names.1.index(after: names.1.startIndex)]).uppercased() if sections.isEmpty || sections[sections.count - 1].0 != title { sections.append((title, [])) } for code in codes { sections[sections.count - 1].1.append((names, id, code)) } } self.sections = sections self.sectionTitles = sections.map { $0.0 } super.init() self.setViewBlock({ return UITracingLayerView() }) self.backgroundColor = theme.list.plainBackgroundColor self.tableView.backgroundColor = theme.list.plainBackgroundColor self.tableView.backgroundColor = self.theme.list.plainBackgroundColor self.tableView.separatorColor = self.theme.list.itemPlainSeparatorColor self.tableView.backgroundView = UIView() self.tableView.sectionIndexColor = self.theme.list.itemAccentColor self.searchTableView.backgroundColor = self.theme.list.plainBackgroundColor self.searchTableView.backgroundColor = self.theme.list.plainBackgroundColor self.searchTableView.separatorColor = self.theme.list.itemPlainSeparatorColor self.searchTableView.backgroundView = UIView() self.searchTableView.sectionIndexColor = self.theme.list.itemAccentColor self.tableView.delegate = self self.tableView.dataSource = self self.searchTableView.delegate = self self.searchTableView.dataSource = self self.view.addSubview(self.tableView) self.view.addSubview(self.searchTableView) } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { self.tableView.contentInset = UIEdgeInsets(top: 0.0, left: 0.0, bottom: layout.intrinsicInsets.bottom, right: 0.0) self.searchTableView.contentInset = UIEdgeInsets(top: 0.0, left: 0.0, bottom: layout.intrinsicInsets.bottom, right: 0.0) transition.updateFrame(view: self.tableView, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: layout.size.height - navigationBarHeight))) transition.updateFrame(view: self.searchTableView, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: layout.size.height - navigationBarHeight))) } func animateIn() { self.layer.animatePosition(from: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) } func animateOut(completion: @escaping () -> Void) { self.layer.animatePosition(from: self.layer.position, to: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { _ in completion() }) } func updateSearchQuery(_ query: String) { if query.isEmpty { self.searchResults = [] self.searchTableView.reloadData() self.searchTableView.isHidden = true } else { self.searchResults = searchCountries(items: self.countryNamesAndCodes, query: query) self.searchTableView.isHidden = false self.searchTableView.reloadData() } } func numberOfSections(in tableView: UITableView) -> Int { if tableView === self.tableView { return self.sections.count } else { return 1 } } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { if tableView === self.tableView { return self.sections[section].1.count } else { return self.searchResults.count } } func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { if tableView === self.tableView { return self.sections[section].0 } else { return nil } } func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) { (view as? UITableViewHeaderFooterView)?.tintColor = self.theme.chatList.sectionHeaderFillColor (view as? UITableViewHeaderFooterView)?.textLabel?.textColor = self.theme.chatList.sectionHeaderTextColor } func sectionIndexTitles(for tableView: UITableView) -> [String]? { if tableView === self.tableView { return self.sectionTitles } else { return nil } } func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int { if tableView === self.tableView { if index == 0 { return 0 } else { return max(0, index - 1) } } else { return 0 } } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell: UITableViewCell if let currentCell = tableView.dequeueReusableCell(withIdentifier: "CountryCell") { cell = currentCell } else { cell = UITableViewCell(style: self.needsSubtitle ? .subtitle : .default, reuseIdentifier: "CountryCell") let label = UILabel() label.font = Font.regular(17.0) cell.accessoryView = label cell.selectedBackgroundView = UIView() } var countryName: String var cleanCountryName: String let originalCountryName: String let code: String if tableView === self.tableView { cleanCountryName = self.sections[indexPath.section].1[indexPath.row].0.1 countryName = "\(emojiFlagForISOCountryCode(self.sections[indexPath.section].1[indexPath.row].1)) \(cleanCountryName)" originalCountryName = self.sections[indexPath.section].1[indexPath.row].0.0 code = "+\(self.sections[indexPath.section].1[indexPath.row].2)" } else { cleanCountryName = self.searchResults[indexPath.row].0.1 countryName = "\(emojiFlagForISOCountryCode(self.searchResults[indexPath.row].1)) \(cleanCountryName)" originalCountryName = self.searchResults[indexPath.row].0.0 code = "+\(self.searchResults[indexPath.row].2)" } cell.accessibilityLabel = cleanCountryName cell.accessibilityValue = code cell.textLabel?.text = countryName cell.detailTextLabel?.text = originalCountryName if self.displayCodes, let label = cell.accessoryView as? UILabel { label.text = code label.sizeToFit() label.textColor = self.theme.list.itemSecondaryTextColor } cell.textLabel?.textColor = self.theme.list.itemPrimaryTextColor cell.detailTextLabel?.textColor = self.theme.list.itemPrimaryTextColor cell.backgroundColor = self.theme.list.plainBackgroundColor cell.selectedBackgroundView?.backgroundColor = self.theme.list.itemHighlightedBackgroundColor return cell } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { if tableView === self.tableView { self.itemSelected(self.sections[indexPath.section].1[indexPath.row]) } else { self.itemSelected(self.searchResults[indexPath.row]) } } func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { self.view.endEditing(true) } }