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
@@ -0,0 +1,163 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
public struct Country: Codable, Equatable {
public static func == (lhs: Country, rhs: Country) -> Bool {
return lhs.id == rhs.id && lhs.name == rhs.name && lhs.localizedName == rhs.localizedName && lhs.countryCodes == rhs.countryCodes && lhs.hidden == rhs.hidden
}
public struct CountryCode: Codable, Equatable {
public let code: String
public let prefixes: [String]
public let patterns: [String]
public init(code: String, prefixes: [String], patterns: [String]) {
self.code = code
self.prefixes = prefixes
self.patterns = patterns
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: StringCodingKey.self)
self.code = try container.decode(String.self, forKey: "c")
self.prefixes = try container.decode([String].self, forKey: "pfx")
self.patterns = try container.decode([String].self, forKey: "ptrn")
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: StringCodingKey.self)
try container.encode(self.code, forKey: "c")
try container.encode(self.prefixes, forKey: "pfx")
try container.encode(self.patterns, forKey: "ptrn")
}
}
public let id: String
public let name: String
public let localizedName: String?
public let countryCodes: [CountryCode]
public let hidden: Bool
public init(id: String, name: String, localizedName: String?, countryCodes: [CountryCode], hidden: Bool) {
self.id = id
self.name = name
self.localizedName = localizedName
self.countryCodes = countryCodes
self.hidden = hidden
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: StringCodingKey.self)
self.id = try container.decode(String.self, forKey: "c")
self.name = try container.decode(String.self, forKey: "n")
self.localizedName = try container.decodeIfPresent(String.self, forKey: "ln")
self.countryCodes = try container.decode([CountryCode].self, forKey: "cc")
self.hidden = try container.decode(Bool.self, forKey: "h")
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: StringCodingKey.self)
try container.encode(self.id, forKey: "c")
try container.encode(self.name, forKey: "n")
try container.encodeIfPresent(self.localizedName, forKey: "ln")
try container.encode(self.countryCodes, forKey: "cc")
try container.encode(self.hidden, forKey: "h")
}
}
public final class CountriesList: Codable, Equatable {
public let countries: [Country]
public let hash: Int32
public init(countries: [Country], hash: Int32) {
self.countries = countries
self.hash = hash
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: StringCodingKey.self)
self.countries = try container.decode([Country].self, forKey: "c")
self.hash = try container.decode(Int32.self, forKey: "h")
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: StringCodingKey.self)
try container.encode(self.countries, forKey: "c")
try container.encode(self.hash, forKey: "h")
}
public static func ==(lhs: CountriesList, rhs: CountriesList) -> Bool {
return lhs.countries == rhs.countries && lhs.hash == rhs.hash
}
}
func _internal_getCountriesList(accountManager: AccountManager<TelegramAccountManagerTypes>, network: Network, langCode: String?, forceUpdate: Bool = false) -> Signal<[Country], NoError> {
let fetch: ([Country]?, Int32?) -> Signal<[Country], NoError> = { current, hash in
return network.request(Api.functions.help.getCountriesList(langCode: langCode ?? "", hash: hash ?? 0))
|> retryRequest
|> mapToSignal { result -> Signal<[Country], NoError> in
switch result {
case let .countriesList(apiCountries, hash):
let result = apiCountries.compactMap { Country(apiCountry: $0) }
if result == current {
return .complete()
} else {
let _ = accountManager.transaction { transaction in
transaction.updateSharedData(SharedDataKeys.countriesList, { _ in
return PreferencesEntry(CountriesList(countries: result, hash: hash))
})
}.start()
return .single(result)
}
case .countriesListNotModified:
return .complete()
}
}
}
if forceUpdate {
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]?.get(CountriesList.self) {
return (countriesList.countries, countriesList.hash)
} else {
return ([], 0)
}
} |> mapToSignal { current, hash -> Signal<[Country], NoError> in
return .single(current)
|> then(fetch(current, hash))
}
}
}
extension Country.CountryCode {
init(apiCountryCode: Api.help.CountryCode) {
switch apiCountryCode {
case let .countryCode(_, countryCode, apiPrefixes, apiPatterns):
let prefixes: [String] = apiPrefixes.flatMap { $0 } ?? []
let patterns: [String] = apiPatterns.flatMap { $0 } ?? []
self.init(code: countryCode, prefixes: prefixes, patterns: patterns)
}
}
}
extension Country {
init(apiCountry: Api.help.Country) {
switch apiCountry {
case let .country(flags, iso2, defaultName, name, countryCodes):
self.init(id: iso2, name: defaultName, localizedName: name, countryCodes: countryCodes.map { Country.CountryCode(apiCountryCode: $0) }, hidden: (flags & 1 << 0) != 0)
}
}
}
@@ -0,0 +1,26 @@
import Foundation
import Postbox
import TelegramApi
extension LocalizationInfo {
init(apiLanguage: Api.LangPackLanguage) {
switch apiLanguage {
case let .langPackLanguage(flags, name, nativeName, langCode, baseLangCode, pluralCode, stringsCount, translatedCount, translationsUrl):
self.init(languageCode: langCode, baseLanguageCode: baseLangCode, customPluralizationCode: pluralCode, title: name, localizedTitle: nativeName, isOfficial: (flags & (1 << 0)) != 0, totalStringCount: stringsCount, translatedStringCount: translatedCount, platformUrl: translationsUrl)
}
}
}
public final class SuggestedLocalizationInfo {
public let languageCode: String
public let extractedEntries: [LocalizationEntry]
public let availableLocalizations: [LocalizationInfo]
init(languageCode: String, extractedEntries: [LocalizationEntry], availableLocalizations: [LocalizationInfo]) {
self.languageCode = languageCode
self.extractedEntries = extractedEntries
self.availableLocalizations = availableLocalizations
}
}
@@ -0,0 +1,66 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
func _internal_removeSavedLocalization(transaction: Transaction, languageCode: String) {
updateLocalizationListStateInteractively(transaction: transaction, { state in
var state = state
state.availableSavedLocalizations = state.availableSavedLocalizations.filter({ $0.languageCode != languageCode })
return state
})
}
func updateLocalizationListStateInteractively(postbox: Postbox, _ f: @escaping (LocalizationListState) -> LocalizationListState) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Void in
updateLocalizationListStateInteractively(transaction: transaction, f)
}
}
func updateLocalizationListStateInteractively(transaction: Transaction, _ f: @escaping (LocalizationListState) -> LocalizationListState) {
transaction.updatePreferencesEntry(key: PreferencesKeys.localizationListState, { current in
let previous = current?.get(LocalizationListState.self) ?? LocalizationListState.defaultSettings
var updated = f(previous)
var removeOfficialIndices: [Int] = []
var officialSet = Set<String>()
for i in 0 ..< updated.availableOfficialLocalizations.count {
if officialSet.contains(updated.availableOfficialLocalizations[i].languageCode) {
removeOfficialIndices.append(i)
} else {
officialSet.insert(updated.availableOfficialLocalizations[i].languageCode)
}
}
for i in removeOfficialIndices.reversed() {
updated.availableOfficialLocalizations.remove(at: i)
}
var removeSavedIndices: [Int] = []
var savedSet = Set<String>()
for i in 0 ..< updated.availableSavedLocalizations.count {
if savedSet.contains(updated.availableSavedLocalizations[i].languageCode) {
removeSavedIndices.append(i)
} else {
savedSet.insert(updated.availableSavedLocalizations[i].languageCode)
}
}
for i in removeSavedIndices.reversed() {
updated.availableSavedLocalizations.remove(at: i)
}
return PreferencesEntry(updated)
})
}
func _internal_synchronizedLocalizationListState(postbox: Postbox, network: Network) -> Signal<Never, NoError> {
return network.request(Api.functions.langpack.getLanguages(langPack: ""))
|> retryRequest
|> mapToSignal { languages -> Signal<Never, NoError> in
let infos: [LocalizationInfo] = languages.map(LocalizationInfo.init(apiLanguage:))
return postbox.transaction { transaction -> Void in
updateLocalizationListStateInteractively(transaction: transaction, { current in
var current = current
current.availableOfficialLocalizations = infos
return current
})
}
|> ignoreValues
}
}
@@ -0,0 +1,19 @@
import Postbox
import SwiftSignalKit
import MtProtoKit
import TelegramApi
public enum RequestLocalizationPreviewError {
case generic
}
func _internal_requestLocalizationPreview(network: Network, identifier: String) -> Signal<LocalizationInfo, RequestLocalizationPreviewError> {
return network.request(Api.functions.langpack.getLanguage(langPack: "", langCode: identifier))
|> mapError { _ -> RequestLocalizationPreviewError in
return .generic
}
|> map { language -> LocalizationInfo in
return LocalizationInfo(apiLanguage: language)
}
}
@@ -0,0 +1,164 @@
import Foundation
import Postbox
import TelegramApi
import SwiftSignalKit
func _internal_currentlySuggestedLocalization(network: Network, extractKeys: [String]) -> Signal<SuggestedLocalizationInfo?, NoError> {
return network.request(Api.functions.help.getConfig())
|> retryRequest
|> mapToSignal { result -> Signal<SuggestedLocalizationInfo?, NoError> in
switch result {
case let .config(_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, suggestedLangCode, _, _, _, _):
if let suggestedLangCode = suggestedLangCode {
return _internal_suggestedLocalizationInfo(network: network, languageCode: suggestedLangCode, extractKeys: extractKeys) |> map(Optional.init)
} else {
return .single(nil)
}
}
}
}
func _internal_suggestedLocalizationInfo(network: Network, languageCode: String, extractKeys: [String]) -> Signal<SuggestedLocalizationInfo, NoError> {
return combineLatest(network.request(Api.functions.langpack.getLanguages(langPack: "")), network.request(Api.functions.langpack.getStrings(langPack: "", langCode: languageCode, keys: extractKeys)))
|> retryRequest
|> map { languages, strings -> SuggestedLocalizationInfo in
var entries: [LocalizationEntry] = []
for string in strings {
switch string {
case let .langPackString(key, value):
entries.append(.string(key: key, value: value))
case let .langPackStringPluralized(_, key, zeroValue, oneValue, twoValue, fewValue, manyValue, otherValue):
entries.append(.pluralizedString(key: key, zero: zeroValue, one: oneValue, two: twoValue, few: fewValue, many: manyValue, other: otherValue))
case let .langPackStringDeleted(key):
entries.append(.string(key: key, value: ""))
}
}
let infos: [LocalizationInfo] = languages.map(LocalizationInfo.init(apiLanguage:))
return SuggestedLocalizationInfo(languageCode: languageCode, extractedEntries: entries, availableLocalizations: infos)
}
}
func _internal_availableLocalizations(postbox: Postbox, network: Network, allowCached: Bool) -> Signal<[LocalizationInfo], NoError> {
let cached: Signal<[LocalizationInfo], NoError>
if allowCached {
cached = postbox.transaction { transaction -> Signal<[LocalizationInfo], NoError> in
if let entry = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedAvailableLocalizations, key: ValueBoxKey(length: 0)))?.get(CachedLocalizationInfos.self) {
return .single(entry.list)
}
return .complete()
} |> switchToLatest
} else {
cached = .complete()
}
let remote = network.request(Api.functions.langpack.getLanguages(langPack: ""))
|> retryRequest
|> mapToSignal { languages -> Signal<[LocalizationInfo], NoError> in
let infos: [LocalizationInfo] = languages.map(LocalizationInfo.init(apiLanguage:))
return postbox.transaction { transaction -> [LocalizationInfo] in
if let entry = CodableEntry(CachedLocalizationInfos(list: infos)) {
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedAvailableLocalizations, key: ValueBoxKey(length: 0)), entry: entry)
}
return infos
}
}
return cached |> then(remote)
}
public enum DownloadLocalizationError {
case generic
}
func _internal_downloadLocalization(network: Network, languageCode: String) -> Signal<Localization, DownloadLocalizationError> {
return network.request(Api.functions.langpack.getLangPack(langPack: "", langCode: languageCode))
|> mapError { _ -> DownloadLocalizationError in
return .generic
}
|> map { result -> Localization in
let version: Int32
var entries: [LocalizationEntry] = []
switch result {
case let .langPackDifference(_, _, versionValue, strings):
version = versionValue
for string in strings {
switch string {
case let .langPackString(key, value):
entries.append(.string(key: key, value: value))
case let .langPackStringPluralized(_, key, zeroValue, oneValue, twoValue, fewValue, manyValue, otherValue):
entries.append(.pluralizedString(key: key, zero: zeroValue, one: oneValue, two: twoValue, few: fewValue, many: manyValue, other: otherValue))
case let .langPackStringDeleted(key):
entries.append(.string(key: key, value: ""))
}
}
}
return Localization(version: version, entries: entries)
}
}
public enum DownloadAndApplyLocalizationError {
case generic
}
func _internal_downloadAndApplyLocalization(accountManager: AccountManager<TelegramAccountManagerTypes>, postbox: Postbox, network: Network, languageCode: String) -> Signal<Void, DownloadAndApplyLocalizationError> {
return _internal_requestLocalizationPreview(network: network, identifier: languageCode)
|> mapError { _ -> DownloadAndApplyLocalizationError in
return .generic
}
|> mapToSignal { preview -> Signal<Void, DownloadAndApplyLocalizationError> in
var primaryAndSecondaryLocalizations: [Signal<Localization, DownloadLocalizationError>] = []
primaryAndSecondaryLocalizations.append(_internal_downloadLocalization(network: network, languageCode: preview.languageCode))
if let secondaryCode = preview.baseLanguageCode {
primaryAndSecondaryLocalizations.append(_internal_downloadLocalization(network: network, languageCode: secondaryCode))
}
return combineLatest(primaryAndSecondaryLocalizations)
|> mapError { _ -> DownloadAndApplyLocalizationError in
return .generic
}
|> mapToSignal { components -> Signal<Void, DownloadAndApplyLocalizationError> in
guard let primaryLocalization = components.first else {
return .fail(.generic)
}
var secondaryComponent: LocalizationComponent?
if let secondaryCode = preview.baseLanguageCode, components.count > 1 {
secondaryComponent = LocalizationComponent(languageCode: secondaryCode, localizedName: "", localization: components[1], customPluralizationCode: nil)
}
return accountManager.transaction { transaction -> Signal<Void, DownloadAndApplyLocalizationError> in
transaction.updateSharedData(SharedDataKeys.localizationSettings, { _ in
return PreferencesEntry(LocalizationSettings(primaryComponent: LocalizationComponent(languageCode: preview.languageCode, localizedName: preview.localizedTitle, localization: primaryLocalization, customPluralizationCode: preview.customPluralizationCode), secondaryComponent: secondaryComponent))
})
return postbox.transaction { transaction -> Signal<Void, DownloadAndApplyLocalizationError> in
updateLocalizationListStateInteractively(transaction: transaction, { state in
var state = state
for i in 0 ..< state.availableSavedLocalizations.count {
if state.availableSavedLocalizations[i].languageCode == preview.languageCode {
state.availableSavedLocalizations.remove(at: i)
break
}
}
state.availableSavedLocalizations.insert(preview, at: 0)
return state
})
network.context.updateApiEnvironment { current in
return current?.withUpdatedLangPackCode(preview.languageCode)
}
return network.request(Api.functions.help.test())
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .complete()
}
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
}
|> castError(DownloadAndApplyLocalizationError.self)
}
|> castError(DownloadAndApplyLocalizationError.self)
|> switchToLatest
}
|> castError(DownloadAndApplyLocalizationError.self)
|> switchToLatest
}
}
}
@@ -0,0 +1,19 @@
import Foundation
import Postbox
import SwiftSignalKit
func _internal_markSuggestedLocalizationAsSeenInteractively(postbox: Postbox, languageCode: String) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Void in
transaction.updatePreferencesEntry(key: PreferencesKeys.suggestedLocalization, { current in
if let current = current?.get(SuggestedLocalizationEntry.self) {
if current.languageCode == languageCode, !current.isSeen {
return PreferencesEntry(SuggestedLocalizationEntry(languageCode: languageCode, isSeen: true))
}
} else {
return PreferencesEntry(SuggestedLocalizationEntry(languageCode: languageCode, isSeen: true))
}
return current
})
}
}
@@ -0,0 +1,69 @@
import SwiftSignalKit
import Postbox
public extension TelegramEngine {
final class Localization {
private let account: Account
init(account: Account) {
self.account = account
}
public func getCountriesList(accountManager: AccountManager<TelegramAccountManagerTypes>, langCode: String?, forceUpdate: Bool = false) -> Signal<[Country], NoError> {
return _internal_getCountriesList(accountManager: accountManager, network: self.account.network, langCode: langCode, forceUpdate: forceUpdate)
}
public func markSuggestedLocalizationAsSeenInteractively(languageCode: String) -> Signal<Void, NoError> {
return _internal_markSuggestedLocalizationAsSeenInteractively(postbox: self.account.postbox, languageCode: languageCode)
}
public func synchronizedLocalizationListState() -> Signal<Never, NoError> {
return _internal_synchronizedLocalizationListState(postbox: self.account.postbox, network: self.account.network)
}
public func suggestedLocalizationInfo(languageCode: String, extractKeys: [String]) -> Signal<SuggestedLocalizationInfo, NoError> {
return _internal_suggestedLocalizationInfo(network: self.account.network, languageCode: languageCode, extractKeys: extractKeys)
}
public func requestLocalizationPreview(identifier: String) -> Signal<LocalizationInfo, RequestLocalizationPreviewError> {
return _internal_requestLocalizationPreview(network: self.account.network, identifier: identifier)
}
public func downloadAndApplyLocalization(accountManager: AccountManager<TelegramAccountManagerTypes>, languageCode: String) -> Signal<Void, DownloadAndApplyLocalizationError> {
return _internal_downloadAndApplyLocalization(accountManager: accountManager, postbox: self.account.postbox, network: self.account.network, languageCode: languageCode)
}
public func removeSavedLocalization(languageCode: String) -> Signal<Never, NoError> {
return self.account.postbox.transaction { transaction -> Void in
_internal_removeSavedLocalization(transaction: transaction, languageCode: languageCode)
}
|> ignoreValues
}
}
}
public extension TelegramEngineUnauthorized {
final class Localization {
private let account: UnauthorizedAccount
init(account: UnauthorizedAccount) {
self.account = account
}
public func getCountriesList(accountManager: AccountManager<TelegramAccountManagerTypes>, langCode: String?, forceUpdate: Bool = false) -> Signal<[Country], NoError> {
return _internal_getCountriesList(accountManager: accountManager, network: self.account.network, langCode: langCode, forceUpdate: forceUpdate)
}
public func markSuggestedLocalizationAsSeenInteractively(languageCode: String) -> Signal<Void, NoError> {
return _internal_markSuggestedLocalizationAsSeenInteractively(postbox: self.account.postbox, languageCode: languageCode)
}
public func currentlySuggestedLocalization(extractKeys: [String]) -> Signal<SuggestedLocalizationInfo?, NoError> {
return _internal_currentlySuggestedLocalization(network: self.account.network, extractKeys: extractKeys)
}
public func downloadAndApplyLocalization(accountManager: AccountManager<TelegramAccountManagerTypes>, languageCode: String) -> Signal<Void, DownloadAndApplyLocalizationError> {
return _internal_downloadAndApplyLocalization(accountManager: accountManager, postbox: self.account.postbox, network: self.account.network, languageCode: languageCode)
}
}
}