GLEGram 12.5 — Initial public release

Based on Swiftgram 12.5 (Telegram iOS 12.5).
All GLEGram features ported and organized in GLEGram/ folder.

Features: Ghost Mode, Saved Deleted Messages, Content Protection Bypass,
Font Replacement, Fake Profile, Chat Export, Plugin System, and more.

See CHANGELOG_12.5.md for full details.
This commit is contained in:
Leeksov
2026-04-06 09:48:12 +03:00
commit 4647310322
39685 changed files with 11052678 additions and 0 deletions
+28
View File
@@ -0,0 +1,28 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
sgdeps = ["//Swiftgram/SGSimpleSettings:SGSimpleSettings"]
swift_library(
name = "TelegramStringFormatting",
module_name = "TelegramStringFormatting",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = sgdeps + [
"//submodules/TelegramCore:TelegramCore",
"//submodules/Display:Display",
"//submodules/PlatformRestrictionMatching:PlatformRestrictionMatching",
"//submodules/LocalizedPeerData:LocalizedPeerData",
"//submodules/TextFormat:TextFormat",
"//submodules/Markdown:Markdown",
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/AppBundle:AppBundle",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,18 @@
import Foundation
import TelegramCore
public func hasBirthdayToday(cachedData: CachedUserData) -> Bool {
if let birthday = cachedData.birthday {
return hasBirthdayToday(birthday: birthday)
}
return false
}
public func hasBirthdayToday(birthday: TelegramBirthday) -> Bool {
let today = Calendar.current.dateComponents(Set([.day, .month]), from: Date())
if today.day == Int(birthday.day) && today.month == Int(birthday.month) {
return true
}
return false
}
@@ -0,0 +1,43 @@
import Foundation
import Contacts
import AddressBook
import TelegramPresentationData
public func localizedPhoneNumberLabel(label: String, strings: PresentationStrings) -> String {
if #available(iOSApplicationExtension 9.0, iOS 9.0, *) {
if label.isEmpty {
return strings.ContactInfo_PhoneLabelMain
} else if label == "X-iPhone" {
return "iPhone"
} else {
return CNLabeledValue<CNPhoneNumber>.localizedString(forLabel: label)
}
} else {
}
if label == "_$!<Mobile>!$_" {
return "mobile"
} else if label == "_$!<Home>!$_" {
return "home"
} else {
return label
}
}
public func localizedGenericContactFieldLabel(label: String, strings: PresentationStrings) -> String {
if #available(iOSApplicationExtension 9.0, iOS 9.0, *) {
if label.isEmpty {
return strings.ContactInfo_PhoneLabelMain
}
return CNLabeledValue<NSString>.localizedString(forLabel: label)
} else {
}
if label == "_$!<Mobile>!$_" {
return "mobile"
} else if label == "_$!<Home>!$_" {
return "home"
} else {
return label
}
}
@@ -0,0 +1,278 @@
import Foundation
import AppBundle
public final class CurrencyFormatterEntry {
public let symbol: String
public let thousandsSeparator: String
public let decimalSeparator: String
public let symbolOnLeft: Bool
public let spaceBetweenAmountAndSymbol: Bool
public let decimalDigits: Int
public init(symbol: String, thousandsSeparator: String, decimalSeparator: String, symbolOnLeft: Bool, spaceBetweenAmountAndSymbol: Bool, decimalDigits: Int) {
self.symbol = symbol
self.thousandsSeparator = thousandsSeparator
self.decimalSeparator = decimalSeparator
self.symbolOnLeft = symbolOnLeft
self.spaceBetweenAmountAndSymbol = spaceBetweenAmountAndSymbol
self.decimalDigits = decimalDigits
}
}
private func getCurrencyExp(currency: String) -> Int {
switch currency {
case "CLF":
return 4
case "BHD", "IQD", "JOD", "KWD", "LYD", "OMR", "TND":
return 3
case "BIF", "BYR", "CLP", "CVE", "DJF", "GNF", "ISK", "JPY", "KMF", "KRW", "MGA", "PYG", "RWF", "UGX", "UYI", "VND", "VUV", "XAF", "XOF", "XPF":
return 0
case "MRO":
return 1
default:
return 2
}
}
private func loadCurrencyFormatterEntries() -> [String: CurrencyFormatterEntry] {
guard let filePath = getAppBundle().path(forResource: "currencies", ofType: "json") else {
return [:]
}
guard let data = try? Data(contentsOf: URL(fileURLWithPath: filePath)) else {
return [:]
}
guard let object = try? JSONSerialization.jsonObject(with: data, options: []), let dict = object as? [String: AnyObject] else {
return [:]
}
var result: [String: CurrencyFormatterEntry] = [:]
for (code, contents) in dict {
if let contentsDict = contents as? [String: AnyObject] {
let entry = CurrencyFormatterEntry(
symbol: contentsDict["symbol"] as! String,
thousandsSeparator: contentsDict["thousandsSeparator"] as! String,
decimalSeparator: contentsDict["decimalSeparator"] as! String,
symbolOnLeft: (contentsDict["symbolOnLeft"] as! NSNumber).boolValue,
spaceBetweenAmountAndSymbol: (contentsDict["spaceBetweenAmountAndSymbol"] as! NSNumber).boolValue,
decimalDigits: getCurrencyExp(currency: code.uppercased())
)
result[code] = entry
result[code.lowercased()] = entry
}
}
let tonEntry = CurrencyFormatterEntry(
symbol: "TON",
thousandsSeparator: ".",
decimalSeparator: ",",
symbolOnLeft: true,
spaceBetweenAmountAndSymbol: false,
decimalDigits: 9
)
result["TON"] = tonEntry
result["ton"] = tonEntry
return result
}
private let currencyFormatterEntries = loadCurrencyFormatterEntries()
public func setupCurrencyNumberFormatter(currency: String) -> NumberFormatter {
guard let entry = currencyFormatterEntries[currency] ?? currencyFormatterEntries["USD"] else {
preconditionFailure()
}
var result = ""
if entry.symbolOnLeft {
result.append("¤")
if entry.spaceBetweenAmountAndSymbol {
result.append(" ")
}
}
result.append("#")
if entry.decimalDigits != 0 {
result.append(entry.decimalSeparator)
}
for _ in 0 ..< entry.decimalDigits {
result.append("#")
}
if entry.decimalDigits != 0 {
result.append("0")
}
if !entry.symbolOnLeft {
if entry.spaceBetweenAmountAndSymbol {
result.append(" ")
}
result.append("¤")
}
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .currency
numberFormatter.positiveFormat = result
numberFormatter.negativeFormat = "-\(result)"
numberFormatter.currencySymbol = ""
numberFormatter.currencyDecimalSeparator = entry.decimalSeparator
numberFormatter.currencyGroupingSeparator = entry.thousandsSeparator
numberFormatter.minimumFractionDigits = entry.decimalDigits
numberFormatter.maximumFractionDigits = entry.decimalDigits
numberFormatter.minimumIntegerDigits = 1
return numberFormatter
}
public func fractionalToCurrencyAmount(value: Double, currency: String) -> Int64? {
guard let entry = currencyFormatterEntries[currency] ?? currencyFormatterEntries["USD"] else {
return nil
}
var factor: Double = 1.0
for _ in 0 ..< entry.decimalDigits {
factor *= 10.0
}
if value > Double(Int64.max) / factor {
return nil
} else {
return Int64(value * factor)
}
}
public func currencyToFractionalAmount(value: Int64, currency: String) -> Double? {
guard let entry = currencyFormatterEntries[currency] ?? currencyFormatterEntries["USD"] else {
return nil
}
var factor: Double = 1.0
for _ in 0 ..< entry.decimalDigits {
factor *= 10.0
}
return Double(value) / factor
}
private func formatIntegerWithThousandsSeparator(_ number: Int64, separator: String) -> String {
if number == 0 {
return "0"
}
let numberString = String(number)
var result = ""
let digits = Array(numberString)
for (index, digit) in digits.enumerated() {
let remainingDigits = digits.count - index
if remainingDigits % 3 == 0 && index > 0 {
result.append(separator)
}
result.append(digit)
}
return result
}
public func formatCurrencyAmount(_ amount: Int64, currency: String) -> String {
if let entry = currencyFormatterEntries[currency] ?? currencyFormatterEntries["USD"] {
var result = ""
if amount < 0 {
result.append("-")
}
if entry.symbolOnLeft {
result.append(entry.symbol)
if entry.spaceBetweenAmountAndSymbol {
result.append(" ")
}
}
var integerPart = abs(amount)
var fractional: [Character] = []
for _ in 0 ..< entry.decimalDigits {
let part = integerPart % 10
integerPart /= 10
if let scalar = UnicodeScalar(UInt32(part + 48)) {
fractional.append(Character(scalar))
}
}
let integerString = formatIntegerWithThousandsSeparator(integerPart, separator: entry.thousandsSeparator)
result.append(integerString)
if !fractional.isEmpty {
result.append(entry.decimalSeparator)
for i in 0 ..< fractional.count {
result.append(fractional[fractional.count - i - 1])
}
}
if !entry.symbolOnLeft {
if entry.spaceBetweenAmountAndSymbol {
result.append(" ")
}
result.append(entry.symbol)
}
return result
} else {
assertionFailure()
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.currencyCode = currency
formatter.negativeFormat = "-¤#,##0.00"
return formatter.string(from: (Float(amount) * 0.01) as NSNumber) ?? ""
}
}
public func formatCurrencyAmountCustom(_ amount: Int64, currency: String, customFormat: CurrencyFormatterEntry? = nil) -> (String, String, Bool) {
if let entry = customFormat ?? currencyFormatterEntries[currency] ?? currencyFormatterEntries["USD"] {
var result = ""
if amount < 0 {
result.append("-")
}
/*if entry.symbolOnLeft {
result.append(entry.symbol)
if entry.spaceBetweenAmountAndSymbol {
result.append(" ")
}
}*/
var integerPart = abs(amount)
var fractional: [Character] = []
for _ in 0 ..< entry.decimalDigits {
let part = integerPart % 10
integerPart /= 10
if let scalar = UnicodeScalar(UInt32(part + 48)) {
fractional.append(Character(scalar))
}
}
result.append("\(integerPart)")
if !fractional.isEmpty {
result.append(entry.decimalSeparator)
}
for i in 0 ..< fractional.count {
result.append(fractional[fractional.count - i - 1])
}
/*if !entry.symbolOnLeft {
if entry.spaceBetweenAmountAndSymbol {
result.append(" ")
}
result.append(entry.symbol)
}*/
return (result, entry.symbol, entry.symbolOnLeft)
} else {
return ("", "", false)
}
}
public struct CurrencyFormat {
public var symbol: String
public var symbolOnLeft: Bool
public var decimalSeparator: String
public var decimalDigits: Int
public init?(currency: String) {
guard let entry = currencyFormatterEntries[currency] else {
return nil
}
self.symbol = entry.symbol
self.symbolOnLeft = entry.symbolOnLeft
self.decimalSeparator = entry.decimalSeparator
self.decimalDigits = entry.decimalDigits
}
}
@@ -0,0 +1,17 @@
import Foundation
import TelegramCore
import TelegramPresentationData
public extension DataSizeStringFormatting {
init(presentationData: PresentationData) {
self.init(decimalSeparator: presentationData.dateTimeFormat.decimalSeparator, byte: presentationData.strings.FileSize_B(_:), kilobyte: presentationData.strings.FileSize_KB(_:), megabyte: presentationData.strings.FileSize_MB(_:), gigabyte: presentationData.strings.FileSize_GB(_:))
}
init (chatPresentationData: ChatPresentationData) {
self.init(decimalSeparator: chatPresentationData.dateTimeFormat.decimalSeparator, byte: chatPresentationData.strings.FileSize_B(_:), kilobyte: chatPresentationData.strings.FileSize_KB(_:), megabyte: chatPresentationData.strings.FileSize_MB(_:), gigabyte: chatPresentationData.strings.FileSize_GB(_:))
}
init (strings: PresentationStrings, decimalSeparator: String) {
self.init(decimalSeparator: decimalSeparator, byte: strings.FileSize_B(_:), kilobyte: strings.FileSize_KB(_:), megabyte: strings.FileSize_MB(_:), gigabyte: strings.FileSize_GB(_:))
}
}
@@ -0,0 +1,178 @@
import Foundation
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import TextFormat
public func getDateTimeComponents(timestamp: Int32) -> (day: Int32, month: Int32, year: Int32, hour: Int32, minutes: Int32) {
var t: time_t = Int(timestamp)
var timeinfo = tm()
localtime_r(&t, &timeinfo);
return (timeinfo.tm_mday, timeinfo.tm_mon + 1, timeinfo.tm_year, timeinfo.tm_hour, timeinfo.tm_min)
}
public func stringForMediumCompactDate(timestamp: Int32, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, withTime: Bool = true) -> String {
var t: time_t = Int(timestamp)
var timeinfo = tm()
localtime_r(&t, &timeinfo);
let day = timeinfo.tm_mday
let month = monthAtIndex(Int(timeinfo.tm_mon), strings: strings)
let timeString: String
if withTime {
timeString = " \(stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), dateTimeFormat: dateTimeFormat))"
} else {
timeString = ""
}
let dateString: String
switch dateTimeFormat.dateFormat {
case .monthFirst:
dateString = String(format: "%@ %02d%@", month, day, timeString)
case .dayFirst:
dateString = String(format: "%02d %@%@", day, month, timeString)
}
return dateString
}
public func stringForMediumDate(timestamp: Int32, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, withTime: Bool = true) -> String {
var t: time_t = Int(timestamp)
var timeinfo = tm()
localtime_r(&t, &timeinfo);
let day = timeinfo.tm_mday
let month = timeinfo.tm_mon + 1
let year = timeinfo.tm_year
let dateString: String
let separator = dateTimeFormat.dateSeparator
let suffix = dateTimeFormat.dateSuffix
let displayYear = dateTimeFormat.requiresFullYear ? year - 100 + 2000 : year - 100
switch dateTimeFormat.dateFormat {
case .monthFirst:
dateString = String(format: "%02d%@%02d%@%02d%@", month, separator, day, separator, displayYear, suffix)
case .dayFirst:
dateString = String(format: "%02d%@%02d%@%02d%@", day, separator, month, separator, displayYear, suffix)
}
if withTime {
let timeString = stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), dateTimeFormat: dateTimeFormat)
return strings.Time_MediumDate(dateString, timeString).string
} else {
return dateString
}
}
public func stringForFullDate(timestamp: Int32, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat) -> String {
var t: time_t = Int(timestamp)
var timeinfo = tm()
localtime_r(&t, &timeinfo);
let dayString = "\(timeinfo.tm_mday)"
let yearString = "\(2000 + timeinfo.tm_year - 100)"
let timeString = stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), dateTimeFormat: dateTimeFormat)
let monthFormat: (String, String, String) -> PresentationStrings.FormattedString
switch timeinfo.tm_mon + 1 {
case 1:
monthFormat = strings.Time_PreciseDate_m1
case 2:
monthFormat = strings.Time_PreciseDate_m2
case 3:
monthFormat = strings.Time_PreciseDate_m3
case 4:
monthFormat = strings.Time_PreciseDate_m4
case 5:
monthFormat = strings.Time_PreciseDate_m5
case 6:
monthFormat = strings.Time_PreciseDate_m6
case 7:
monthFormat = strings.Time_PreciseDate_m7
case 8:
monthFormat = strings.Time_PreciseDate_m8
case 9:
monthFormat = strings.Time_PreciseDate_m9
case 10:
monthFormat = strings.Time_PreciseDate_m10
case 11:
monthFormat = strings.Time_PreciseDate_m11
case 12:
monthFormat = strings.Time_PreciseDate_m12
default:
return ""
}
return monthFormat(dayString, yearString, timeString).string
}
public func stringForDate(timestamp: Int32, timeZone: TimeZone? = TimeZone(secondsFromGMT: 0), strings: PresentationStrings) -> String {
let formatter = DateFormatter()
formatter.timeStyle = .none
formatter.dateStyle = .medium
formatter.timeZone = timeZone
formatter.locale = localeWithStrings(strings)
return formatter.string(from: Date(timeIntervalSince1970: Double(timestamp)))
}
public func stringForDate(date: Date, timeZone: TimeZone? = TimeZone(secondsFromGMT: 0), strings: PresentationStrings) -> String {
let formatter = DateFormatter()
formatter.timeStyle = .none
formatter.dateStyle = .medium
formatter.timeZone = timeZone
formatter.locale = localeWithStrings(strings)
return formatter.string(from: date)
}
public func stringForDateWithoutYear(date: Date, timeZone: TimeZone? = TimeZone(secondsFromGMT: 0), strings: PresentationStrings) -> String {
let formatter = DateFormatter()
formatter.timeStyle = .none
formatter.timeZone = timeZone
formatter.locale = localeWithStrings(strings)
formatter.setLocalizedDateFormatFromTemplate("MMMMd")
return formatter.string(from: date)
}
public func roundDateToDays(_ timestamp: Int32) -> Int32 {
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = TimeZone(secondsFromGMT: 0)!
var components = calendar.dateComponents(Set([.era, .year, .month, .day]), from: Date(timeIntervalSince1970: Double(timestamp)))
components.hour = 0
components.minute = 0
components.second = 0
guard let date = calendar.date(from: components) else {
assertionFailure()
return timestamp
}
return Int32(date.timeIntervalSince1970)
}
// MARK: Swiftgram
public func stringForDateWithoutDay(date: Date, timeZone: TimeZone? = TimeZone(secondsFromGMT: 0), strings: PresentationStrings) -> String {
let formatter = DateFormatter()
formatter.timeStyle = .none
formatter.timeZone = timeZone
formatter.locale = localeWithStrings(strings)
formatter.setLocalizedDateFormatFromTemplate("MMMMyyyy")
return formatter.string(from: date)
}
public func stringForDateWithoutDayAndMonth(date: Date, timeZone: TimeZone? = TimeZone(secondsFromGMT: 0), strings: PresentationStrings) -> String {
let formatter = DateFormatter()
formatter.timeStyle = .none
formatter.timeZone = timeZone
formatter.locale = localeWithStrings(strings)
formatter.setLocalizedDateFormatFromTemplate("yyyy")
return formatter.string(from: date)
}
@@ -0,0 +1,12 @@
import UIKit
public func stringForDeviceType() -> String {
let model = UIDevice.current.model.lowercased()
if model.contains("ipad") {
return "iPad"
} else if model.contains("ipod") {
return "iPod touch"
} else {
return "iPhone"
}
}
@@ -0,0 +1,61 @@
import Foundation
import MapKit
import TelegramPresentationData
private var sharedShortDistanceFormatter: MKDistanceFormatter?
public func shortStringForDistance(strings: PresentationStrings, distance: Int32) -> String {
let distanceFormatter: MKDistanceFormatter
if let currentDistanceFormatter = sharedShortDistanceFormatter {
distanceFormatter = currentDistanceFormatter
} else {
distanceFormatter = MKDistanceFormatter()
distanceFormatter.unitStyle = .abbreviated
sharedShortDistanceFormatter = distanceFormatter
}
let locale = localeWithStrings(strings)
if distanceFormatter.locale != locale {
distanceFormatter.locale = locale
}
let distance = max(1, distance)
var result = distanceFormatter.string(fromDistance: Double(distance))
if result.hasPrefix("0 ") {
result = result.replacingOccurrences(of: "0 ", with: "1 ")
}
return result
}
private var sharedDistanceFormatter: MKDistanceFormatter?
public func stringForDistance(strings: PresentationStrings, distance: CLLocationDistance) -> String {
let distanceFormatter: MKDistanceFormatter
if let currentDistanceFormatter = sharedDistanceFormatter {
distanceFormatter = currentDistanceFormatter
} else {
distanceFormatter = MKDistanceFormatter()
distanceFormatter.unitStyle = .full
sharedDistanceFormatter = distanceFormatter
}
let locale = localeWithStrings(strings)
if distanceFormatter.locale != locale {
distanceFormatter.locale = locale
}
return distanceFormatter.string(fromDistance: distance)
}
public func flagEmoji(countryCode: String) -> String {
if countryCode.uppercased() == "FT" {
return "🏴‍☠️"
}
if countryCode.uppercased() == "XX" {
return "🏳️"
}
let base : UInt32 = 127397
var flagString = ""
for v in countryCode.uppercased().unicodeScalars {
flagString.unicodeScalars.append(UnicodeScalar(base + v.value)!)
}
return flagString
}
@@ -0,0 +1,10 @@
import Foundation
import TelegramPresentationData
public func formatCollectibleNumber(_ number: Int32, dateTimeFormat: PresentationDateTimeFormat) -> String {
if number > 9999 {
return presentationStringsFormattedNumber(number, dateTimeFormat.groupingSeparator)
} else {
return "\(number)"
}
}
@@ -0,0 +1,28 @@
import Foundation
import TelegramPresentationData
private let systemLocaleRegionSuffix: String = {
let identifier = Locale.current.identifier
if let range = identifier.range(of: "_") {
return String(identifier[range.lowerBound...])
} else {
return ""
}
}()
public let usEnglishLocale = Locale(identifier: "en_US")
public func localeWithStrings(_ strings: PresentationStrings) -> Locale {
var languageCode = strings.baseLanguageCode
// MARK: - Swiftgram fix for locale bugs, like location crash
if #available(iOS 18, *) {
let rawSuffix = "-raw"
if languageCode.hasSuffix(rawSuffix) {
languageCode = String(languageCode.dropLast(rawSuffix.count))
}
}
let code = languageCode + systemLocaleRegionSuffix
return Locale(identifier: code)
}
@@ -0,0 +1,577 @@
import Foundation
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import PlatformRestrictionMatching
import TextFormat
public enum MessageContentKindKey {
case text
case image
case video
case videoMessage
case audioMessage
case sticker
case animation
case file
case contact
case game
case location
case liveLocation
case expiredImage
case expiredVideo
case expiredVoiceMessage
case expiredVideoMessage
case poll
case todo
case restricted
case dice
case invoice
case story
case giveaway
case paidContent
}
public enum MessageContentKind: Equatable {
case text(NSAttributedString)
case image
case video
case videoMessage
case audioMessage
case sticker(String)
case animation
case file(String)
case contact
case game(String)
case location
case liveLocation
case expiredImage
case expiredVideo
case expiredVoiceMessage
case expiredVideoMessage
case poll(String)
case todo(String)
case restricted(String)
case dice(String)
case invoice(String)
case story
case giveaway
public func isSemanticallyEqual(to other: MessageContentKind) -> Bool {
switch self {
case .text:
if case .text = other {
return true
} else {
return false
}
case .image:
if case .image = other {
return true
} else {
return false
}
case .video:
if case .video = other {
return true
} else {
return false
}
case .videoMessage:
if case .videoMessage = other {
return true
} else {
return false
}
case .audioMessage:
if case .audioMessage = other {
return true
} else {
return false
}
case .sticker:
if case .sticker = other {
return true
} else {
return false
}
case .animation:
if case .animation = other {
return true
} else {
return false
}
case .file:
if case .file = other {
return true
} else {
return false
}
case .contact:
if case .contact = other {
return true
} else {
return false
}
case .game:
if case .game = other {
return true
} else {
return false
}
case .location:
if case .location = other {
return true
} else {
return false
}
case .liveLocation:
if case .liveLocation = other {
return true
} else {
return false
}
case .expiredImage:
if case .expiredImage = other {
return true
} else {
return false
}
case .expiredVideo:
if case .expiredVideo = other {
return true
} else {
return false
}
case .expiredVoiceMessage:
if case .expiredVoiceMessage = other {
return true
} else {
return false
}
case .expiredVideoMessage:
if case .expiredVideoMessage = other {
return true
} else {
return false
}
case .poll:
if case .poll = other {
return true
} else {
return false
}
case .todo:
if case .todo = other {
return true
} else {
return false
}
case .restricted:
if case .restricted = other {
return true
} else {
return false
}
case .dice:
if case .dice = other {
return true
} else {
return false
}
case .invoice:
if case .invoice = other {
return true
} else {
return false
}
case .story:
if case .story = other {
return true
} else {
return false
}
case .giveaway:
if case .giveaway = other {
return true
} else {
return false
}
}
}
public var key: MessageContentKindKey {
switch self {
case .text:
return .text
case .image:
return .image
case .video:
return .video
case .videoMessage:
return .videoMessage
case .audioMessage:
return .audioMessage
case .sticker:
return .sticker
case .animation:
return .animation
case .file:
return .file
case .contact:
return .contact
case .game:
return .game
case .location:
return .location
case .liveLocation:
return .liveLocation
case .expiredImage:
return .expiredImage
case .expiredVideo:
return .expiredVideo
case .expiredVoiceMessage:
return .expiredVoiceMessage
case .expiredVideoMessage:
return .expiredVideoMessage
case .poll:
return .poll
case .todo:
return .todo
case .restricted:
return .restricted
case .dice:
return .dice
case .invoice:
return .invoice
case .story:
return .story
case .giveaway:
return .giveaway
}
}
}
public func messageTextWithAttributes(message: EngineMessage) -> NSAttributedString {
var attributedText = NSAttributedString(string: message.text)
var entities: TextEntitiesMessageAttribute?
for attribute in message.attributes {
if let attribute = attribute as? TextEntitiesMessageAttribute {
entities = attribute
break
}
}
if let entities = entities?.entities {
let updatedString = NSMutableAttributedString(attributedString: attributedText)
for entity in entities.sorted(by: { $0.range.lowerBound > $1.range.lowerBound }) {
guard case let .CustomEmoji(_, fileId) = entity.type else {
continue
}
let range = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)
let currentDict = updatedString.attributes(at: range.lowerBound, effectiveRange: nil)
var updatedAttributes: [NSAttributedString.Key: Any] = currentDict
updatedAttributes[ChatTextInputAttributes.customEmoji] = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: message.associatedMedia[MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)] as? TelegramMediaFile)
let insertString = NSAttributedString(string: updatedString.attributedSubstring(from: range).string, attributes: updatedAttributes)
updatedString.replaceCharacters(in: range, with: insertString)
}
attributedText = updatedString
}
return attributedText
}
public func messageContentKind(contentSettings: ContentSettings, message: EngineMessage, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, accountPeerId: EnginePeer.Id) -> MessageContentKind {
for attribute in message.attributes {
if let attribute = attribute as? RestrictedContentMessageAttribute {
if let text = attribute.platformText(platform: "ios", contentSettings: contentSettings, chatId: message.author?.id.id._internalGetInt64Value()) {
return .restricted(text)
}
break
}
}
for media in message.media {
if let kind = mediaContentKind(EngineMedia(media), message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: accountPeerId) {
return kind
}
}
return .text(messageTextWithAttributes(message: message))
}
public func mediaContentKind(_ media: EngineMedia, message: EngineMessage? = nil, strings: PresentationStrings? = nil, nameDisplayOrder: PresentationPersonNameOrder? = nil, dateTimeFormat: PresentationDateTimeFormat? = nil, accountPeerId: EnginePeer.Id? = nil) -> MessageContentKind? {
switch media {
case let .expiredContent(expiredMedia):
switch expiredMedia.data {
case .image:
return .expiredImage
case .file:
return .expiredVideo
case .voiceMessage:
return .expiredVoiceMessage
case .videoMessage:
return .expiredVideoMessage
}
case .image:
return .image
case let .file(file):
var fileName: String = ""
var result: MessageContentKind?
for attribute in file.attributes {
switch attribute {
case let .Sticker(text, _, _):
return .sticker(text)
case let .FileName(name):
fileName = name
case let .Audio(isVoice, _, title, performer, _):
if isVoice {
return .audioMessage
} else {
if let title = title, let performer = performer, !title.isEmpty, !performer.isEmpty {
return .file(title + "" + performer)
} else if let title = title, !title.isEmpty {
return .file(title)
} else if let performer = performer, !performer.isEmpty {
return .file(performer)
}
}
case let .Video(_, _, flags, _, _, _):
if file.isAnimated {
result = .animation
} else {
if flags.contains(.instantRoundVideo) {
result = .videoMessage
} else {
result = .video
}
}
default:
break
}
}
if let result = result {
return result
}
if file.isVideoSticker || file.isAnimatedSticker {
return .sticker("")
}
return .file(fileName)
case .contact:
return .contact
case let .game(game):
return .game(game.title)
case let .geo(location):
if location.liveBroadcastingTimeout != nil {
return .liveLocation
} else {
return .location
}
case .action:
if let message = message, let strings = strings, let nameDisplayOrder = nameDisplayOrder, let accountPeerId = accountPeerId {
return .text(NSAttributedString(string: plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat ?? PresentationDateTimeFormat(timeFormat: .military, dateFormat: .dayFirst, dateSeparator: ".", dateSuffix: "", requiresFullYear: false, decimalSeparator: ".", groupingSeparator: ""), message: message, accountPeerId: accountPeerId, forChatList: false, forForumOverview: false)?.0 ?? ""))
} else {
return nil
}
case let .poll(poll):
return .poll(poll.text)
case let .todo(todo):
return .todo(todo.text)
case let .dice(dice):
return .dice(dice.emoji)
case let .invoice(invoice):
if !invoice.description.isEmpty {
return .invoice(invoice.description)
} else {
return .invoice(invoice.title)
}
case .story:
return .story
case .giveaway, .giveawayResults:
return .giveaway
case let .webpage(webpage):
if let message, message.text.isEmpty, case let .Loaded(content) = webpage.content {
return .text(NSAttributedString(string: content.displayUrl))
} else {
return nil
}
case let .paidContent(paidContent):
switch paidContent.extendedMedia.first {
case let .preview(_, _, videoDuration):
if let _ = videoDuration {
return .video
} else {
return .image
}
case let .full(media):
if media is TelegramMediaImage {
return .image
} else if media is TelegramMediaFile {
return .video
} else {
return nil
}
default:
return nil
}
default:
return nil
}
}
public func stringForMediaKind(_ kind: MessageContentKind, strings: PresentationStrings) -> (NSAttributedString, Bool) {
switch kind {
case let .text(text):
return (foldLineBreaks(text), false)
case .image:
return (NSAttributedString(string: strings.Message_Photo), true)
case .video:
return (NSAttributedString(string: strings.Message_Video), true)
case .videoMessage:
return (NSAttributedString(string: strings.Message_VideoMessage), true)
case .audioMessage:
return (NSAttributedString(string: strings.Message_Audio), true)
case let .sticker(text):
if text.isEmpty {
return (NSAttributedString(string: strings.Message_Sticker), true)
} else {
return (NSAttributedString(string: strings.Message_StickerText(text).string), true)
}
case .animation:
return (NSAttributedString(string: strings.Message_Animation), true)
case let .file(text):
if text.isEmpty {
return (NSAttributedString(string: strings.Message_File), true)
} else {
return (NSAttributedString(string: text), true)
}
case .contact:
return (NSAttributedString(string: strings.Message_Contact), true)
case let .game(text):
return (NSAttributedString(string: text), true)
case .location:
return (NSAttributedString(string: strings.Message_Location), true)
case .liveLocation:
return (NSAttributedString(string: strings.Message_LiveLocation), true)
case .expiredImage:
return (NSAttributedString(string: strings.Message_ImageExpired), true)
case .expiredVideo:
return (NSAttributedString(string: strings.Message_VideoExpired), true)
case .expiredVoiceMessage:
return (NSAttributedString(string: strings.Message_VoiceMessageExpired), true)
case .expiredVideoMessage:
return (NSAttributedString(string: strings.Message_VideoMessageExpired), true)
case let .poll(text):
return (NSAttributedString(string: "📊 \(text)"), false)
case let .todo(text):
return (NSAttributedString(string: "☑️ \(text)"), false)
case let .restricted(text):
return (NSAttributedString(string: text), false)
case let .dice(emoji):
return (NSAttributedString(string: emoji), true)
case let .invoice(text):
return (NSAttributedString(string: text), true)
case .story:
return (NSAttributedString(string: strings.Message_Story), true)
case .giveaway:
return (NSAttributedString(string: strings.Message_Giveaway), true)
}
}
public func descriptionStringForMessage(contentSettings: ContentSettings, message: EngineMessage, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, accountPeerId: EnginePeer.Id) -> (NSAttributedString, Bool, Bool) {
let contentKind = messageContentKind(contentSettings: contentSettings, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: accountPeerId)
if !message.text.isEmpty && ![.expiredImage, .expiredVideo].contains(contentKind.key) {
return (foldLineBreaks(messageTextWithAttributes(message: message)), false, true)
}
let result = stringForMediaKind(contentKind, strings: strings)
return (result.0, result.1, false)
}
public func foldLineBreaks(_ text: String) -> String {
let lines = text.split { $0.isNewline }
var result = ""
for line in lines {
if line.isEmpty {
continue
}
if result.isEmpty {
result += line
} else {
result += " " + line
}
}
result = result.replacingOccurrences(of: "\\s+", with: " ", options: .regularExpression)
return result
}
public func foldLineBreaks(_ text: NSAttributedString) -> NSAttributedString {
let remainingString = NSMutableAttributedString(attributedString: text)
var lines: [NSAttributedString] = []
while true {
if let range = remainingString.string.range(of: "\n") {
let mappedRange = NSRange(range, in: remainingString.string)
let restString = remainingString.attributedSubstring(from: NSRange(location: 0, length: mappedRange.upperBound - 1))
lines.append(restString)
remainingString.replaceCharacters(in: NSRange(location: 0, length: mappedRange.upperBound), with: "")
} else {
if lines.isEmpty {
return text
}
if !remainingString.string.isEmpty {
lines.append(remainingString)
}
break
}
}
let result = NSMutableAttributedString()
for line in lines {
if line.string.isEmpty {
continue
}
if result.string.isEmpty {
result.append(line)
} else {
let currentAttributes = line.attributes(at: 0, effectiveRange: nil).filter { key, _ in
switch key {
case .font, .foregroundColor:
return true
default:
return false
}
}
result.append(NSAttributedString(string: " ", attributes: currentAttributes))
result.append(line)
}
}
return result
}
public func trimToLineCount(_ text: String, lineCount: Int) -> String {
if lineCount < 1 {
return ""
}
var result = ""
var i = 0
text.enumerateLines { line, stop in
if !result.isEmpty {
result += "\n"
}
result += line
i += 1
if i == lineCount {
stop = true
}
}
return result
}
@@ -0,0 +1,40 @@
import Foundation
public enum ArabicNumeralStringType {
case western
case arabic
case persian
}
public func normalizeArabicNumeralString(_ string: String, type: ArabicNumeralStringType) -> String {
var string = string
let numerals = [
("0", "٠", "۰"),
("1", "١", "۱"),
("2", "٢", "۲"),
("3", "٣", "۳"),
("4", "٤", "۴"),
("5", "٥", "۵"),
("6", "٦", "۶"),
("7", "٧", "۷"),
("8", "٨", "۸"),
("9", "٩", "۹"),
(",", "٫", "٫")
]
for (western, arabic, persian) in numerals {
switch type {
case .western:
string = string.replacingOccurrences(of: arabic, with: western)
string = string.replacingOccurrences(of: persian, with: western)
case .arabic:
string = string.replacingOccurrences(of: western, with: arabic)
string = string.replacingOccurrences(of: persian, with: arabic)
case .persian:
string = string.replacingOccurrences(of: western, with: persian)
string = string.replacingOccurrences(of: arabic, with: persian)
}
}
return string
}
@@ -0,0 +1,38 @@
import Foundation
import TelegramPresentationData
import TelegramUIPreferences
import TelegramCore
public func stringForFullAuthorName(message: EngineMessage, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, accountPeerId: EnginePeer.Id) -> [String] {
var authorString: [String] = []
if let author = message.author, [Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel].contains(message.id.peerId.namespace) {
var authorName = ""
if author.id == accountPeerId {
authorName = strings.DialogList_You
} else if author.isDeleted {
authorName = strings.User_DeletedAccount
} else {
authorName = author.displayTitle(strings: strings, displayOrder: nameDisplayOrder)
}
if let peer = message.peers[message.id.peerId].flatMap(EnginePeer.init), author.id != peer.id {
authorString = [authorName, peer.displayTitle(strings: strings, displayOrder: nameDisplayOrder)]
} else {
authorString = [authorName]
}
} else if let peer = message.peers[message.id.peerId].flatMap(EnginePeer.init) {
if message.id.peerId.namespace == Namespaces.Peer.CloudChannel {
authorString = [peer.displayTitle(strings: strings, displayOrder: nameDisplayOrder)]
} else {
if message.id.peerId == accountPeerId {
authorString = [strings.DialogList_SavedMessages]
} else if message.id.peerId.isAnonymousSavedMessages {
authorString = [strings.ChatList_AuthorHidden]
} else if message.flags.contains(.Incoming) {
authorString = [peer.displayTitle(strings: strings, displayOrder: nameDisplayOrder)]
} else {
authorString = [strings.DialogList_You, peer.displayTitle(strings: strings, displayOrder: nameDisplayOrder)]
}
}
}
return authorString
}
@@ -0,0 +1,131 @@
import Foundation
import TelegramCore
import TelegramPresentationData
private let modernSoundsNamePaths: [KeyPath<PresentationStrings, String>] = [
\.NotificationsSound_Note,
\.NotificationsSound_Aurora,
\.NotificationsSound_Bamboo,
\.NotificationsSound_Chord,
\.NotificationsSound_Circles,
\.NotificationsSound_Complete,
\.NotificationsSound_Hello,
\.NotificationsSound_Input,
\.NotificationsSound_Keys,
\.NotificationsSound_Popcorn,
\.NotificationsSound_Pulse,
\.NotificationsSound_Synth,
\.NotificationsSound_Rebound,
\.NotificationsSound_Antic,
\.NotificationsSound_Cheers,
\.NotificationsSound_Droplet,
\.NotificationsSound_Handoff,
\.NotificationsSound_Milestone,
\.NotificationsSound_Passage,
\.NotificationsSound_Portal,
\.NotificationsSound_Rattle,
\.NotificationsSound_Slide,
\.NotificationsSound_Welcome
]
private let classicSoundNamePaths: [KeyPath<PresentationStrings, String>] = [
\.NotificationsSound_Tritone,
\.NotificationsSound_Tremolo,
\.NotificationsSound_Alert,
\.NotificationsSound_Bell,
\.NotificationsSound_Calypso,
\.NotificationsSound_Chime,
\.NotificationsSound_Glass,
\.NotificationsSound_Telegraph
]
private func soundName(strings: PresentationStrings, sound: PeerMessageSound, notificationSoundList: NotificationSoundList?) -> String {
switch sound {
case .none:
return strings.NotificationsSound_None
case .default:
return ""
case let .bundledModern(id):
if id >= 0 && Int(id) < modernSoundsNamePaths.count {
return strings[keyPath: modernSoundsNamePaths[Int(id)]]
}
return "Sound \(id)"
case let .bundledClassic(id):
if id >= 0 && Int(id) < classicSoundNamePaths.count {
return strings[keyPath: classicSoundNamePaths[Int(id)]]
}
return "Sound \(id)"
case let .cloud(fileId):
if let (id, legacyCategory) = getCloudLegacySound(id: fileId) {
switch legacyCategory {
case .modern:
if id >= 0 && Int(id) < modernSoundsNamePaths.count {
return strings[keyPath: modernSoundsNamePaths[Int(id)]]
}
case .classic:
if id >= 0 && Int(id) < classicSoundNamePaths.count {
return strings[keyPath: classicSoundNamePaths[Int(id)]]
}
}
}
guard let notificationSoundList = notificationSoundList else {
return strings.Channel_NotificationLoading
}
for sound in notificationSoundList.sounds {
if sound.file.fileId.id == fileId {
for attribute in sound.file.attributes {
switch attribute {
case let .Audio(_, _, title, performer, _):
if let title = title, !title.isEmpty, let performer = performer, !performer.isEmpty {
return "\(title) - \(performer)"
} else if let title = title, !title.isEmpty {
return title
} else if let performer = performer, !performer.isEmpty {
return performer
}
default:
break
}
}
if let fileName = sound.file.fileName, !fileName.isEmpty {
if let range = fileName.range(of: ".", options: .backwards) {
return String(fileName[fileName.startIndex ..< range.lowerBound])
} else {
return fileName
}
}
return "Cloud Tone"
}
}
return ""
}
}
public func localizedPeerNotificationSoundString(strings: PresentationStrings, notificationSoundList: NotificationSoundList?, sound: PeerMessageSound, default: PeerMessageSound? = nil) -> String {
switch sound {
case .default:
if let defaultSound = `default` {
let name = soundName(strings: strings, sound: defaultSound, notificationSoundList: notificationSoundList)
let actualName: String
if name.isEmpty {
actualName = soundName(strings: strings, sound: defaultCloudPeerNotificationSound, notificationSoundList: notificationSoundList)
} else {
actualName = name
}
return strings.UserInfo_NotificationsDefaultSound(actualName).string
} else {
let name = soundName(strings: strings, sound: defaultCloudPeerNotificationSound, notificationSoundList: notificationSoundList)
return name
}
default:
let name = soundName(strings: strings, sound: sound, notificationSoundList: notificationSoundList)
if name.isEmpty {
return localizedPeerNotificationSoundString(strings: strings, notificationSoundList: notificationSoundList, sound: .default, default: `default`)
} else {
return name
}
}
}
@@ -0,0 +1,685 @@
import Foundation
import TelegramCore
import TelegramPresentationData
import TextFormat
public func stringForTimestamp(day: Int32, month: Int32, year: Int32, dateTimeFormat: PresentationDateTimeFormat) -> String {
let separator = dateTimeFormat.dateSeparator
let suffix = dateTimeFormat.dateSuffix
let displayYear = dateTimeFormat.requiresFullYear ? year - 100 + 2000 : year - 100
switch dateTimeFormat.dateFormat {
case .monthFirst:
return String(format: "%02d%@%02d%@%02d%@", month, separator, day, separator, displayYear, suffix)
case .dayFirst:
return String(format: "%02d%@%02d%@%02d%@", day, separator, month, separator, displayYear, suffix)
}
}
public func stringForTimestamp(day: Int32, month: Int32, dateTimeFormat: PresentationDateTimeFormat) -> String {
let separator = dateTimeFormat.dateSeparator
let suffix = dateTimeFormat.dateSuffix
switch dateTimeFormat.dateFormat {
case .monthFirst:
return String(format: "%02d%@%02d%@", month, separator, day, suffix)
case .dayFirst:
return String(format: "%02d%@%02d%@", day, separator, month, suffix)
}
}
public func shortStringForDayOfWeek(strings: PresentationStrings, day: Int32) -> String {
switch day {
case 0:
return strings.Weekday_ShortSunday
case 1:
return strings.Weekday_ShortMonday
case 2:
return strings.Weekday_ShortTuesday
case 3:
return strings.Weekday_ShortWednesday
case 4:
return strings.Weekday_ShortThursday
case 5:
return strings.Weekday_ShortFriday
case 6:
return strings.Weekday_ShortSaturday
default:
return ""
}
}
public func stringForMonth(strings: PresentationStrings, month: Int32) -> String {
switch month {
case 0:
return strings.Month_GenJanuary
case 1:
return strings.Month_GenFebruary
case 2:
return strings.Month_GenMarch
case 3:
return strings.Month_GenApril
case 4:
return strings.Month_GenMay
case 5:
return strings.Month_GenJune
case 6:
return strings.Month_GenJuly
case 7:
return strings.Month_GenAugust
case 8:
return strings.Month_GenSeptember
case 9:
return strings.Month_GenOctober
case 10:
return strings.Month_GenNovember
case 11:
return strings.Month_GenDecember
default:
return ""
}
}
public func stringForMonth(strings: PresentationStrings, month: Int32, ofYear year: Int32) -> String {
let yearString = "\(1900 + year)"
switch month {
case 0:
return strings.Time_MonthOfYear_m1(yearString).string
case 1:
return strings.Time_MonthOfYear_m2(yearString).string
case 2:
return strings.Time_MonthOfYear_m3(yearString).string
case 3:
return strings.Time_MonthOfYear_m4(yearString).string
case 4:
return strings.Time_MonthOfYear_m5(yearString).string
case 5:
return strings.Time_MonthOfYear_m6(yearString).string
case 6:
return strings.Time_MonthOfYear_m7(yearString).string
case 7:
return strings.Time_MonthOfYear_m8(yearString).string
case 8:
return strings.Time_MonthOfYear_m9(yearString).string
case 9:
return strings.Time_MonthOfYear_m10(yearString).string
case 10:
return strings.Time_MonthOfYear_m11(yearString).string
default:
return strings.Time_MonthOfYear_m12(yearString).string
}
}
func monthAtIndex(_ index: Int, strings: PresentationStrings) -> String {
switch index {
case 0:
return strings.Month_ShortJanuary
case 1:
return strings.Month_ShortFebruary
case 2:
return strings.Month_ShortMarch
case 3:
return strings.Month_ShortApril
case 4:
return strings.Month_ShortMay
case 5:
return strings.Month_ShortJune
case 6:
return strings.Month_ShortJuly
case 7:
return strings.Month_ShortAugust
case 8:
return strings.Month_ShortSeptember
case 9:
return strings.Month_ShortOctober
case 10:
return strings.Month_ShortNovember
case 11:
return strings.Month_ShortDecember
default:
return ""
}
}
public func stringForCompactDate(timestamp: Int32, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat) -> String {
var t: time_t = time_t(timestamp)
var timeinfo: tm = tm()
localtime_r(&t, &timeinfo)
return "\(shortStringForDayOfWeek(strings: strings, day: timeinfo.tm_wday)) \(timeinfo.tm_mday) \(monthAtIndex(Int(timeinfo.tm_mon), strings: strings))"
}
public func stringForCompactBirthday(_ birthday: TelegramBirthday, strings: PresentationStrings, showAge: Bool = false) -> String {
var components: [String] = []
components.append("\(birthday.day)")
components.append(monthAtIndex(Int(birthday.month) - 1, strings: strings))
if let year = birthday.year {
components.append("\(year)")
if showAge {
var dateComponents = DateComponents()
dateComponents.day = Int(birthday.day)
dateComponents.month = Int(birthday.month)
dateComponents.year = Int(year)
let calendar = Calendar.current
if let birthDate = calendar.date(from: dateComponents) {
if let age = calendar.dateComponents([.year], from: birthDate, to: Date()).year, age > 0 {
components.append("(\(strings.UserInfo_Age(Int32(age))))")
}
}
}
}
return components.joined(separator: " ")
}
public func ageForBirthday(_ birthday: TelegramBirthday) -> Int? {
guard let year = birthday.year else {
return nil
}
var dateComponents = DateComponents()
dateComponents.day = Int(birthday.day)
dateComponents.month = Int(birthday.month)
dateComponents.year = Int(year)
let calendar = Calendar.current
if let birthDate = calendar.date(from: dateComponents) {
if let age = calendar.dateComponents([.year], from: birthDate, to: Date()).year {
return age
}
}
return nil
}
public enum RelativeTimestampFormatDay {
case today
case yesterday
case tomorrow
}
public func stringForUserPresence(strings: PresentationStrings, day: RelativeTimestampFormatDay, dateTimeFormat: PresentationDateTimeFormat, hours: Int32, minutes: Int32) -> String {
let dayString: String
switch day {
case .today, .tomorrow:
dayString = strings.LastSeen_TodayAt(stringForShortTimestamp(hours: hours, minutes: minutes, dateTimeFormat: dateTimeFormat)).string
case .yesterday:
dayString = strings.LastSeen_YesterdayAt(stringForShortTimestamp(hours: hours, minutes: minutes, dateTimeFormat: dateTimeFormat)).string
}
return dayString
}
private func humanReadableStringForTimestamp(strings: PresentationStrings, day: RelativeTimestampFormatDay, dateTimeFormat: PresentationDateTimeFormat, hours: Int32, minutes: Int32, format: HumanReadableStringFormat? = nil) -> PresentationStrings.FormattedString {
let result: PresentationStrings.FormattedString
switch day {
case .today:
let string = stringForShortTimestamp(hours: hours, minutes: minutes, dateTimeFormat: dateTimeFormat)
result = format?.todayFormatString(string) ?? strings.Time_TodayAt(string)
case .yesterday:
let string = stringForShortTimestamp(hours: hours, minutes: minutes, dateTimeFormat: dateTimeFormat)
result = format?.yesterdayFormatString(string) ?? strings.Time_YesterdayAt(string)
case .tomorrow:
let string = stringForShortTimestamp(hours: hours, minutes: minutes, dateTimeFormat: dateTimeFormat)
result = format?.tomorrowFormatString(string) ?? strings.Time_TomorrowAt(string)
}
return result
}
public struct HumanReadableStringFormat {
let dateFormatString: (String) -> PresentationStrings.FormattedString
let tomorrowFormatString: (String) -> PresentationStrings.FormattedString
let todayFormatString: (String) -> PresentationStrings.FormattedString
let yesterdayFormatString: (String) -> PresentationStrings.FormattedString
let daysFormatString: ((Int) -> PresentationStrings.FormattedString)?
public init(
dateFormatString: @escaping (String) -> PresentationStrings.FormattedString,
tomorrowFormatString: @escaping (String) -> PresentationStrings.FormattedString,
todayFormatString: @escaping (String) -> PresentationStrings.FormattedString,
yesterdayFormatString: @escaping (String) -> PresentationStrings.FormattedString = { PresentationStrings.FormattedString(string: $0, ranges: []) },
daysFormatString: ((Int) -> PresentationStrings.FormattedString)? = nil
) {
self.dateFormatString = dateFormatString
self.tomorrowFormatString = tomorrowFormatString
self.todayFormatString = todayFormatString
self.yesterdayFormatString = yesterdayFormatString
self.daysFormatString = daysFormatString
}
}
public func humanReadableStringForTimestamp(strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, timestamp: Int32, alwaysShowTime: Bool = false, allowYesterday: Bool = true, format: HumanReadableStringFormat? = nil) -> PresentationStrings.FormattedString {
var t: time_t = time_t(timestamp)
var timeinfo: tm = tm()
localtime_r(&t, &timeinfo)
let timestampNow = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
var now: time_t = time_t(timestampNow)
var timeinfoNow: tm = tm()
localtime_r(&now, &timeinfoNow)
if timeinfo.tm_year != timeinfoNow.tm_year {
let string: String
if alwaysShowTime {
string = stringForMediumDate(timestamp: timestamp, strings: strings, dateTimeFormat: dateTimeFormat)
} else {
string = stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, year: timeinfo.tm_year, dateTimeFormat: dateTimeFormat)
}
return format?.dateFormatString(string) ?? PresentationStrings.FormattedString(string: string, ranges: [])
}
let dayDifference = timeinfo.tm_yday - timeinfoNow.tm_yday
if dayDifference == 0 || (dayDifference == -1 && allowYesterday) || dayDifference == 1 {
let day: RelativeTimestampFormatDay
if dayDifference == 0 {
day = .today
} else if dayDifference == -1 {
day = .yesterday
} else {
day = .tomorrow
}
return humanReadableStringForTimestamp(strings: strings, day: day, dateTimeFormat: dateTimeFormat, hours: timeinfo.tm_hour, minutes: timeinfo.tm_min, format: format)
} else if dayDifference < 7, let daysFormatString = format?.daysFormatString {
return daysFormatString(Int(dayDifference))
} else {
let string: String
if alwaysShowTime {
string = stringForMediumDate(timestamp: timestamp, strings: strings, dateTimeFormat: dateTimeFormat)
} else {
string = stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, year: timeinfo.tm_year, dateTimeFormat: dateTimeFormat)
}
return format?.dateFormatString(string) ?? PresentationStrings.FormattedString(string: string, ranges: [])
}
}
public enum RelativeUserPresenceLastSeen {
case justNow
case minutesAgo(Int32)
case hoursAgo(Int32)
case todayAt(hours: Int32, minutes: Int32)
case yesterdayAt(hours: Int32, minutes: Int32)
case thisYear(month: Int32, day: Int32)
case atDate(year: Int32, month: Int32)
}
public enum RelativeUserPresenceStatus {
case offline
case online(at: Int32)
case lastSeen(at: Int32)
case recently
case lastWeek
case lastMonth
}
public func relativeUserPresenceStatus(_ presence: EnginePeer.Presence, relativeTo timestamp: Int32) -> RelativeUserPresenceStatus {
switch presence.status {
case .longTimeAgo:
return .offline
case let .present(statusTimestamp):
if statusTimestamp >= timestamp {
return .online(at: statusTimestamp)
} else {
return .lastSeen(at: statusTimestamp)
}
case .recently:
let activeUntil = presence.lastActivity + 30
if activeUntil >= timestamp {
return .online(at: activeUntil)
} else {
return .recently
}
case .lastWeek:
return .lastWeek
case .lastMonth:
return .lastMonth
}
}
public func stringForRelativeTimestamp(strings: PresentationStrings, relativeTimestamp: Int32, relativeTo timestamp: Int32, dateTimeFormat: PresentationDateTimeFormat) -> String {
var t: time_t = time_t(relativeTimestamp)
var timeinfo: tm = tm()
localtime_r(&t, &timeinfo)
var now: time_t = time_t(timestamp)
var timeinfoNow: tm = tm()
localtime_r(&now, &timeinfoNow)
if timeinfo.tm_year != timeinfoNow.tm_year {
return stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, year: timeinfo.tm_year, dateTimeFormat: dateTimeFormat)
}
let dayDifference = timeinfo.tm_yday - timeinfoNow.tm_yday
if dayDifference > -7 {
if dayDifference == 0 {
return stringForShortTimestamp(hours: timeinfo.tm_hour, minutes: timeinfo.tm_min, dateTimeFormat: dateTimeFormat)
} else {
return shortStringForDayOfWeek(strings: strings, day: timeinfo.tm_wday)
}
} else {
return stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, dateTimeFormat: dateTimeFormat)
}
}
public func stringForPreciseRelativeTimestamp(strings: PresentationStrings, relativeTimestamp: Int32, relativeTo timestamp: Int32, dateTimeFormat: PresentationDateTimeFormat) -> String {
var t: time_t = time_t(relativeTimestamp)
var timeinfo: tm = tm()
localtime_r(&t, &timeinfo)
var now: time_t = time_t(timestamp)
var timeinfoNow: tm = tm()
localtime_r(&now, &timeinfoNow)
let dayDifference = timeinfo.tm_yday - timeinfoNow.tm_yday
if dayDifference == 0 {
return stringForShortTimestamp(hours: timeinfo.tm_hour, minutes: timeinfo.tm_min, dateTimeFormat: dateTimeFormat)
} else {
return "\(stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, year: timeinfo.tm_year, dateTimeFormat: dateTimeFormat)), \(stringForShortTimestamp(hours: timeinfo.tm_hour, minutes: timeinfo.tm_min, dateTimeFormat: dateTimeFormat))"
}
}
public func stringForRelativeLiveLocationTimestamp(strings: PresentationStrings, relativeTimestamp: Int32, relativeTo timestamp: Int32, dateTimeFormat: PresentationDateTimeFormat) -> String {
let difference = timestamp - relativeTimestamp
if difference < 60 {
return strings.LiveLocationUpdated_JustNow
} else if difference < 60 * 60 {
let minutes = difference / 60
return strings.LiveLocationUpdated_MinutesAgo(minutes)
} else {
var t: time_t = time_t(relativeTimestamp)
var timeinfo: tm = tm()
localtime_r(&t, &timeinfo)
var now: time_t = time_t(timestamp)
var timeinfoNow: tm = tm()
localtime_r(&now, &timeinfoNow)
let dayDifference = timeinfo.tm_yday - timeinfoNow.tm_yday
let hours = timeinfo.tm_hour
let minutes = timeinfo.tm_min
if dayDifference == 0 {
return strings.LiveLocationUpdated_TodayAt(stringForShortTimestamp(hours: hours, minutes: minutes, dateTimeFormat: dateTimeFormat)).string
} else {
return stringForFullDate(timestamp: relativeTimestamp, strings: strings, dateTimeFormat: dateTimeFormat)
}
}
}
public func stringForRelativeSymbolicTimestamp(strings: PresentationStrings, relativeTimestamp: Int32, relativeTo timestamp: Int32, dateTimeFormat: PresentationDateTimeFormat) -> String {
var t: time_t = time_t(relativeTimestamp)
var timeinfo: tm = tm()
localtime_r(&t, &timeinfo)
var now: time_t = time_t(timestamp)
var timeinfoNow: tm = tm()
localtime_r(&now, &timeinfoNow)
let dayDifference = timeinfo.tm_yday - timeinfoNow.tm_yday
let hours = timeinfo.tm_hour
let minutes = timeinfo.tm_min
if dayDifference == 0 {
return strings.Time_TodayAt(stringForShortTimestamp(hours: hours, minutes: minutes, dateTimeFormat: dateTimeFormat)).string
} else {
return stringForFullDate(timestamp: relativeTimestamp, strings: strings, dateTimeFormat: dateTimeFormat)
}
}
public func stringForRelativeLiveLocationUpdateTimestamp(strings: PresentationStrings, relativeTimestamp: Int32, relativeTo timestamp: Int32, dateTimeFormat: PresentationDateTimeFormat) -> String {
var t: time_t = time_t(relativeTimestamp)
var timeinfo: tm = tm()
localtime_r(&t, &timeinfo)
var now: time_t = time_t(timestamp)
var timeinfoNow: tm = tm()
localtime_r(&now, &timeinfoNow)
if timeinfo.tm_year != timeinfoNow.tm_year {
return stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, year: timeinfo.tm_year, dateTimeFormat: dateTimeFormat)
}
let dayDifference = timeinfo.tm_yday - timeinfoNow.tm_yday
if dayDifference > -7 {
if dayDifference == 0 {
return stringForShortTimestamp(hours: timeinfo.tm_hour, minutes: timeinfo.tm_min, dateTimeFormat: dateTimeFormat)
} else {
return shortStringForDayOfWeek(strings: strings, day: timeinfo.tm_wday)
}
} else {
return stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, dateTimeFormat: dateTimeFormat)
}
}
public func stringForRelativeActivityTimestamp(strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, preciseTime: Bool = false, relativeTimestamp: Int32, relativeTo timestamp: Int32) -> String {
let difference = timestamp - relativeTimestamp
if difference < 60 {
return strings.Time_JustNow
} else if difference < 60 * 60 {
let minutes = difference / 60
return strings.Time_MinutesAgo(minutes)
} else {
var t: time_t = time_t(relativeTimestamp)
var timeinfo: tm = tm()
localtime_r(&t, &timeinfo)
var now: time_t = time_t(timestamp)
var timeinfoNow: tm = tm()
localtime_r(&now, &timeinfoNow)
if timeinfo.tm_year != timeinfoNow.tm_year {
return strings.Time_AtDate(stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, year: timeinfo.tm_year, dateTimeFormat: dateTimeFormat)).string
}
let dayDifference = timeinfo.tm_yday - timeinfoNow.tm_yday
if dayDifference == 0 || dayDifference == -1 {
let day: RelativeTimestampFormatDay
if dayDifference == 0 {
let minutes = difference / (60 * 60)
return strings.Time_HoursAgo(minutes)
} else {
day = .yesterday
}
return humanReadableStringForTimestamp(strings: strings, day: day, dateTimeFormat: dateTimeFormat, hours: timeinfo.tm_hour, minutes: timeinfo.tm_min).string
} else if preciseTime {
return strings.Time_AtPreciseDate(stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, year: timeinfo.tm_year, dateTimeFormat: dateTimeFormat), stringForShortTimestamp(hours: timeinfo.tm_hour, minutes: timeinfo.tm_min, dateTimeFormat: dateTimeFormat)).string
} else {
return strings.Time_AtDate(stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, year: timeinfo.tm_year, dateTimeFormat: dateTimeFormat)).string
}
}
}
public func stringForStoryActivityTimestamp(strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, preciseTime: Bool = false, relativeTimestamp: Int32, relativeTo timestamp: Int32, short: Bool = false) -> String {
let difference = timestamp - relativeTimestamp
if difference < 60 {
return short ? strings.ShortTime_JustNow : strings.Time_JustNow
} else if difference < 60 * 60 {
let minutes = difference / 60
return short ? strings.ShortTime_MinutesAgo(minutes) : strings.Time_MinutesAgo(minutes)
} else {
var t: time_t = time_t(relativeTimestamp)
var timeinfo: tm = tm()
localtime_r(&t, &timeinfo)
var now: time_t = time_t(timestamp)
var timeinfoNow: tm = tm()
localtime_r(&now, &timeinfoNow)
if timeinfo.tm_year != timeinfoNow.tm_year {
return strings.Time_AtDate(stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, year: timeinfo.tm_year, dateTimeFormat: dateTimeFormat)).string
}
let dayDifference = timeinfo.tm_yday - timeinfoNow.tm_yday
if dayDifference == 0 || dayDifference == -1 {
let day: RelativeTimestampFormatDay
if dayDifference == 0 || short {
let hours = difference / (60 * 60)
return short ? strings.ShortTime_HoursAgo(hours) : strings.Time_HoursAgo(hours)
} else {
day = .yesterday
}
return humanReadableStringForTimestamp(strings: strings, day: day, dateTimeFormat: dateTimeFormat, hours: timeinfo.tm_hour, minutes: timeinfo.tm_min).string
} else if preciseTime {
let yearDate: String
if timeinfo.tm_year == timeinfoNow.tm_year {
if timeinfo.tm_yday == timeinfoNow.tm_yday {
yearDate = strings.Weekday_Today
} else {
yearDate = strings.Date_ChatDateHeader(monthAtIndex(Int(timeinfo.tm_mon), strings: strings), "\(timeinfo.tm_mday)").string
}
} else {
yearDate = strings.Date_ChatDateHeaderYear(monthAtIndex(Int(timeinfo.tm_mon), strings: strings), "\(timeinfo.tm_mday)", "\(1900 + timeinfo.tm_year)").string
}
return strings.Time_AtPreciseDate(yearDate, stringForShortTimestamp(hours: timeinfo.tm_hour, minutes: timeinfo.tm_min, dateTimeFormat: dateTimeFormat)).string
} else {
return strings.Time_AtDate(stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, year: timeinfo.tm_year, dateTimeFormat: dateTimeFormat)).string
}
}
}
public func stringAndActivityForUserPresence(strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, presence: EnginePeer.Presence, relativeTo timestamp: Int32, expanded: Bool = false) -> (String, Bool) {
switch presence.status {
case let .present(statusTimestamp):
if statusTimestamp >= timestamp {
return (strings.Presence_online, true)
} else {
let difference = timestamp - statusTimestamp
if difference < 60 {
return (strings.LastSeen_JustNow, false)
} else if difference < 60 * 60 && !expanded {
let minutes = difference / 60
return (strings.LastSeen_MinutesAgo(minutes), false)
} else {
var t: time_t = time_t(statusTimestamp)
var timeinfo: tm = tm()
localtime_r(&t, &timeinfo)
var now: time_t = time_t(timestamp)
var timeinfoNow: tm = tm()
localtime_r(&now, &timeinfoNow)
if timeinfo.tm_year != timeinfoNow.tm_year {
return (strings.LastSeen_AtDate(stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, year: timeinfo.tm_year, dateTimeFormat: dateTimeFormat)).string, false)
}
let dayDifference = timeinfo.tm_yday - timeinfoNow.tm_yday
if dayDifference == 0 || dayDifference == -1 {
let day: RelativeTimestampFormatDay
if dayDifference == 0 {
if expanded {
day = .today
} else {
let minutes = difference / (60 * 60)
return (strings.LastSeen_HoursAgo(minutes), false)
}
} else {
day = .yesterday
}
return (stringForUserPresence(strings: strings, day: day, dateTimeFormat: dateTimeFormat, hours: timeinfo.tm_hour, minutes: timeinfo.tm_min), false)
} else {
return (strings.LastSeen_AtDate(stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, year: timeinfo.tm_year, dateTimeFormat: dateTimeFormat)).string, false)
}
}
}
case .recently:
let activeUntil = presence.lastActivity + 30
if activeUntil >= timestamp {
return (strings.Presence_online, true)
} else {
return (strings.LastSeen_Lately, false)
}
case .lastWeek:
return (strings.LastSeen_WithinAWeek, false)
case .lastMonth:
return (strings.LastSeen_WithinAMonth, false)
case .longTimeAgo:
return (strings.LastSeen_ALongTimeAgo, false)
}
}
public func peerStatusExpirationString(statusTimestamp: Int32, relativeTo timestamp: Int32, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat) -> String {
let difference = max(statusTimestamp - timestamp, 60)
if difference < 60 * 60 {
return strings.PeerStatusExpiration_Minutes(Int32(round(Double(difference) / Double(60))))
} else if difference < 24 * 60 * 60 {
return strings.PeerStatusExpiration_Hours(Int32(round(Double(difference) / Double(60 * 60))))
} else {
var t: time_t = time_t(statusTimestamp)
var timeinfo: tm = tm()
localtime_r(&t, &timeinfo)
var now: time_t = time_t(timestamp)
var timeinfoNow: tm = tm()
localtime_r(&now, &timeinfoNow)
let dayDifference = timeinfo.tm_yday - timeinfoNow.tm_yday
if dayDifference == 1 {
return strings.PeerStatusExpiration_TomorrowAt(stringForShortTimestamp(hours: timeinfo.tm_hour, minutes: timeinfo.tm_min, dateTimeFormat: dateTimeFormat)).string
} else {
return strings.PeerStatusExpiration_AtDate(stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, dateTimeFormat: dateTimeFormat)).string
}
}
}
public func userPresenceStringRefreshTimeout(_ presence: TelegramUserPresence, relativeTo timestamp: Int32) -> Double {
switch presence.status {
case let .present(statusTimestamp):
if statusTimestamp >= timestamp {
return Double(statusTimestamp - timestamp)
} else {
let difference = timestamp - statusTimestamp
if difference < 30 {
return Double((30 - difference) + 1)
} else if difference < 60 * 60 {
return Double((difference % 60) + 1)
} else {
return Double.infinity
}
}
case .recently:
let activeUntil = presence.lastActivity + 30
if activeUntil >= timestamp {
return Double(activeUntil - timestamp + 1)
} else {
return Double.infinity
}
case .none, .lastWeek, .lastMonth:
return Double.infinity
}
}
public func stringForRemainingMuteInterval(strings: PresentationStrings, muteInterval value: Int32) -> String {
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
let value = max(1 * 60, value - timestamp)
if value <= 1 * 60 * 60 {
return strings.MuteExpires_Minutes(Int32(round(Float(value) / 60)))
} else if value <= 24 * 60 * 60 {
return strings.MuteExpires_Hours(Int32(round(Float(value) / (60 * 60))))
} else {
return strings.MuteExpires_Days(Int32(round(Float(value) / (24 * 60 * 60))))
}
}
public func stringForIntervalSinceUpdateAction(strings: PresentationStrings, value: Int32) -> String {
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
let value = max(1 * 60, timestamp - value)
if value <= 1 * 60 * 60 {
return strings.Chat_NonContactUser_UpdatedMinutes(Int32(round(Float(value) / 60)))
} else if value <= 24 * 60 * 60 {
return strings.Chat_NonContactUser_UpdatedHours(Int32(round(Float(value) / (60 * 60))))
} else {
return strings.Chat_NonContactUser_UpdatedDays(Int32(round(Float(value) / (24 * 60 * 60))))
}
}
public func stringForGiftUpgradeTimestamp(strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, timestamp: Int32) -> String {
var t: time_t = time_t(timestamp)
var timeinfo: tm = tm()
localtime_r(&t, &timeinfo)
let time = stringForShortTimestamp(hours: timeinfo.tm_hour, minutes: timeinfo.tm_min, dateTimeFormat: dateTimeFormat)
let date = strings.Date_ChatDateHeader(monthAtIndex(Int(timeinfo.tm_mon), strings: strings), "\(timeinfo.tm_mday)").string
return "\(time), \(date)"
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,18 @@
import Foundation
public func stringForDuration(_ duration: Int32, position: Int32? = nil) -> String {
var duration = duration
if let position = position {
duration = max(0, duration - position)
}
let hours = duration / 3600
let minutes = duration / 60 % 60
let seconds = duration % 60
let durationString: String
if hours > 0 {
durationString = String(format: "%d:%02d:%02d", hours, minutes, seconds)
} else {
durationString = String(format: "%d:%02d", minutes, seconds)
}
return durationString
}
@@ -0,0 +1,144 @@
import Foundation
import UIKit
import TelegramCore
import TelegramPresentationData
let walletAddressLength: Int = 48
public func formatTonAddress(_ address: String) -> String {
var address = address
address.insert("\n", at: address.index(address.startIndex, offsetBy: address.count / 2))
return address
}
public func convertStarsToTon(_ amount: StarsAmount, tonUsdRate: Double, starsUsdRate: Double) -> Int64 {
let usdRate = starsUsdRate / 1000.0 / 100.0
let usdValue = Double(amount.value) * usdRate
let tonValue = usdValue / tonUsdRate * 1000000000.0
return Int64(tonValue)
}
public func convertTonToStars(_ amount: StarsAmount, tonUsdRate: Double, starsUsdRate: Double) -> Int64 {
let usdRate = starsUsdRate / 1000.0 / 100.0
let usdValue = Double(amount.value) / 1000000000 * tonUsdRate
let starsValue = usdValue / usdRate
return Int64(starsValue)
}
public func formatTonUsdValue(_ value: Int64, divide: Bool = true, rate: Double = 1.0, dateTimeFormat: PresentationDateTimeFormat) -> String {
let decimalSeparator = dateTimeFormat.decimalSeparator
let normalizedValue: Double = divide ? Double(value) / 1000000000 : Double(value)
var formattedValue = String(format: "%0.2f", normalizedValue * rate)
formattedValue = formattedValue.replacingOccurrences(of: ".", with: decimalSeparator)
if let dotIndex = formattedValue.firstIndex(of: decimalSeparator.first!) {
let integerPartString = formattedValue[..<dotIndex]
if let integerPart = Int32(integerPartString) {
let modifiedIntegerPart = presentationStringsFormattedNumber(integerPart, dateTimeFormat.groupingSeparator)
let resultString = "$\(modifiedIntegerPart)\(formattedValue[dotIndex...])"
return resultString
}
}
return "$\(formattedValue)"
}
public func formatTonAmountText(_ value: Int64, dateTimeFormat: PresentationDateTimeFormat, showPlus: Bool = false, maxDecimalPositions: Int? = 2) -> String {
var balanceText = "\(abs(value))"
while balanceText.count < 10 {
balanceText.insert("0", at: balanceText.startIndex)
}
balanceText.insert(contentsOf: dateTimeFormat.decimalSeparator, at: balanceText.index(balanceText.endIndex, offsetBy: -9))
while true {
if balanceText.hasSuffix("0") {
if balanceText.hasSuffix("\(dateTimeFormat.decimalSeparator)0") {
balanceText.removeLast()
balanceText.removeLast()
break
} else {
balanceText.removeLast()
}
} else {
break
}
}
if let dotIndex = balanceText.range(of: dateTimeFormat.decimalSeparator) {
if let maxDecimalPositions {
if let endIndex = balanceText.index(dotIndex.upperBound, offsetBy: maxDecimalPositions, limitedBy: balanceText.endIndex) {
balanceText = String(balanceText[balanceText.startIndex..<endIndex])
} else {
balanceText = String(balanceText[balanceText.startIndex..<balanceText.endIndex])
}
}
let integerPartString = balanceText[..<dotIndex.lowerBound]
if let integerPart = Int32(integerPartString) {
let modifiedIntegerPart = presentationStringsFormattedNumber(integerPart, dateTimeFormat.groupingSeparator)
var resultString = "\(modifiedIntegerPart)\(balanceText[dotIndex.lowerBound...])"
if value < 0 {
resultString.insert("-", at: resultString.startIndex)
} else if showPlus {
resultString.insert("+", at: resultString.startIndex)
}
return resultString
}
} else if let integerPart = Int32(balanceText) {
balanceText = presentationStringsFormattedNumber(integerPart, dateTimeFormat.groupingSeparator)
}
if value < 0 {
balanceText.insert("-", at: balanceText.startIndex)
} else if showPlus {
balanceText.insert("+", at: balanceText.startIndex)
}
return balanceText
}
public func formatStarsAmountText(_ amount: StarsAmount, dateTimeFormat: PresentationDateTimeFormat, showPlus: Bool = false) -> String {
var balanceText = presentationStringsFormattedNumber(Int32(clamping: amount.value), dateTimeFormat.groupingSeparator)
let fraction = abs(Double(amount.nanos)) / 10e6
if fraction > 0.0 {
balanceText.append(dateTimeFormat.decimalSeparator)
balanceText.append("\(Int32(fraction))")
}
if amount.value < 0 {
} else if showPlus {
balanceText.insert("+", at: balanceText.startIndex)
}
return balanceText
}
public func formatCurrencyAmountText(_ amount: CurrencyAmount, dateTimeFormat: PresentationDateTimeFormat, showPlus: Bool = false, maxDecimalPositions: Int? = 2) -> String {
switch amount.currency {
case .stars:
return formatStarsAmountText(amount.amount, dateTimeFormat: dateTimeFormat, showPlus: showPlus)
case .ton:
return formatTonAmountText(amount.amount.value, dateTimeFormat: dateTimeFormat, showPlus: showPlus, maxDecimalPositions: maxDecimalPositions)
}
}
private let invalidAddressCharacters = CharacterSet(charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_=").inverted
public func isValidTonAddress(_ address: String, exactLength: Bool = false) -> Bool {
if address.count > walletAddressLength || address.rangeOfCharacter(from: invalidAddressCharacters) != nil {
return false
}
if exactLength && address.count != walletAddressLength {
return false
}
return true
}
public func tonAmountAttributedString(_ string: String, integralFont: UIFont, fractionalFont: UIFont, color: UIColor, decimalSeparator: String) -> NSAttributedString {
let result = NSMutableAttributedString()
if let range = string.range(of: decimalSeparator) {
let integralPart = String(string[..<range.lowerBound])
let fractionalPart = String(string[range.lowerBound...])
result.append(NSAttributedString(string: integralPart, font: integralFont, textColor: color))
result.append(NSAttributedString(string: fractionalPart, font: fractionalFont, textColor: color))
} else {
result.append(NSAttributedString(string: string, font: integralFont, textColor: color))
}
return result
}
@@ -0,0 +1,47 @@
import Foundation
private enum TemperatureUnit {
case celsius
case fahrenheit
var suffix: String {
switch self {
case .celsius:
return "°C"
case .fahrenheit:
return "°F"
}
}
}
private var cachedTemperatureUnit: TemperatureUnit?
private func currentTemperatureUnit() -> TemperatureUnit {
if let cachedTemperatureUnit {
return cachedTemperatureUnit
}
let temperatureFormatter = MeasurementFormatter()
temperatureFormatter.locale = Locale.current
let fahrenheitMeasurement = Measurement(value: 0, unit: UnitTemperature.fahrenheit)
let fahrenheitString = temperatureFormatter.string(from: fahrenheitMeasurement)
var temperatureUnit: TemperatureUnit = .celsius
if fahrenheitString.contains("F") || fahrenheitString.contains("Fahrenheit") {
temperatureUnit = .fahrenheit
}
cachedTemperatureUnit = temperatureUnit
return temperatureUnit
}
private var formatter: MeasurementFormatter = {
let formatter = MeasurementFormatter()
formatter.locale = Locale.current
formatter.unitStyle = .short
formatter.numberFormatter.maximumFractionDigits = 0
return formatter
}()
public func stringForTemperature(_ value: Double) -> String {
let valueString = formatter.string(from: Measurement(value: value, unit: UnitTemperature.celsius)).trimmingCharacters(in: CharacterSet(charactersIn: "0123456789-,.").inverted)
return valueString + currentTemperatureUnit().suffix
}