mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
409 lines
16 KiB
Swift
409 lines
16 KiB
Swift
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))
|
|
} else {
|
|
assertionFailure()
|
|
}
|
|
}
|
|
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<Data>()
|
|
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)
|
|
}
|
|
}
|