This commit is contained in:
Ilya Laktyushin 2026-02-06 14:25:13 +04:00
commit 25952cfef6
20 changed files with 313 additions and 44 deletions

View file

@ -15769,3 +15769,17 @@ Error: %8$@";
"Gift.View.Context.Craft" = "Craft";
"Group.OwnershipTransfer.DescriptionShortInfo" = "This will transfer the full **owner rights** for **%1$@** to **%2$@**.";
"FormattedDate.InMinutes_1" = "in %@ minute";
"FormattedDate.InMinutes_any" = "in %@ minutes";
"FormattedDate.InHours_1" = "in %@ hour";
"FormattedDate.InHours_any" = "in %@ hours";
"FormattedDate.InDays_1" = "in %@ day";
"FormattedDate.InDays_any" = "in %@ days";
"FormattedDate.MinutesAgo_1" = "%@ minute ago";
"FormattedDate.MinutesAgo_any" = "%@ minutes ago";
"FormattedDate.HoursAgo_1" = "%@ hour ago";
"FormattedDate.HoursAgo_any" = "%@ hours ago";
"FormattedDate.DaysAgo_1" = "%@ day ago";
"FormattedDate.DaysAgo_any" = "%@ days ago";

View file

@ -255,6 +255,7 @@ public enum ChatControllerInteractionLongTapAction {
case hashtag(String)
case timecode(Double, String)
case bankCard(String)
case date(Int32)
}
public enum ChatHistoryMessageSelection: Equatable {

View file

@ -791,21 +791,21 @@ func messageTextEntitiesFromApiEntities(_ entities: [Api.MessageEntity]) -> [Mes
if (flags & (1 << 0)) != 0 {
format = .relative
} else {
let timeFormat: MessageTextEntityType.DateTimeFormat.TimeFormat
let timeFormat: MessageTextEntityType.DateTimeFormat.TimeFormat?
if (flags & (1 << 1)) != 0 {
timeFormat = .short
} else if (flags & (1 << 2)) != 0 {
timeFormat = .long
} else {
timeFormat = .short
timeFormat = nil
}
let dateFormat: MessageTextEntityType.DateTimeFormat.DateFormat
let dateFormat: MessageTextEntityType.DateTimeFormat.DateFormat?
if (flags & (1 << 3)) != 0 {
dateFormat = .short
} else if (flags & (1 << 4)) != 0 {
dateFormat = .long
} else {
dateFormat = .short
dateFormat = nil
}
format = .full(timeFormat: timeFormat, dateFormat: dateFormat)
}

View file

@ -65,12 +65,16 @@ func apiEntitiesFromMessageTextEntities(_ entities: [MessageTextEntity], associa
flags |= 1 << 1
case .long:
flags |= 1 << 2
default:
break
}
switch dateFormat {
case .short:
flags |= 1 << 3
case .long:
flags |= 1 << 4
default:
break
}
}
apiEntities.append(.messageEntityFormattedDate(.init(flags: flags, offset: offset, length: length, date: date)))

View file

@ -16,27 +16,27 @@ public enum MessageTextEntityType: Equatable {
}
case relative
case full(timeFormat: TimeFormat, dateFormat: DateFormat)
case full(timeFormat: TimeFormat?, dateFormat: DateFormat?)
public init(rawValue: Int32) {
if (rawValue & (1 << 0)) != 0 {
self = .relative
} else {
let timeFormat: MessageTextEntityType.DateTimeFormat.TimeFormat
let timeFormat: MessageTextEntityType.DateTimeFormat.TimeFormat?
if (rawValue & (1 << 1)) != 0 {
timeFormat = .short
} else if (rawValue & (1 << 2)) != 0 {
timeFormat = .long
} else {
timeFormat = .short
timeFormat = nil
}
let dateFormat: MessageTextEntityType.DateTimeFormat.DateFormat
let dateFormat: MessageTextEntityType.DateTimeFormat.DateFormat?
if (rawValue & (1 << 3)) != 0 {
dateFormat = .short
} else if (rawValue & (1 << 4)) != 0 {
dateFormat = .long
} else {
dateFormat = .short
dateFormat = nil
}
self = .full(timeFormat: timeFormat, dateFormat: dateFormat)
}
@ -53,12 +53,16 @@ public enum MessageTextEntityType: Equatable {
rawValue |= 1 << 1
case .long:
rawValue |= 1 << 2
default:
break
}
switch dateFormat {
case .short:
rawValue |= 1 << 3
case .long:
rawValue |= 1 << 4
default:
break
}
}
return rawValue

View file

@ -1,8 +1,60 @@
import Foundation
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
public func stringForShortTimestamp(hours: Int32, minutes: Int32, dateTimeFormat: PresentationDateTimeFormat, formatAsPlainText: Bool = false) -> String {
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 <= 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 <= 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):
var string = ""
if let dateFormat {
switch dateFormat {
case .short:
string += stringForMediumDate(timestamp: timestamp, strings: strings, dateTimeFormat: dateTimeFormat, withTime: false)
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
}
}
public func stringForShortTimestamp(hours: Int32, minutes: Int32, seconds: Int32? = nil, dateTimeFormat: PresentationDateTimeFormat, formatAsPlainText: Bool = false) -> String {
switch dateTimeFormat.timeFormat {
case .regular:
let hourString: String
@ -28,17 +80,23 @@ public func stringForShortTimestamp(hours: Int32, minutes: Int32, dateTimeFormat
spaceCharacter = "\u{00a0}"
}
if minutes >= 10 {
return "\(hourString):\(minutes)\(spaceCharacter)\(periodString)"
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):0\(minutes)\(spaceCharacter)\(periodString)"
return "\(hourString):\(minuteString)\(spaceCharacter)\(periodString)"
}
case .military:
return String(format: "%02d:%02d", arguments: [Int(hours), Int(minutes)])
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, local: Bool = true) -> String {
public func stringForMessageTimestamp(timestamp: Int32, dateTimeFormat: PresentationDateTimeFormat, withSeconds: Bool = false, local: Bool = true) -> String {
var t = Int(timestamp)
var timeinfo = tm()
if local {
@ -47,7 +105,7 @@ public func stringForMessageTimestamp(timestamp: Int32, dateTimeFormat: Presenta
gmtime_r(&t, &timeinfo)
}
return stringForShortTimestamp(hours: timeinfo.tm_hour, minutes: timeinfo.tm_min, dateTimeFormat: dateTimeFormat)
return stringForShortTimestamp(hours: timeinfo.tm_hour, minutes: timeinfo.tm_min, seconds: withSeconds ? timeinfo.tm_sec : nil, dateTimeFormat: dateTimeFormat)
}
public func getDateTimeComponents(timestamp: Int32) -> (day: Int32, month: Int32, year: Int32, hour: Int32, minutes: Int32) {

View file

@ -175,7 +175,7 @@ public final class ChatBotInfoItemNode: ListViewItemNode {
break
case .ignore:
return .fail
case .url, .phone, .peerMention, .textMention, .botCommand, .hashtag, .instantPage, .wallpaper, .theme, .call, .conferenceCall, .openMessage, .timecode, .bankCard, .tooltip, .openPollResults, .copy, .largeEmoji, .customEmoji, .custom:
case .url, .phone, .peerMention, .textMention, .botCommand, .hashtag, .instantPage, .wallpaper, .theme, .call, .conferenceCall, .openMessage, .timecode, .bankCard, .tooltip, .openPollResults, .copy, .largeEmoji, .customEmoji, .date, .custom:
return .waitForSingleTap
}
}

View file

@ -1672,7 +1672,8 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
TelegramTextAttributes.PeerTextMention,
TelegramTextAttributes.BotCommand,
TelegramTextAttributes.Hashtag,
TelegramTextAttributes.BankCard
TelegramTextAttributes.BankCard,
TelegramTextAttributes.Date
]
for name in possibleNames {
if let _ = attributes[NSAttributedString.Key(rawValue: name)] {

View file

@ -159,6 +159,7 @@ public struct ChatMessageBubbleContentTapAction {
case ignore
case openPollResults(Data)
case copy(String)
case date(Int32, String)
case largeEmoji(String, String?, TelegramMediaFile)
case customEmoji(TelegramMediaFile)
case custom(() -> Void)

View file

@ -21,6 +21,7 @@ import PersistentStringHash
import GridMessageSelectionNode
import AppBundle
import Markdown
import TelegramStringFormatting
import WallpaperBackgroundNode
import ChatPresentationInterfaceState
import ChatMessageBackground
@ -1296,7 +1297,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
break
case .ignore:
return .fail
case .url, .phone, .peerMention, .textMention, .botCommand, .hashtag, .instantPage, .wallpaper, .theme, .call, .conferenceCall, .openMessage, .timecode, .bankCard, .tooltip, .openPollResults, .copy, .largeEmoji, .customEmoji, .custom:
case .url, .phone, .peerMention, .textMention, .botCommand, .hashtag, .instantPage, .wallpaper, .theme, .call, .conferenceCall, .openMessage, .timecode, .bankCard, .tooltip, .openPollResults, .copy, .largeEmoji, .customEmoji, .date, .custom:
return .waitForSingleTap
}
}
@ -5658,6 +5659,15 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
item.controllerInteraction.openLargeEmojiInfo(emoji, fitz, file)
})
}
case let .date(date, _):
if let item = self.item {
return .action(InternalBubbleTapAction.Action { [weak self] in
guard let self, let contentNode = self.contextContentNodeForLink(stringForFullDate(timestamp: date, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat), rects: rects) else {
return
}
item.controllerInteraction.longTap(.date(date), ChatControllerInteraction.LongTapParams(message: item.message, contentNode: contentNode, messageNode: self, progress: tapAction.activate?()))
})
}
case let .customEmoji(file):
if let item = self.item {
return .optionalAction({
@ -5824,6 +5834,13 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
break
case .customEmoji:
break
case let .date(date, _):
return .action(InternalBubbleTapAction.Action { [weak self] in
guard let self, let contentNode = self.contextContentNodeForLink(stringForFullDate(timestamp: date, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat), rects: rects) else {
return
}
item.controllerInteraction.longTap(.date(date), ChatControllerInteraction.LongTapParams(message: item.content.firstMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?()))
})
case .custom:
break
}

View file

@ -233,7 +233,8 @@ public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode
TelegramTextAttributes.PeerTextMention,
TelegramTextAttributes.BotCommand,
TelegramTextAttributes.Hashtag,
TelegramTextAttributes.BankCard
TelegramTextAttributes.BankCard,
TelegramTextAttributes.Date
]
for name in possibleNames {
if let _ = attributes[NSAttributedString.Key(rawValue: name)] {

View file

@ -1149,7 +1149,8 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
TelegramTextAttributes.BotCommand,
TelegramTextAttributes.Hashtag,
TelegramTextAttributes.Timecode,
TelegramTextAttributes.BankCard
TelegramTextAttributes.BankCard,
TelegramTextAttributes.Date
]
for name in possibleNames {
if let _ = attributes[NSAttributedString.Key(rawValue: name)] {
@ -1202,6 +1203,12 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
return ChatMessageBubbleContentTapAction(content: .copy(pre))
} else if let code = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Code)] as? String {
return ChatMessageBubbleContentTapAction(content: .copy(code))
} else if let date = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Date)] as? Int32 {
var string = ""
if let (_, text, _) = self.textNode.textNode.attributeSubstringWithRange(name: TelegramTextAttributes.Date, index: index) {
string = text
}
return ChatMessageBubbleContentTapAction(content: .date(date, string), rects: rects)
} else if let _ = attributes[NSAttributedString.Key(rawValue: "Attribute__Blockquote")] {
if let _ = self.textNode.textNode.collapsibleBlockAtPoint(textLocalPoint) {
return ChatMessageBubbleContentTapAction(content: .none)
@ -1296,7 +1303,8 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
TelegramTextAttributes.BotCommand,
TelegramTextAttributes.Hashtag,
TelegramTextAttributes.Timecode,
TelegramTextAttributes.BankCard
TelegramTextAttributes.BankCard,
TelegramTextAttributes.Date
]
for name in possibleNames {
if let _ = attributes[NSAttributedString.Key(rawValue: name)] {

View file

@ -686,7 +686,8 @@ private final class ChatMessageTodoItemNode: ASDisplayNode {
TelegramTextAttributes.PeerTextMention,
TelegramTextAttributes.BotCommand,
TelegramTextAttributes.Hashtag,
TelegramTextAttributes.BankCard
TelegramTextAttributes.BankCard,
TelegramTextAttributes.Date
]
for name in possibleNames {
if let _ = attributes[NSAttributedString.Key(rawValue: name)], let textRects = textNode.textNode.attributeRects(name: name, at: index) {
@ -1584,7 +1585,8 @@ public class ChatMessageTodoBubbleContentNode: ChatMessageBubbleContentNode {
TelegramTextAttributes.PeerTextMention,
TelegramTextAttributes.BotCommand,
TelegramTextAttributes.Hashtag,
TelegramTextAttributes.BankCard
TelegramTextAttributes.BankCard,
TelegramTextAttributes.Date
]
for name in possibleNames {
if let _ = attributes[NSAttributedString.Key(rawValue: name)], let textRects = self.textNode.textNode.attributeRects(name: name, at: index) {

View file

@ -566,6 +566,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
strongSelf.presentController(actionSheet, .window(.root), nil)
case .bankCard:
break
case .date:
break
}
}
}, todoItemLongTap: { _, _ in

View file

@ -564,7 +564,8 @@ final class StoryContentCaptionComponent: Component {
TelegramTextAttributes.BotCommand,
TelegramTextAttributes.Hashtag,
TelegramTextAttributes.Timecode,
TelegramTextAttributes.BankCard
TelegramTextAttributes.BankCard,
TelegramTextAttributes.Date
]
for name in possibleNames {
if let _ = attributes[NSAttributedString.Key(rawValue: name)] {

View file

@ -0,0 +1,98 @@
import Foundation
import UIKit
import SwiftSignalKit
import Postbox
import TelegramCore
import AsyncDisplayKit
import Display
import ContextUI
import UndoUI
import AccountContext
import ChatMessageItemView
import ChatMessageItemCommon
import ChatControllerInteraction
import EventKit
import EventKitUI
extension ChatControllerImpl: EKEventEditViewDelegate {
func openDateContextMenu(date: Int32, params: ChatControllerInteraction.LongTapParams) -> Void {
guard let message = params.message, let contentNode = params.contentNode else {
return
}
guard let messages = self.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(message.id) else {
return
}
var updatedMessages = messages
for i in 0 ..< updatedMessages.count {
if updatedMessages[i].id == message.id {
let message = updatedMessages.remove(at: i)
updatedMessages.insert(message, at: 0)
break
}
}
let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil// anyRecognizer as? TapLongTapOrDoubleTapGestureRecognizer
let gesture: ContextGesture? = nil // anyRecognizer as? ContextGesture
let source: ContextContentSource
// if let location = location {
// source = .location(ChatMessageContextLocationContentSource(controller: self, location: messageNode.view.convert(messageNode.bounds, to: nil).origin.offsetBy(dx: location.x, dy: location.y)))
// } else {
source = .extracted(ChatMessageLinkContextExtractedContentSource(chatNode: self.chatDisplayNode, contentNode: contentNode))
// }
//TODO:localize
var items: [ContextMenuItem] = []
items.append(
.action(ContextMenuActionItem(text: "Copy Date", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
f(.default)
guard let self else {
return
}
UIPasteboard.general.string = "\(date)"
self.present(UndoOverlayController(presentationData: self.presentationData, content: .copy(text: presentationData.strings.Conversation_CardNumberCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current)
}))
)
items.append(
.action(ContextMenuActionItem(text: "Add to Calendar", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Calendar"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
f(.default)
guard let self else {
return
}
let eventStore = EKEventStore()
let event = EKEvent(eventStore: eventStore)
event.startDate = Date(timeIntervalSince1970: Double(date))
event.endDate = Date(timeIntervalSince1970: Double(date + 3600))
let editViewController = EKEventEditViewController()
editViewController.eventStore = eventStore
editViewController.event = event
editViewController.editViewDelegate = self
if let rootController = self.navigationController?.view.window?.rootViewController {
rootController.present(editViewController, animated: true)
}
}))
)
self.canReadHistory.set(false)
let controller = makeContextController(presentationData: self.presentationData, source: source, items: .single(ContextController.Items(content: .list(items))), recognizer: recognizer, gesture: gesture, disableScreenshots: false)
controller.dismissed = { [weak self] in
self?.canReadHistory.set(true)
}
self.window?.presentInGlobalOverlay(controller)
}
public func eventEditViewController(_ controller: EKEventEditViewController, didCompleteWith action: EKEventEditViewAction) {
controller.dismiss(animated: true)
}
}

View file

@ -35,6 +35,8 @@ extension ChatControllerImpl {
self.openBankCardContextMenu(number: number, params: params)
case let .phone(number):
self.openPhoneContextMenu(number: number, params: params)
case let .date(date):
self.openDateContextMenu(date: date, params: params)
}
}
}

View file

@ -15,6 +15,8 @@ private let whitelistedHosts: Set<String> = Set([
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 dataAndPhoneNumberAndDateDetector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType([.link, .phoneNumber, .date]).rawValue)
private let validHashtagSet: CharacterSet = {
var set = CharacterSet.alphanumerics
set.insert("_")
@ -100,8 +102,9 @@ public struct EnabledEntityTypes: OptionSet {
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 date = EnabledEntityTypes(rawValue: 1 << 8)
public static let all: EnabledEntityTypes = [.command, .mention, .hashtag, .allUrl, .phoneNumber]
public static let all: EnabledEntityTypes = [.command, .mention, .hashtag, .allUrl, .phoneNumber, .date]
}
private func commitEntity(_ utf16: String.UTF16View, _ type: CurrentEntityType, _ range: Range<String.UTF16View.Index>, _ enabledTypes: EnabledEntityTypes, _ entities: inout [MessageTextEntity], mediaDuration: Double? = nil) {
@ -254,7 +257,9 @@ public func generateTextEntities(_ text: String, enabledTypes: EnabledEntityType
let utf16 = text.utf16
var detector: NSDataDetector?
if enabledTypes.contains(.phoneNumber) && (enabledTypes.contains(.allUrl) || enabledTypes.contains(.internalUrl)) {
if enabledTypes.contains(.phoneNumber) && enabledTypes.contains(.date) && (enabledTypes.contains(.allUrl) || enabledTypes.contains(.internalUrl)) {
detector = dataAndPhoneNumberAndDateDetector
} else if enabledTypes.contains(.phoneNumber) && (enabledTypes.contains(.allUrl) || enabledTypes.contains(.internalUrl)) {
detector = dataAndPhoneNumberDetector
} else if enabledTypes.contains(.phoneNumber) {
detector = phoneNumberDetector
@ -267,7 +272,7 @@ public func generateTextEntities(_ text: String, enabledTypes: EnabledEntityType
if let detector = detector {
detector.enumerateMatches(in: text, options: [], range: NSMakeRange(0, utf16.count), using: { result, _, _ in
if let result = result {
if result.resultType == NSTextCheckingResult.CheckingType.link || result.resultType == NSTextCheckingResult.CheckingType.phoneNumber {
if [NSTextCheckingResult.CheckingType.link, NSTextCheckingResult.CheckingType.phoneNumber, NSTextCheckingResult.CheckingType.date].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 {
@ -293,6 +298,8 @@ public func generateTextEntities(_ text: String, enabledTypes: EnabledEntityType
}
type = .Url
} else if result.resultType == NSTextCheckingResult.CheckingType.date, let date = result.date?.timeIntervalSince1970 {
type = .FormattedDate(format: .relative, date: Int32(date))
} else {
type = .PhoneNumber
}

View file

@ -84,13 +84,47 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti
let baseQuoteTintColor = baseQuoteTintColor ?? baseColor
var nsString: NSString?
let string = NSMutableAttributedString(string: text, attributes: [NSAttributedString.Key.font: baseFont, NSAttributedString.Key.foregroundColor: baseColor])
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)
let 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):
// let replacement = stringForEntityFormattedDate()
// switch format {
// case .relative:
// replacement = relative
// case let .full(timeFormat, dateFormat):
// }
//
// 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
// 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 {
@ -103,12 +137,11 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti
skipEntity = false
continue
}
let stringLength = string.length
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
guard var range = adjustedRanges[i] else {
continue
}
let stringLength = string.length
if range.location > stringLength {
continue
} else if range.location + range.length > stringLength {
@ -121,13 +154,13 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti
string.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range)
}
if nsString == nil {
nsString = text as NSString
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 = text as NSString
nsString = string.string as NSString
}
if underlineLinks && underlineAllLinks {
string.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range)
@ -136,7 +169,7 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti
case .PhoneNumber:
string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range)
if nsString == nil {
nsString = text as NSString
nsString = string.string as NSString
}
if underlineLinks && underlineAllLinks {
string.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range)
@ -145,7 +178,7 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti
case let .TextUrl(url):
string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range)
if nsString == nil {
nsString = text as NSString
nsString = string.string as NSString
}
if underlineLinks && underlineAllLinks {
string.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range)
@ -168,7 +201,7 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti
string.addAttribute(NSAttributedString.Key.font, value: linkFont, range: range)
}
if nsString == nil {
nsString = text as NSString
nsString = string.string as NSString
}
string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention), value: nsString!.substring(with: range), range: range)
case .Strikethrough:
@ -183,16 +216,21 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti
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 = text as NSString
nsString = string.string as NSString
}
let hashtag = nsString!.substring(with: range)
if i + 1 != entities.count {
if case .Mention = entities[i + 1].type {
let nextRange = NSRange(location: entities[i + 1].range.lowerBound, length: entities[i + 1].range.upperBound - entities[i + 1].range.lowerBound)
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 {
@ -231,17 +269,20 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti
string.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range)
}
if nsString == nil {
nsString = text as NSString
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 = text as NSString
nsString = string.string as NSString
}
if let codeBlockTitleColor, let codeBlockAccentColor, let codeBlockBackgroundColor {
var title: NSAttributedString?
@ -260,7 +301,7 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti
string.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range)
}
if nsString == nil {
nsString = text as NSString
nsString = string.string as NSString
}
string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.BankCard), value: nsString!.substring(with: range), range: range)
case .Spoiler:
@ -269,6 +310,12 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti
} 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)
@ -276,7 +323,7 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti
string.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range)
}
if nsString == nil {
nsString = text as NSString
nsString = string.string as NSString
}
let text = nsString!.substring(with: range)
if let time = parseTimecodeString(text) {

View file

@ -44,4 +44,5 @@ public struct TelegramTextAttributes {
public static let Spoiler = "TelegramSpoiler"
public static let Code = "TelegramCode"
public static let Button = "TelegramButton"
public static let Date = "TelegramDate"
}