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
@@ -0,0 +1,30 @@
import Foundation
import UIKit
import Markdown
public func addAttributesToStringWithRanges(_ stringWithRanges: (String, [(Int, NSRange)]), body: MarkdownAttributeSet, argumentAttributes: [Int: MarkdownAttributeSet], textAlignment: NSTextAlignment = .natural) -> NSAttributedString {
let result = NSMutableAttributedString()
var bodyAttributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: body.font, NSAttributedString.Key.foregroundColor: body.textColor, NSAttributedString.Key.paragraphStyle: paragraphStyleWithAlignment(textAlignment)]
if !body.additionalAttributes.isEmpty {
for (key, value) in body.additionalAttributes {
bodyAttributes[NSAttributedString.Key(rawValue: key)] = value
}
}
result.append(NSAttributedString(string: stringWithRanges.0, attributes: bodyAttributes))
for (index, range) in stringWithRanges.1 {
if let attributes = argumentAttributes[index] {
var argumentAttributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: attributes.font, NSAttributedString.Key.foregroundColor: attributes.textColor, NSAttributedString.Key.paragraphStyle: paragraphStyleWithAlignment(textAlignment)]
if !attributes.additionalAttributes.isEmpty {
for (key, value) in attributes.additionalAttributes {
argumentAttributes[NSAttributedString.Key(rawValue: key)] = value
}
}
result.addAttributes(argumentAttributes, range: range)
}
}
return result
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,77 @@
import Foundation
import TelegramCore
private struct PercentCounterItem: Comparable {
var index: Int = 0
var percent: Int = 0
var remainder: Int = 0
static func <(lhs: PercentCounterItem, rhs: PercentCounterItem) -> Bool {
if lhs.remainder > rhs.remainder {
return true
} else if lhs.remainder < rhs.remainder {
return false
}
return lhs.percent < rhs.percent
}
}
private func adjustPercentCount(_ items: [PercentCounterItem], left: Int) -> [PercentCounterItem] {
var left = left
var items = items.sorted(by: <)
var i:Int = 0
while i != items.count {
let item = items[i]
var j = i + 1
loop: while j != items.count {
if items[j].percent != item.percent || items[j].remainder != item.remainder {
break loop
}
j += 1
}
if items[i].remainder == 0 {
break
}
let equal = j - i
if equal <= left {
left -= equal
while i != j {
items[i].percent += 1
i += 1
}
} else {
i = j
}
}
return items
}
public func countNicePercent(votes: [Int], total: Int) -> [Int] {
var result: [Int] = []
var items: [PercentCounterItem] = []
for _ in votes {
result.append(0)
items.append(PercentCounterItem())
}
let count = votes.count
var left:Int = 100
for i in 0 ..< votes.count {
let votes = votes[i]
items[i].index = i
items[i].percent = Int((Float(votes) * 100) / Float(total))
items[i].remainder = (votes * 100) - (items[i].percent * total)
left -= items[i].percent
}
if left > 0 && left <= count {
items = adjustPercentCount(items, left: left)
}
for item in items {
result[item.index] = item.percent
}
return result
}
@@ -0,0 +1,212 @@
import SGSimpleSettings
import Foundation
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
public func stringForEntityFormattedDate(timestamp: Int32, format: MessageTextEntityType.DateTimeFormat, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat) -> String {
switch format {
case .relative:
let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
let value = currentTimestamp - timestamp
if value > 0 {
if value < 60 {
return strings.FormattedDate_SecondsAgo(value)
} else if value <= 1 * 60 * 60 {
return strings.FormattedDate_MinutesAgo(Int32(round(Float(value) / 60)))
} else if value <= 24 * 60 * 60 {
return strings.FormattedDate_HoursAgo(Int32(round(Float(value) / (60 * 60))))
} else {
return strings.FormattedDate_DaysAgo(Int32(round(Float(value) / (24 * 60 * 60))))
}
} else {
let value = abs(value)
if value < 60 {
return strings.FormattedDate_InSeconds(value)
} else if value <= 1 * 60 * 60 {
return strings.FormattedDate_InMinutes(Int32(round(Float(value) / 60)))
} else if value <= 24 * 60 * 60 {
return strings.FormattedDate_InHours(Int32(round(Float(value) / (60 * 60))))
} else {
return strings.FormattedDate_InDays(Int32(round(Float(value) / (24 * 60 * 60))))
}
}
case let .full(timeFormat, dateFormat, dayOfWeek):
let _ = dayOfWeek
var string = ""
if dayOfWeek {
var t: time_t = Int(timestamp)
var timeinfo = tm()
localtime_r(&t, &timeinfo);
string = stringForDayOfWeek(strings: strings, day: timeinfo.tm_wday, short: dateFormat == .short)
}
if let dateFormat {
if !string.isEmpty {
string += " "
}
switch dateFormat {
case .short:
string += stringForShortDate(timestamp: timestamp, strings: strings, dateTimeFormat: dateTimeFormat)
case .long:
string += stringForFullDate(timestamp: timestamp, strings: strings, dateTimeFormat: dateTimeFormat)
}
}
if let timeFormat {
let timeString: String
switch timeFormat {
case .short:
timeString = stringForMessageTimestamp(timestamp: timestamp, dateTimeFormat: dateTimeFormat)
case .long:
timeString = stringForMessageTimestamp(timestamp: timestamp, dateTimeFormat: dateTimeFormat, withSeconds: true)
}
if !string.isEmpty {
string = strings.Time_AtPreciseDate(string, timeString).string
} else {
string = timeString
}
}
return string
}
}
private func stringForDayOfWeek(strings: PresentationStrings, day: Int32, short: Bool) -> String {
switch day {
case 0:
return short ? strings.Weekday_ShortSunday : strings.Weekday_Sunday
case 1:
return short ? strings.Weekday_ShortMonday : strings.Weekday_Monday
case 2:
return short ? strings.Weekday_ShortTuesday : strings.Weekday_Tuesday
case 3:
return short ? strings.Weekday_ShortWednesday : strings.Weekday_Wednesday
case 4:
return short ? strings.Weekday_ShortThursday : strings.Weekday_Thursday
case 5:
return short ? strings.Weekday_ShortFriday : strings.Weekday_Friday
case 6:
return short ? strings.Weekday_ShortSaturday : strings.Weekday_Saturday
default:
return ""
}
}
public func stringForShortTimestamp(hours: Int32, minutes: Int32, seconds: Int32? = nil, dateTimeFormat: PresentationDateTimeFormat, formatAsPlainText: Bool = false) -> String {
switch dateTimeFormat.timeFormat {
case .regular:
let hourString: String
if hours == 0 {
hourString = "12"
} else if hours > 12 {
hourString = "\(hours - 12)"
} else {
hourString = "\(hours)"
}
let periodString: String
if hours >= 12 {
periodString = "PM"
} else {
periodString = "AM"
}
let spaceCharacter: String
if formatAsPlainText {
spaceCharacter = " "
} else {
spaceCharacter = "\u{00a0}"
}
let minuteString = String(format: "%02d", arguments: [Int(minutes)])
if let seconds {
let secondString = String(format: "%02d", arguments: [Int(seconds)])
return "\(hourString):\(minuteString):\(secondString)\(spaceCharacter)\(periodString)"
} else {
return "\(hourString):\(minuteString)\(spaceCharacter)\(periodString)"
}
case .military:
if let seconds {
return String(format: "%02d:%02d:%02d", arguments: [Int(hours), Int(minutes), Int(seconds)])
} else {
return String(format: "%02d:%02d", arguments: [Int(hours), Int(minutes)])
}
}
}
public func stringForMessageTimestamp(timestamp: Int32, dateTimeFormat: PresentationDateTimeFormat, withSeconds: Bool = false, local: Bool = true) -> String {
var t = Int(timestamp)
var timeinfo = tm()
if local {
localtime_r(&t, &timeinfo)
} else {
gmtime_r(&t, &timeinfo)
}
var withSeconds = withSeconds
// MARK: Swiftgram
if SGSimpleSettings.shared.secondsInMessages { withSeconds = true }
//
return stringForShortTimestamp(hours: timeinfo.tm_hour, minutes: timeinfo.tm_min, seconds: withSeconds ? timeinfo.tm_sec : nil, dateTimeFormat: dateTimeFormat)
}
public func stringForShortDate(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)
}
return dateString
}
private 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 monthFormat: (String, String) -> PresentationStrings.FormattedString
switch timeinfo.tm_mon + 1 {
case 1:
monthFormat = strings.FormattedDate_LongDate_m1
case 2:
monthFormat = strings.FormattedDate_LongDate_m2
case 3:
monthFormat = strings.FormattedDate_LongDate_m3
case 4:
monthFormat = strings.FormattedDate_LongDate_m4
case 5:
monthFormat = strings.FormattedDate_LongDate_m5
case 6:
monthFormat = strings.FormattedDate_LongDate_m6
case 7:
monthFormat = strings.FormattedDate_LongDate_m7
case 8:
monthFormat = strings.FormattedDate_LongDate_m8
case 9:
monthFormat = strings.FormattedDate_LongDate_m9
case 10:
monthFormat = strings.FormattedDate_LongDate_m10
case 11:
monthFormat = strings.FormattedDate_LongDate_m11
case 12:
monthFormat = strings.FormattedDate_LongDate_m12
default:
return ""
}
return monthFormat(dayString, yearString).string
}
@@ -0,0 +1,500 @@
import Foundation
import UIKit
import TelegramCore
import Emoji
private let whitelistedHosts: Set<String> = Set([
"telegram.org",
"t.me",
"telegram.me",
"telegra.ph",
"telesco.pe",
"fragment.com"
])
private let dataDetector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType([.link]).rawValue)
private let dataAndPhoneNumberDetector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType([.link, .phoneNumber]).rawValue)
private let phoneNumberDetector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType([.phoneNumber]).rawValue)
private let validHashtagSet: CharacterSet = {
var set = CharacterSet.alphanumerics
set.insert("_")
return set
}()
private let validIdentifierSet: CharacterSet = {
var set = CharacterSet(charactersIn: "a".unicodeScalars.first! ... "z".unicodeScalars.first!)
set.insert(charactersIn: "A".unicodeScalars.first! ... "Z".unicodeScalars.first!)
set.insert(charactersIn: "0".unicodeScalars.first! ... "9".unicodeScalars.first!)
set.insert("_")
return set
}()
private let identifierDelimiterSet: CharacterSet = {
var set = CharacterSet.punctuationCharacters
set.formUnion(CharacterSet.whitespacesAndNewlines)
set.insert("|")
set.insert("/")
return set
}()
private let externalIdentifierDelimiterSet: CharacterSet = {
var set = identifierDelimiterSet
set.remove(".")
return set
}()
private let timecodeDelimiterSet: CharacterSet = {
var set = CharacterSet.punctuationCharacters
set.formUnion(CharacterSet.whitespacesAndNewlines)
set.remove(":")
return set
}()
private let validTimecodeSet: CharacterSet = {
var set = CharacterSet(charactersIn: "0".unicodeScalars.first! ... "9".unicodeScalars.first!)
set.insert(":")
return set
}()
private let validTimecodePreviousSet: CharacterSet = {
var set = CharacterSet.whitespacesAndNewlines
set.insert("(")
set.insert("[")
return set
}()
public struct ApplicationSpecificEntityType {
public static let Timecode: Int32 = 1
public static let Button: Int32 = 2
}
private enum CurrentEntityType {
case command
case mention
case hashtag
case phoneNumber
case timecode
var type: EnabledEntityTypes {
switch self {
case .command:
return .command
case .mention:
return .mention
case .hashtag:
return .hashtag
case .phoneNumber:
return .phoneNumber
case .timecode:
return .timecode
}
}
}
public struct EnabledEntityTypes: OptionSet {
public var rawValue: Int32
public init(rawValue: Int32) {
self.rawValue = rawValue
}
public static let command = EnabledEntityTypes(rawValue: 1 << 0)
public static let mention = EnabledEntityTypes(rawValue: 1 << 1)
public static let hashtag = EnabledEntityTypes(rawValue: 1 << 2)
public static let allUrl = EnabledEntityTypes(rawValue: 1 << 3)
public static let phoneNumber = EnabledEntityTypes(rawValue: 1 << 4)
public static let timecode = EnabledEntityTypes(rawValue: 1 << 5)
public static let external = EnabledEntityTypes(rawValue: 1 << 6)
public static let internalUrl = EnabledEntityTypes(rawValue: 1 << 7)
public static let all: EnabledEntityTypes = [.command, .mention, .hashtag, .allUrl, .phoneNumber]
}
private func commitEntity(_ utf16: String.UTF16View, _ type: CurrentEntityType, _ range: Range<String.UTF16View.Index>, _ enabledTypes: EnabledEntityTypes, _ entities: inout [MessageTextEntity], mediaDuration: Double? = nil) {
if !enabledTypes.contains(type.type) {
return
}
let indexRange: Range<Int> = utf16.distance(from: utf16.startIndex, to: range.lowerBound) ..< utf16.distance(from: utf16.startIndex, to: range.upperBound)
var overlaps = false
for entity in entities {
if entity.range.overlaps(indexRange) {
if case .Spoiler = entity.type {
} else {
overlaps = true
break
}
}
}
if !overlaps {
let entityType: MessageTextEntityType
switch type {
case .command:
entityType = .BotCommand
case .mention:
entityType = .Mention
case .hashtag:
entityType = .Hashtag
case .phoneNumber:
entityType = .PhoneNumber
case .timecode:
entityType = .Custom(type: ApplicationSpecificEntityType.Timecode)
}
if case .timecode = type {
if let mediaDuration = mediaDuration, let timecode = parseTimecodeString(String(utf16[range])), timecode <= mediaDuration {
entities.append(MessageTextEntity(range: indexRange, type: entityType))
}
} else {
entities.append(MessageTextEntity(range: indexRange, type: entityType))
}
}
}
public func generateChatInputTextEntities(_ text: NSAttributedString, maxAnimatedEmojisInText: Int? = nil, generateLinks: Bool = false) -> [MessageTextEntity] {
var entities: [MessageTextEntity] = []
text.enumerateAttributes(in: NSRange(location: 0, length: text.length), options: [], using: { attributes, range, _ in
for (key, value) in attributes {
if key == ChatTextInputAttributes.bold {
entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .Bold))
} else if key == ChatTextInputAttributes.italic {
entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .Italic))
} else if key == ChatTextInputAttributes.monospace {
entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .Code))
} else if key == ChatTextInputAttributes.strikethrough {
entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .Strikethrough))
} else if key == ChatTextInputAttributes.underline {
entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .Underline))
} else if key == ChatTextInputAttributes.textMention, let value = value as? ChatTextInputTextMentionAttribute {
entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .TextMention(peerId: value.peerId)))
} else if key == ChatTextInputAttributes.textUrl, let value = value as? ChatTextInputTextUrlAttribute {
entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .TextUrl(url: value.url)))
} else if key == ChatTextInputAttributes.date, let value = value as? ChatTextInputTextDateAttribute {
entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .FormattedDate(format: nil, date: value.date)))
} else if key == ChatTextInputAttributes.spoiler {
entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .Spoiler))
} else if key == ChatTextInputAttributes.customEmoji, let value = value as? ChatTextInputTextCustomEmojiAttribute {
entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .CustomEmoji(stickerPack: nil, fileId: value.fileId)))
} else if key == ChatTextInputAttributes.block, let value = value as? ChatTextInputTextQuoteAttribute {
switch value.kind {
case .quote:
entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .BlockQuote(isCollapsed: value.isCollapsed)))
case let .code(language):
entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .Pre(language: language)))
}
}
}
})
if generateLinks {
for entity in generateTextEntities(text.string, enabledTypes: .allUrl) {
if case .Url = entity.type {
entities.append(entity)
}
}
}
while true {
var hadReductions = false
scan: for i in 0 ..< entities.count {
if case .BlockQuote = entities[i].type {
inner: for j in 0 ..< entities.count {
if j == i {
continue inner
}
if case .BlockQuote = entities[j].type {
if entities[i].range.upperBound == entities[j].range.lowerBound || entities[i].range.lowerBound == entities[j].range.upperBound {
entities[i].range = min(entities[i].range.lowerBound, entities[j].range.lowerBound) ..< max(entities[i].range.upperBound, entities[j].range.upperBound)
entities.remove(at: j)
hadReductions = true
break scan
}
}
}
break scan
}
}
if !hadReductions {
break
}
}
while true {
var hadReductions = false
scan: for i in 0 ..< entities.count {
if case let .Pre(language) = entities[i].type {
inner: for j in 0 ..< entities.count {
if j == i {
continue inner
}
if case .Pre(language) = entities[j].type {
if entities[i].range.upperBound == entities[j].range.lowerBound || entities[i].range.lowerBound == entities[j].range.upperBound {
entities[i].range = min(entities[i].range.lowerBound, entities[j].range.lowerBound) ..< max(entities[i].range.upperBound, entities[j].range.upperBound)
entities.remove(at: j)
hadReductions = true
break scan
}
}
}
break scan
}
}
if !hadReductions {
break
}
}
return entities
}
public func generateTextEntities(_ text: String, enabledTypes: EnabledEntityTypes, currentEntities: [MessageTextEntity] = []) -> [MessageTextEntity] {
var entities: [MessageTextEntity] = currentEntities
let utf16 = text.utf16
var detector: NSDataDetector?
if enabledTypes.contains(.phoneNumber) && (enabledTypes.contains(.allUrl) || enabledTypes.contains(.internalUrl)) {
detector = dataAndPhoneNumberDetector
} else if enabledTypes.contains(.phoneNumber) {
detector = phoneNumberDetector
} else if enabledTypes.contains(.allUrl) || enabledTypes.contains(.internalUrl) {
detector = dataDetector
}
let delimiterSet = enabledTypes.contains(.external) ? externalIdentifierDelimiterSet : identifierDelimiterSet
if let detector = detector {
detector.enumerateMatches(in: text, options: [], range: NSMakeRange(0, utf16.count), using: { result, _, _ in
if let result = result {
if [NSTextCheckingResult.CheckingType.link, NSTextCheckingResult.CheckingType.phoneNumber].contains(result.resultType) {
let lowerBound = utf16.index(utf16.startIndex, offsetBy: result.range.location).samePosition(in: text)
let upperBound = utf16.index(utf16.startIndex, offsetBy: result.range.location + result.range.length).samePosition(in: text)
if let lowerBound = lowerBound, let upperBound = upperBound {
let type: MessageTextEntityType
if result.resultType == NSTextCheckingResult.CheckingType.link {
if !enabledTypes.contains(.allUrl) && enabledTypes.contains(.internalUrl) {
guard let url = result.url else {
return
}
if url.scheme != "tg" {
guard var host = url.host?.lowercased() else {
return
}
let www = "www."
if host.hasPrefix(www) {
host.removeFirst(www.count)
}
if whitelistedHosts.contains(host) {
} else {
return
}
}
}
type = .Url
} else {
type = .PhoneNumber
}
entities.append(MessageTextEntity(range: utf16.distance(from: text.startIndex, to: lowerBound) ..< utf16.distance(from: text.startIndex, to: upperBound), type: type))
}
}
}
})
}
var index = utf16.startIndex
var currentEntity: (CurrentEntityType, Range<String.UTF16View.Index>)?
var previousScalar: UnicodeScalar?
while index != utf16.endIndex {
let c = utf16[index]
let scalar = UnicodeScalar(c)
var notFound = true
if let scalar = scalar {
if scalar == "/" {
notFound = false
if let previousScalar = previousScalar, !delimiterSet.contains(previousScalar) {
if let entity = currentEntity, entity.0 == .command {
currentEntity = nil
}
} else {
if let (type, range) = currentEntity {
commitEntity(utf16, type, range, enabledTypes, &entities)
}
currentEntity = (.command, index ..< index)
}
} else if scalar == "@" {
notFound = false
if let (type, range) = currentEntity {
if case .command = type {
currentEntity = (type, range.lowerBound ..< utf16.index(after: index))
} else {
commitEntity(utf16, type, range, enabledTypes, &entities)
currentEntity = (.mention, index ..< index)
}
} else {
currentEntity = (.mention, index ..< index)
}
} else if scalar == "#" {
notFound = false
if let (type, range) = currentEntity {
commitEntity(utf16, type, range, enabledTypes, &entities)
}
currentEntity = (.hashtag, index ..< index)
}
if notFound {
if let (type, range) = currentEntity {
switch type {
case .command, .mention:
if validIdentifierSet.contains(scalar) {
currentEntity = (type, range.lowerBound ..< utf16.index(after: index))
} else if delimiterSet.contains(scalar) {
if let (type, range) = currentEntity {
commitEntity(utf16, type, range, enabledTypes, &entities)
}
currentEntity = nil
}
case .hashtag:
if validHashtagSet.contains(scalar) {
currentEntity = (type, range.lowerBound ..< utf16.index(after: index))
} else if delimiterSet.contains(scalar) {
if let (type, range) = currentEntity {
commitEntity(utf16, type, range, enabledTypes, &entities)
}
currentEntity = nil
}
default:
break
}
}
}
}
index = utf16.index(after: index)
previousScalar = scalar
}
if let (type, range) = currentEntity {
commitEntity(utf16, type, range, enabledTypes, &entities)
}
return entities
}
public func addLocallyGeneratedEntities(_ text: String, enabledTypes: EnabledEntityTypes, entities: [MessageTextEntity], mediaDuration: Double? = nil) -> [MessageTextEntity]? {
var resultEntities = entities
var hasDigits = false
var hasColons = false
let detectPhoneNumbers = enabledTypes.contains(.phoneNumber)
let detectTimecodes = enabledTypes.contains(.timecode)
if detectPhoneNumbers || detectTimecodes {
loop: for c in text.utf16 {
if let scalar = UnicodeScalar(c) {
if scalar >= "0" && scalar <= "9" {
hasDigits = true
if !detectTimecodes || hasColons {
break loop
}
} else if scalar == ":" {
hasColons = true
if !detectPhoneNumbers || hasDigits {
break loop
}
}
}
}
}
if hasDigits || hasColons {
if let phoneNumberDetector = phoneNumberDetector, detectPhoneNumbers {
let utf16 = text.utf16
phoneNumberDetector.enumerateMatches(in: text, options: [], range: NSMakeRange(0, utf16.count), using: { result, _, _ in
if let result = result {
if result.resultType == NSTextCheckingResult.CheckingType.phoneNumber {
let lowerBound = utf16.index(utf16.startIndex, offsetBy: result.range.location).samePosition(in: text)
let upperBound = utf16.index(utf16.startIndex, offsetBy: result.range.location + result.range.length).samePosition(in: text)
if let lowerBound = lowerBound, let upperBound = upperBound {
commitEntity(utf16, .phoneNumber, lowerBound ..< upperBound, enabledTypes, &resultEntities)
}
}
}
})
}
if hasColons && detectTimecodes {
let utf16 = text.utf16
let delimiterSet = timecodeDelimiterSet
var index = utf16.startIndex
var currentEntity: (CurrentEntityType, Range<String.UTF16View.Index>)?
var previousScalar: UnicodeScalar?
while index != utf16.endIndex {
let c = utf16[index]
let scalar = UnicodeScalar(c)
var notFound = true
if let scalar = scalar {
if validTimecodeSet.contains(scalar) {
notFound = false
if let (type, range) = currentEntity, type == .timecode {
currentEntity = (.timecode, range.lowerBound ..< utf16.index(after: index))
} else if previousScalar == nil || validTimecodePreviousSet.contains(previousScalar!) {
currentEntity = (.timecode, index ..< index)
}
}
if notFound {
if let (type, range) = currentEntity {
switch type {
case .timecode:
if delimiterSet.contains(scalar) {
commitEntity(utf16, type, range, enabledTypes, &resultEntities, mediaDuration: mediaDuration)
currentEntity = nil
}
default:
break
}
}
}
}
index = utf16.index(after: index)
previousScalar = scalar
}
if let (type, range) = currentEntity {
commitEntity(utf16, type, range, enabledTypes, &resultEntities, mediaDuration: mediaDuration)
}
}
}
if resultEntities.count != entities.count {
return resultEntities
} else {
return nil
}
}
public func parseTimecodeString(_ string: String?) -> Double? {
if let string = string, string.rangeOfCharacter(from: validTimecodeSet.inverted) == nil {
let components = string.components(separatedBy: ":")
if components.count > 1 && components.count <= 3 {
if components.count == 3 {
if let hours = Int(components[0]), let minutes = Int(components[1]), let seconds = Int(components[2]) {
if hours >= 0 && hours < 48 && minutes >= 0 && minutes < 60 && seconds >= 0 && seconds < 60 {
return Double(seconds) + Double(minutes) * 60.0 + Double(hours) * 60.0 * 60.0
}
}
} else if components.count == 2 {
if let minutes = Int(components[0]), let seconds = Int(components[1]) {
if minutes >= 0 && minutes < 60 && seconds >= 0 && seconds < 60 {
return Double(seconds) + Double(minutes) * 60.0
}
}
}
}
}
return nil
}
@@ -0,0 +1,17 @@
import Foundation
public extension String {
func rightJustified(width: Int, pad: String = " ", truncate: Bool = false) -> String {
guard width > count else {
return truncate ? String(suffix(width)) : self
}
return String(repeating: pad, count: width - count) + self
}
func leftJustified(width: Int, pad: String = " ", truncate: Bool = false) -> String {
guard width > count else {
return truncate ? String(prefix(width)) : self
}
return self + String(repeating: pad, count: width - count)
}
}
@@ -0,0 +1,667 @@
import Foundation
import UIKit
import Postbox
import TelegramCore
import Display
import libprisma
import SwiftSignalKit
import TelegramPresentationData
public func chatInputStateStringWithAppliedEntities(_ text: String, entities: [MessageTextEntity]) -> NSAttributedString {
var nsString: NSString?
let string = NSMutableAttributedString(string: text)
var skipEntity = false
let stringLength = string.length
for i in 0 ..< entities.count {
if skipEntity {
skipEntity = false
continue
}
let entity = entities[i]
var range = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)
if nsString == nil {
nsString = text as NSString
}
if range.location >= stringLength {
continue
}
if range.location + range.length > stringLength {
range.length = stringLength - range.location
}
switch entity.type {
case .Url, .Email, .PhoneNumber, .Mention, .Hashtag, .BotCommand:
break
case .Bold:
string.addAttribute(ChatTextInputAttributes.bold, value: true as NSNumber, range: range)
case .Italic:
string.addAttribute(ChatTextInputAttributes.italic, value: true as NSNumber, range: range)
case let .TextMention(peerId):
string.addAttribute(ChatTextInputAttributes.textMention, value: ChatTextInputTextMentionAttribute(peerId: peerId), range: range)
case let .TextUrl(url):
string.addAttribute(ChatTextInputAttributes.textUrl, value: ChatTextInputTextUrlAttribute(url: url), range: range)
case let .FormattedDate(_, date):
string.addAttribute(ChatTextInputAttributes.date, value: ChatTextInputTextDateAttribute(date: date), range: range)
case .Code:
string.addAttribute(ChatTextInputAttributes.monospace, value: true as NSNumber, range: range)
case .Strikethrough:
string.addAttribute(ChatTextInputAttributes.strikethrough, value: true as NSNumber, range: range)
case .Underline:
string.addAttribute(ChatTextInputAttributes.underline, value: true as NSNumber, range: range)
case .Spoiler:
string.addAttribute(ChatTextInputAttributes.spoiler, value: true as NSNumber, range: range)
case let .CustomEmoji(_, fileId):
string.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: nil), range: range)
case let .Pre(language):
string.addAttribute(ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .code(language: language), isCollapsed: false), range: range)
case let .BlockQuote(isCollapsed):
string.addAttribute(ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .quote, isCollapsed: isCollapsed), range: range)
default:
break
}
}
while true {
var found = false
string.enumerateAttribute(ChatTextInputAttributes.block, in: NSRange(location: 0, length: string.length), using: { value, range, stop in
if let value = value as? ChatTextInputTextQuoteAttribute, value.isCollapsed {
found = true
let blockString = string.attributedSubstring(from: range)
string.replaceCharacters(in: range, with: "")
string.insert(NSAttributedString(string: " ", attributes: [
ChatTextInputAttributes.collapsedBlock: blockString
]), at: range.lowerBound)
stop.pointee = true
}
})
if !found {
break
}
}
return string
}
private let syntaxHighlighter = Syntaxer()
public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], strings: PresentationStrings? = nil, dateTimeFormat: PresentationDateTimeFormat? = nil, baseColor: UIColor, linkColor: UIColor, baseQuoteTintColor: UIColor? = nil, baseQuoteSecondaryTintColor: UIColor? = nil, baseQuoteTertiaryTintColor: UIColor? = nil, codeBlockTitleColor: UIColor? = nil, codeBlockAccentColor: UIColor? = nil, codeBlockBackgroundColor: UIColor? = nil, baseFont: UIFont, linkFont: UIFont, boldFont: UIFont, italicFont: UIFont, boldItalicFont: UIFont, fixedFont: UIFont, blockQuoteFont: UIFont, underlineLinks: Bool = true, external: Bool = false, message: Message?, entityFiles: [MediaId: TelegramMediaFile] = [:], adjustQuoteFontSize: Bool = false, cachedMessageSyntaxHighlight: CachedMessageSyntaxHighlight? = nil, paragraphAlignment: NSTextAlignment? = nil) -> NSAttributedString {
let baseQuoteTintColor = baseQuoteTintColor ?? baseColor
var nsString: NSString?
let baseAttributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: baseFont, NSAttributedString.Key.foregroundColor: baseColor]
let string = NSMutableAttributedString(string: text, attributes: baseAttributes)
var skipEntity = false
var underlineAllLinks = false
if linkColor.argb == baseColor.argb {
underlineAllLinks = true
}
var adjustedRanges: [NSRange?] = []
adjustedRanges.reserveCapacity(entities.count)
var rangeDelta = 0
for entity in entities {
let originalRange = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)
var range = NSRange(location: originalRange.location + rangeDelta, length: originalRange.length)
let stringLength = string.length
if range.location > stringLength {
adjustedRanges.append(nil)
continue
} else if range.location + range.length > stringLength {
range.length = stringLength - range.location
}
switch entity.type {
case let .FormattedDate(format, date):
if let format, let strings, let dateTimeFormat {
let replacement = stringForEntityFormattedDate(timestamp: date, format: format, strings: strings, dateTimeFormat: dateTimeFormat)
let replacementString = NSAttributedString(string: replacement, attributes: baseAttributes)
string.replaceCharacters(in: range, with: replacementString)
let newRange = NSRange(location: range.location, length: (replacement as NSString).length)
adjustedRanges.append(newRange)
rangeDelta += newRange.length - range.length
} else {
adjustedRanges.append(range)
}
default:
adjustedRanges.append(range)
}
}
var fontAttributeMask: [ChatTextFontAttributes] = Array(repeating: [], count: string.length)
let addFontAttributes: (NSRange, ChatTextFontAttributes) -> Void = { range, attributes in
for i in range.lowerBound ..< range.upperBound {
fontAttributeMask[i].formUnion(attributes)
}
}
for i in 0 ..< entities.count {
if skipEntity {
skipEntity = false
continue
}
let entity = entities[i]
guard var range = adjustedRanges[i] else {
continue
}
let stringLength = string.length
if range.location > stringLength {
continue
} else if range.location + range.length > stringLength {
range.length = stringLength - range.location
}
switch entity.type {
case .Url:
string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range)
if underlineLinks {
string.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range)
}
if nsString == nil {
nsString = string.string as NSString
}
string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.URL), value: nsString!.substring(with: range), range: range)
case .Email:
string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range)
if nsString == nil {
nsString = string.string as NSString
}
if underlineLinks && underlineAllLinks {
string.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range)
}
string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.URL), value: "mailto:\(nsString!.substring(with: range))", range: range)
case .PhoneNumber:
string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range)
if nsString == nil {
nsString = string.string as NSString
}
if underlineLinks && underlineAllLinks {
string.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range)
}
string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.URL), value: "tel:\(nsString!.substring(with: range))", range: range)
case let .TextUrl(url):
string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range)
if nsString == nil {
nsString = string.string as NSString
}
if underlineLinks && underlineAllLinks {
string.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range)
}
if external {
string.addAttribute(NSAttributedString.Key.link, value: url, range: range)
} else {
string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.URL), value: url, range: range)
}
case .Bold:
addFontAttributes(range, .bold)
case .Italic:
addFontAttributes(range, .italic)
case .Mention:
string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range)
if underlineLinks && underlineAllLinks {
string.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range)
}
if linkFont !== baseFont {
string.addAttribute(NSAttributedString.Key.font, value: linkFont, range: range)
}
if nsString == nil {
nsString = string.string as NSString
}
string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention), value: nsString!.substring(with: range), range: range)
case .Strikethrough:
string.addAttribute(NSAttributedString.Key.strikethroughStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range)
case .Underline:
string.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range)
case let .TextMention(peerId):
string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range)
if underlineLinks && underlineAllLinks {
string.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range)
}
if linkFont !== baseFont {
string.addAttribute(NSAttributedString.Key.font, value: linkFont, range: range)
}
if nsString == nil {
nsString = string.string as NSString
}
let mention = nsString!.substring(with: range)
string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention), value: TelegramPeerMention(peerId: peerId, mention: mention), range: range)
case .Hashtag:
if nsString == nil {
nsString = string.string as NSString
}
let hashtag = nsString!.substring(with: range)
if i + 1 != entities.count {
if case .Mention = entities[i + 1].type {
guard let nextRange = adjustedRanges[i + 1] else {
break
}
if nextRange.location == range.location + range.length + 1 && nsString!.character(at: range.location + range.length) == 43 {
skipEntity = true
if nextRange.length > 0 {
let peerName: String = nsString!.substring(with: NSRange(location: nextRange.location + 1, length: nextRange.length - 1))
let combinedRange = NSRange(location: range.location, length: nextRange.location + nextRange.length - range.location)
string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: combinedRange)
if linkColor.isEqual(baseColor) {
string.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: combinedRange)
}
string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag), value: TelegramHashtag(peerName: peerName, hashtag: hashtag), range: combinedRange)
}
}
}
}
if !skipEntity {
var hashtagValue = hashtag
var peerNameValue: String?
if hashtagValue.contains("@") {
let components = hashtagValue.components(separatedBy: "@")
if components.count == 2, let firstComponent = components.first, let lastComponent = components.last, !firstComponent.isEmpty && !lastComponent.isEmpty {
hashtagValue = firstComponent
peerNameValue = lastComponent
}
}
string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range)
if underlineLinks && underlineAllLinks {
string.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range)
}
string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag), value: TelegramHashtag(peerName: peerNameValue, hashtag: hashtagValue), range: range)
}
case .BotCommand:
string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range)
if underlineLinks && underlineAllLinks {
string.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range)
}
if nsString == nil {
nsString = string.string as NSString
}
string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand), value: nsString!.substring(with: range), range: range)
case .Code:
addFontAttributes(range, .monospace)
if nsString == nil {
nsString = string.string as NSString
}
string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.Code), value: nsString!.substring(with: range), range: range)
case let .Pre(language):
addFontAttributes(range, .monospace)
addFontAttributes(range, .blockQuote)
if nsString == nil {
nsString = string.string as NSString
}
if let codeBlockTitleColor, let codeBlockAccentColor, let codeBlockBackgroundColor {
var title: NSAttributedString?
if let language, !language.isEmpty {
title = NSAttributedString(string: language.capitalized, font: boldFont.withSize(round(boldFont.pointSize * 0.8235294117647058)), textColor: codeBlockTitleColor)
}
string.addAttribute(NSAttributedString.Key(rawValue: "Attribute__Blockquote"), value: TextNodeBlockQuoteData(kind: .code(language: language), title: title, color: codeBlockAccentColor, secondaryColor: nil, tertiaryColor: nil, backgroundColor: codeBlockBackgroundColor, isCollapsible: false), range: range)
}
case let .BlockQuote(isCollapsed):
addFontAttributes(range, .blockQuote)
string.addAttribute(NSAttributedString.Key(rawValue: "Attribute__Blockquote"), value: TextNodeBlockQuoteData(kind: .quote, title: nil, color: baseQuoteTintColor, secondaryColor: baseQuoteSecondaryTintColor, tertiaryColor: baseQuoteTertiaryTintColor, backgroundColor: baseQuoteTintColor.withMultipliedAlpha(0.1), isCollapsible: isCollapsed), range: range)
case .BankCard:
string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range)
if underlineLinks && underlineAllLinks {
string.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range)
}
if nsString == nil {
nsString = string.string as NSString
}
string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.BankCard), value: nsString!.substring(with: range), range: range)
case .Spoiler:
if external {
string.addAttribute(NSAttributedString.Key.backgroundColor, value: UIColor.gray, range: range)
} else {
string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler), value: true as NSNumber, range: range)
}
case let .FormattedDate(_, date):
string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range)
if underlineLinks && underlineAllLinks {
string.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range)
}
string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.Date), value: date, range: range)
case let .Custom(type):
if type == ApplicationSpecificEntityType.Timecode {
string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range)
if underlineLinks && underlineAllLinks {
string.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range)
}
if nsString == nil {
nsString = string.string as NSString
}
let text = nsString!.substring(with: range)
if let time = parseTimecodeString(text) {
string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.Timecode), value: TelegramTimecode(time: time, text: text), range: range)
}
} else if type == ApplicationSpecificEntityType.Button {
string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.Button), value: true as NSNumber, range: range)
addFontAttributes(range, .smaller)
}
case let .CustomEmoji(_, fileId):
let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)
var emojiFile: TelegramMediaFile?
if let file = message?.associatedMedia[mediaId] as? TelegramMediaFile {
emojiFile = file
} else {
emojiFile = entityFiles[mediaId]
}
string.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: emojiFile), range: range)
default:
break
}
func setFont(range: NSRange, fontAttributes: ChatTextFontAttributes) {
var font: UIFont
var isQuote = false
var fontAttributes = fontAttributes
if fontAttributes.contains(.blockQuote) {
fontAttributes.remove(.blockQuote)
isQuote = true
}
if fontAttributes == [.bold, .italic] {
font = boldItalicFont
} else if fontAttributes == [.bold] {
font = boldFont
} else if fontAttributes == [.italic] {
font = italicFont
} else if fontAttributes == [.monospace] {
font = fixedFont
} else if fontAttributes == [.smaller] {
font = baseFont.withSize(floor(baseFont.pointSize * 0.9))
} else {
font = baseFont
}
if adjustQuoteFontSize, isQuote {
font = font.withSize(round(font.pointSize * 0.8235294117647058))
}
string.addAttribute(.font, value: font, range: range)
}
var currentAttributeSpan: (startIndex: Int, attributes: ChatTextFontAttributes)?
for i in 0 ..< fontAttributeMask.count {
if fontAttributeMask[i] != currentAttributeSpan?.attributes {
if let currentAttributeSpan {
setFont(range: NSRange(location: currentAttributeSpan.startIndex, length: i - currentAttributeSpan.startIndex), fontAttributes: currentAttributeSpan.attributes)
}
currentAttributeSpan = (i, fontAttributeMask[i])
}
}
if let currentAttributeSpan {
setFont(range: NSRange(location: currentAttributeSpan.startIndex, length: fontAttributeMask.count - currentAttributeSpan.startIndex), fontAttributes: currentAttributeSpan.attributes)
}
}
string.enumerateAttribute(NSAttributedString.Key("Attribute__Blockquote"), in: NSRange(location: 0, length: string.length), using: { value, range, _ in
guard let value = value as? TextNodeBlockQuoteData, case let .code(language) = value.kind, let language, !language.isEmpty else {
return
}
let codeText = (string.string as NSString).substring(with: range)
if let cachedMessageSyntaxHighlight, let entry = cachedMessageSyntaxHighlight.values[CachedMessageSyntaxHighlight.Spec(language: language, text: codeText)] {
for entity in entry.entities {
string.addAttribute(.foregroundColor, value: UIColor(rgb: UInt32(bitPattern: entity.color)), range: NSRange(location: range.location + entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound))
}
}
})
if let paragraphAlignment = paragraphAlignment {
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = paragraphAlignment
string.addAttribute(.paragraphStyle, value: paragraphStyle, range: NSMakeRange(0, string.length))
}
return string
}
public final class MessageSyntaxHighlight: Codable, Equatable {
public struct Entity: Codable, Equatable {
private enum CodingKeys: String, CodingKey {
case color = "c"
case rangeLow = "r"
case rangeLength = "rl"
}
public var color: Int32
public var range: Range<Int>
public init(color: Int32, range: Range<Int>) {
self.color = color
self.range = range
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.color = try container.decode(Int32.self, forKey: .color)
let rangeLow = Int(try container.decode(Int32.self, forKey: .rangeLow))
let rangeLength = Int(try container.decode(Int32.self, forKey: .rangeLength))
self.range = rangeLow ..< (rangeLow + rangeLength)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.color, forKey: .color)
try container.encode(Int32(self.range.lowerBound), forKey: .rangeLow)
try container.encode(Int32(self.range.upperBound - self.range.lowerBound), forKey: .rangeLength)
}
}
private enum CodingKeys: String, CodingKey {
case entities = "e"
}
public let entities: [Entity]
public init(entities: [Entity]) {
self.entities = entities
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.entities = try container.decode([Entity].self, forKey: .entities)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.entities, forKey: .entities)
}
public static func ==(lhs: MessageSyntaxHighlight, rhs: MessageSyntaxHighlight) -> Bool {
if lhs.entities != rhs.entities {
return false
}
return true
}
}
public final class CachedMessageSyntaxHighlight: Codable, Equatable {
public struct Spec: Hashable, Codable {
private enum CodingKeys: String, CodingKey {
case language = "l"
case text = "t"
}
public var language: String
public var text: String
public init(language: String, text: String) {
self.language = language
self.text = text
}
}
private enum CodingKeys: String, CodingKey {
case values = "v"
}
private struct CodingValueEntry: Codable {
private enum CodingKeys: String, CodingKey {
case key = "k"
case value = "v"
}
let key: Spec
let value: MessageSyntaxHighlight
init(key: Spec, value: MessageSyntaxHighlight) {
self.key = key
self.value = value
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.key = try container.decode(Spec.self, forKey: .key)
self.value = try container.decode(MessageSyntaxHighlight.self, forKey: .value)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.key, forKey: .key)
try container.encode(self.value, forKey: .value)
}
}
public let values: [Spec: MessageSyntaxHighlight]
public init(values: [Spec: MessageSyntaxHighlight]) {
self.values = values
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let valueEntries = try container.decode([CodingValueEntry].self, forKey: .values)
var values: [Spec: MessageSyntaxHighlight] = [:]
for entry in valueEntries {
values[entry.key] = entry.value
}
self.values = values
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
let valueEntries = self.values.map { CodingValueEntry(key: $0.key, value: $0.value) }
try container.encode(valueEntries, forKey: .values)
}
public static func ==(lhs: CachedMessageSyntaxHighlight, rhs: CachedMessageSyntaxHighlight) -> Bool {
if lhs.values != rhs.values {
return false
}
return true
}
}
private let messageSyntaxHighlightQueue = Queue(name: "MessageSyntaxHighlight")
public func extractMessageSyntaxHighlightSpecs(text: String, entities: [MessageTextEntity]) -> [CachedMessageSyntaxHighlight.Spec] {
if entities.isEmpty {
return []
}
var result: [CachedMessageSyntaxHighlight.Spec] = []
let nsString = text as NSString
for entity in entities {
if case let .Pre(language) = entity.type, let language, !language.isEmpty {
var range = entity.range
if range.lowerBound < 0 {
range = 0 ..< range.upperBound
}
if range.upperBound > nsString.length {
range = range.lowerBound ..< nsString.length
}
if range.upperBound != range.lowerBound {
result.append(CachedMessageSyntaxHighlight.Spec(language: language, text: nsString.substring(with: NSRange(location: range.lowerBound, length: range.upperBound - range.lowerBound))))
}
}
}
return result
}
private let internalFixedCodeFont = Font.regular(17.0)
public func asyncUpdateMessageSyntaxHighlight(engine: TelegramEngine, messageId: EngineMessage.Id, current: CachedMessageSyntaxHighlight?, specs: [CachedMessageSyntaxHighlight.Spec]) -> Signal<Never, NoError> {
if let current, !specs.contains(where: { current.values[$0] == nil }) {
return .complete()
}
return Signal { subscriber in
var updated: [CachedMessageSyntaxHighlight.Spec: MessageSyntaxHighlight] = [:]
let theme = SyntaxterTheme(dark: false, textColor: .black, textFont: internalFixedCodeFont, italicFont: internalFixedCodeFont, mediumFont: internalFixedCodeFont)
for spec in specs {
if let value = current?.values[spec] {
updated[spec] = value
} else {
var entities: [MessageSyntaxHighlight.Entity] = []
if let syntaxHighlighter {
if let highlightedString = syntaxHighlighter.syntax(spec.text, language: spec.language, theme: theme) {
highlightedString.enumerateAttribute(.foregroundColor, in: NSRange(location: 0, length: highlightedString.length), using: { value, subRange, _ in
if let value = value as? UIColor, value != .black {
entities.append(MessageSyntaxHighlight.Entity(color: Int32(bitPattern: value.rgb), range: subRange.lowerBound ..< subRange.upperBound))
}
})
}
}
updated[spec] = MessageSyntaxHighlight(entities: entities)
}
}
if let entry = CodableEntry(CachedMessageSyntaxHighlight(values: updated)) {
return engine.messages.storeLocallyDerivedData(messageId: messageId, data: ["code": entry]).start(completed: {
subscriber.putCompletion()
})
} else {
return EmptyDisposable
}
}
|> runOn(messageSyntaxHighlightQueue)
}
public func asyncStanaloneSyntaxHighlight(current: CachedMessageSyntaxHighlight?, specs: [CachedMessageSyntaxHighlight.Spec]) -> Signal<CachedMessageSyntaxHighlight, NoError> {
if let current, !specs.contains(where: { current.values[$0] == nil }) {
return .single(current)
}
return Signal { subscriber in
var updated: [CachedMessageSyntaxHighlight.Spec: MessageSyntaxHighlight] = [:]
let theme = SyntaxterTheme(dark: false, textColor: .black, textFont: internalFixedCodeFont, italicFont: internalFixedCodeFont, mediumFont: internalFixedCodeFont)
for spec in specs {
if let value = current?.values[spec] {
updated[spec] = value
} else {
var entities: [MessageSyntaxHighlight.Entity] = []
if let syntaxHighlighter {
if let highlightedString = syntaxHighlighter.syntax(spec.text, language: spec.language, theme: theme) {
highlightedString.enumerateAttribute(.foregroundColor, in: NSRange(location: 0, length: highlightedString.length), using: { value, subRange, _ in
if let value = value as? UIColor, value != .black {
entities.append(MessageSyntaxHighlight.Entity(color: Int32(bitPattern: value.rgb), range: subRange.lowerBound ..< subRange.upperBound))
}
})
}
}
updated[spec] = MessageSyntaxHighlight(entities: entities)
}
}
subscriber.putNext(CachedMessageSyntaxHighlight(values: updated))
subscriber.putCompletion()
return EmptyDisposable
}
|> runOn(messageSyntaxHighlightQueue)
}
@@ -0,0 +1,48 @@
import Foundation
import TelegramCore
public final class TelegramHashtag {
public let peerName: String?
public let hashtag: String
public init(peerName: String?, hashtag: String) {
self.peerName = peerName
self.hashtag = hashtag
}
}
public final class TelegramPeerMention {
public let peerId: EnginePeer.Id
public let mention: String
public init(peerId: EnginePeer.Id, mention: String) {
self.peerId = peerId
self.mention = mention
}
}
public final class TelegramTimecode {
public let time: Double
public let text: String
public init(time: Double, text: String) {
self.time = time
self.text = text
}
}
public struct TelegramTextAttributes {
public static let URL = "UrlAttributeT"
public static let PeerMention = "TelegramPeerMention"
public static let PeerTextMention = "TelegramPeerTextMention"
public static let BotCommand = "TelegramBotCommand"
public static let Hashtag = "TelegramHashtag"
public static let BankCard = "TelegramBankCard"
public static let Timecode = "TelegramTimecode"
public static let BlockQuote = "TelegramBlockQuote"
public static let Pre = "TelegramPre"
public static let Spoiler = "TelegramSpoiler"
public static let Code = "TelegramCode"
public static let Button = "TelegramButton"
public static let Date = "TelegramDate"
}