Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
+30
View File
@@ -0,0 +1,30 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "CountrySelectionUI",
module_name = "CountrySelectionUI",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/TelegramCore:TelegramCore",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/TelegramStringFormatting:TelegramStringFormatting",
"//submodules/SearchBarNode:SearchBarNode",
"//submodules/AppBundle:AppBundle",
"//submodules/ComponentFlow",
"//submodules/Components/BundleIconComponent",
"//submodules/TelegramUI/Components/EdgeEffect",
"//submodules/TelegramUI/Components/GlassBarButtonComponent",
"//submodules/TelegramUI/Components/SearchInputPanelComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,467 @@
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
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 code = Country.CountryCode(code: countryCode, prefixes: [], patterns: !pattern.isEmpty ? [pattern] : [])
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 {
currentLocation = maybeNameRange.upperBound
} else {
break
}
}
countryCodesByPrefix = countriesByPrefix
return result
}
private var countryCodes: [Country] = loadCountryCodes()
private var countryCodesByPrefix: [String: (Country, Country.CountryCode)] = [:]
public func loadServerCountryCodes(accountManager: AccountManager<TelegramAccountManagerTypes>, engine: TelegramEngineUnauthorized, completion: @escaping () -> Void) {
let _ = (engine.localization.getCountriesList(accountManager: accountManager, 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()
}
})
}
public func loadServerCountryCodes(accountManager: AccountManager<TelegramAccountManagerTypes>, engine: TelegramEngine, completion: @escaping () -> Void) {
let _ = (engine.localization.getCountriesList(accountManager: accountManager, 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()
}
})
}
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), 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) {
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)
}
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)]) {
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
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]
}
}
}
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 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), 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: self.theme.rootController.navigationBar.glassBarButtonBackgroundColor,
isDark: self.theme.overallDarkAppearance,
state: .generic,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: self.theme.rootController.navigationBar.glassBarButtonForegroundColor
)
)),
action: { [weak self] _ in
self?.cancelPressed()
}
))
)
let searchComponent: AnyComponentWithIdentity<Empty>?
if !self.controllerNode.isSearching {
searchComponent = AnyComponentWithIdentity(
id: "search",
component: AnyComponent(GlassBarButtonComponent(
size: barButtonSize,
backgroundColor: self.theme.rootController.navigationBar.glassBarButtonBackgroundColor,
isDark: self.theme.overallDarkAppearance,
state: .generic,
component: AnyComponentWithIdentity(id: "search", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Search",
tintColor: self.theme.rootController.navigationBar.glassBarButtonForegroundColor
)
)),
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)
}
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)
}
}
@@ -0,0 +1,492 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramCore
import TelegramPresentationData
import TelegramStringFormatting
import AppBundle
import ComponentFlow
import EdgeEffect
import SearchInputPanelComponent
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()
public 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<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
}
public 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.1.components(separatedBy: " ")
let abbrTwo = componentsTwo.compactMap { $0.first.flatMap { String($0) } }.reduce(into: String(), { $0.append(contentsOf: $1) }).replacingOccurrences(of: "&", with: "")
let phoneCodes = item.2.map { "\($0)" }.joined(separator: " ")
let string = "\(item.0.0) \((item.0.1)) \(item.1) \(abbrOne) \(abbrTwo) \(phoneCodes)"
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
var deactivateSearch: () -> Void = {}
private let theme: PresentationTheme
private let strings: PresentationStrings
private let displayCodes: Bool
private let glass: 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])]
private let topEdgeEffectView: EdgeEffectView
private var searchInput: ComponentView<Empty>?
var isSearching = true
private var validLayout: ContainerViewLayout?
init(theme: PresentationTheme, strings: PresentationStrings, displayCodes: Bool, glass: Bool, itemSelected: @escaping (((String, String), String, Int)) -> Void) {
self.theme = theme
self.strings = strings
self.displayCodes = displayCodes
self.glass = glass
self.itemSelected = itemSelected
self.needsSubtitle = strings.baseLanguageCode != "en"
self.tableView = UITableView(frame: CGRect(), style: glass ? .insetGrouped : .plain)
if #available(iOS 15.0, *) {
self.tableView.sectionHeaderTopPadding = 0.0
}
self.searchTableView = UITableView(frame: CGRect(), style: glass ? .insetGrouped : .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 }
self.topEdgeEffectView = EdgeEffectView()
super.init()
self.setViewBlock({
return UITracingLayerView()
})
if glass {
self.backgroundColor = theme.list.blocksBackgroundColor
self.tableView.backgroundColor = theme.list.blocksBackgroundColor
self.searchTableView.backgroundColor = theme.list.blocksBackgroundColor
} else {
self.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.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)
if glass {
self.view.addSubview(self.topEdgeEffectView)
}
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.validLayout = layout
self.tableView.contentInset = UIEdgeInsets(top: navigationBarHeight, left: 0.0, bottom: layout.intrinsicInsets.bottom, right: 0.0)
self.searchTableView.contentInset = UIEdgeInsets(top: navigationBarHeight, left: 0.0, bottom: layout.intrinsicInsets.bottom, right: 0.0)
transition.updateFrame(view: self.tableView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: layout.size.height)))
transition.updateFrame(view: self.searchTableView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: layout.size.height)))
if self.glass {
let edgeEffectHeight: CGFloat = 88.0
let topEdgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: edgeEffectHeight))
transition.updateFrame(view: self.topEdgeEffectView, frame: topEdgeEffectFrame)
self.topEdgeEffectView.update(content: .clear, blur: true, alpha: 1.0, rect: topEdgeEffectFrame, edge: .top, edgeSize: topEdgeEffectFrame.height, transition: ComponentTransition(transition))
}
if self.glass && self.isSearching {
let searchInput: ComponentView<Empty>
if let current = self.searchInput {
searchInput = current
} else {
searchInput = ComponentView()
self.searchInput = searchInput
}
let searchInputSize = searchInput.update(
transition: .immediate,
component: AnyComponent(
SearchInputPanelComponent(
theme: self.theme,
strings: self.strings,
metrics: layout.metrics,
safeInsets: layout.safeInsets,
updated: { [weak self] query in
guard let self else {
return
}
self.updateSearchQuery(query)
},
cancel: { [weak self] in
guard let self else {
return
}
self.deactivateSearch()
self.updateSearchQuery("")
}
)
),
environment: {},
containerSize: CGSize(width: layout.size.width, height: layout.size.height)
)
let bottomInset: CGFloat = layout.insets(options: .input).bottom
let searchInputFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - bottomInset - searchInputSize.height), size: searchInputSize)
if let searchInputView = searchInput.view as? SearchInputPanelComponent.View {
if searchInputView.superview == nil {
self.view.addSubview(searchInputView)
searchInputView.frame = CGRect(origin: CGPoint(x: searchInputFrame.minX, y: layout.size.height), size: searchInputFrame.size)
searchInputView.activateInput()
}
transition.updateFrame(view: searchInputView, frame: searchInputFrame)
}
} else if let searchInput = self.searchInput {
self.searchInput = nil
if let searchInputView = searchInput.view {
transition.updateFrame(view: searchInputView, frame: CGRect(origin: CGPoint(x: searchInputView.frame.minX, y: layout.size.height), size: searchInputView.frame.size), completion: { _ in
searchInputView.removeFromSuperview()
})
}
}
}
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)
}
}
@@ -0,0 +1,121 @@
import Foundation
import AppBundle
import TelegramStringFormatting
public func emojiFlagForISOCountryCode(_ countryCode: String) -> String {
if countryCode.count != 2 {
return ""
}
if countryCode == "FT" {
return "🏴‍☠️"
} else if countryCode == "XG" {
return "🛰️"
} else if countryCode == "XV" {
return "🌍"
} else if countryCode == "TS" {
return "🏳️"
}
if ["YL"].contains(countryCode) {
return ""
}
return flagEmoji(countryCode: countryCode)
}
private func loadCountriesInfo() -> [(Int, String, String)] {
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 endOfLine1 = "\r\n"
let endOfLine2 = "\n"
var array: [(Int, String, String)] = []
var currentLocation = data.startIndex
while true {
guard let codeRange = data.range(of: delimiter, options: [], range: currentLocation ..< data.endIndex, locale: nil) else {
break
}
guard let countryCode = Int(data[currentLocation ..< codeRange.lowerBound]) else {
break
}
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 countryName: String
let nameRange1 = data.range(of: endOfLine1, options: [], range: patternRange.upperBound ..< data.endIndex)
let nameRange2 = data.range(of: endOfLine2, options: [], range: patternRange.upperBound ..< data.endIndex)
var nameRange: Range<String.Index>?
if let nameRange1 = nameRange1, let nameRange2 = nameRange2 {
if nameRange1.lowerBound < nameRange2.lowerBound {
nameRange = nameRange1
} else {
nameRange = nameRange2
}
} else {
nameRange = nameRange1 ?? nameRange2
}
if let nameRange = nameRange {
countryName = String(data[patternRange.upperBound ..< nameRange.lowerBound])
currentLocation = nameRange.upperBound
} else {
countryName = String(data[patternRange.upperBound ..< data.index(data.endIndex, offsetBy: -1)])
}
array.append((countryCode, countryId, countryName))
if nameRange == nil {
break
}
}
return array
}
let phoneCountriesInfo = loadCountriesInfo()
public let countryCodeToIdAndName: [Int: (String, String)] = {
var dict: [Int: (String, String)] = [:]
for (code, id, name) in phoneCountriesInfo {
if dict[code] == nil {
dict[code] = (id, name)
}
}
return dict
}()
public struct CountryCodeAndId: Hashable {
let code: Int
let id: String
public init(code: Int, id: String) {
self.code = code
self.id = id
}
}
public let countryCodeAndIdToName: [CountryCodeAndId: String] = {
var dict: [CountryCodeAndId: String] = [:]
for (code, id, name) in phoneCountriesInfo {
dict[CountryCodeAndId(code: code, id: id)] = name
}
return dict
}()