mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-08-02 00:17:02 +00:00
Added recent stickers clearing Added sending logs via email Added forward recipient change on forward acccessory panel tap Tweaked undo panel design Various UI fixes
499 lines
28 KiB
Swift
499 lines
28 KiB
Swift
import Foundation
|
|
import AsyncDisplayKit
|
|
import Display
|
|
import TelegramCore
|
|
import Postbox
|
|
|
|
private final class CachedChatMessageText {
|
|
let text: String
|
|
let inputEntities: [MessageTextEntity]?
|
|
let entities: [MessageTextEntity]?
|
|
|
|
init(text: String, inputEntities: [MessageTextEntity]?, entities: [MessageTextEntity]?) {
|
|
self.text = text
|
|
self.inputEntities = inputEntities
|
|
self.entities = entities
|
|
}
|
|
|
|
func matches(text: String, inputEntities: [MessageTextEntity]?) -> Bool {
|
|
if self.text != text {
|
|
return false
|
|
}
|
|
if let current = self.inputEntities, let inputEntities = inputEntities {
|
|
if current != inputEntities {
|
|
return false
|
|
}
|
|
} else if (self.inputEntities != nil) != (inputEntities != nil) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
|
private let textNode: TextNode
|
|
private let textAccessibilityOverlayNode: TextAccessibilityOverlayNode
|
|
private let statusNode: ChatMessageDateAndStatusNode
|
|
private var linkHighlightingNode: LinkHighlightingNode?
|
|
|
|
private var textHighlightingNodes: [LinkHighlightingNode] = []
|
|
|
|
private var cachedChatMessageText: CachedChatMessageText?
|
|
|
|
required init() {
|
|
self.textNode = TextNode()
|
|
|
|
self.statusNode = ChatMessageDateAndStatusNode()
|
|
|
|
self.textAccessibilityOverlayNode = TextAccessibilityOverlayNode()
|
|
|
|
super.init()
|
|
|
|
self.textNode.isUserInteractionEnabled = false
|
|
self.textNode.contentMode = .topLeft
|
|
self.textNode.contentsScale = UIScreenScale
|
|
self.textNode.displaysAsynchronously = true
|
|
self.addSubnode(self.textNode)
|
|
self.addSubnode(self.textAccessibilityOverlayNode)
|
|
|
|
self.textAccessibilityOverlayNode.openUrl = { [weak self] url in
|
|
self?.item?.controllerInteraction.openUrl(url, false, false)
|
|
}
|
|
}
|
|
|
|
required init?(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) {
|
|
let textLayout = TextNode.asyncLayout(self.textNode)
|
|
let statusLayout = self.statusNode.asyncLayout()
|
|
|
|
let currentCachedChatMessageText = self.cachedChatMessageText
|
|
|
|
return { item, layoutConstants, _, _, _ in
|
|
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
|
|
|
|
return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in
|
|
let message = item.message
|
|
|
|
let incoming = item.message.effectivelyIncoming(item.context.account.peerId)
|
|
|
|
var maxTextWidth = CGFloat.greatestFiniteMagnitude
|
|
for media in item.message.media {
|
|
if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, content.type == "telegram_background" {
|
|
maxTextWidth = layoutConstants.wallpapers.maxTextWidth
|
|
break
|
|
}
|
|
}
|
|
|
|
let horizontalInset = layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right
|
|
let textConstrainedSize = CGSize(width: min(maxTextWidth, constrainedSize.width - horizontalInset), height: constrainedSize.height)
|
|
|
|
var edited = false
|
|
var sentViaBot = false
|
|
var viewCount: Int?
|
|
for attribute in item.message.attributes {
|
|
if let _ = attribute as? EditedMessageAttribute {
|
|
edited = true
|
|
} else if let attribute = attribute as? ViewCountMessageAttribute {
|
|
viewCount = attribute.count
|
|
} else if let _ = attribute as? InlineBotMessageAttribute {
|
|
sentViaBot = true
|
|
}
|
|
}
|
|
if let author = item.message.author as? TelegramUser, author.botInfo != nil {
|
|
sentViaBot = true
|
|
}
|
|
|
|
let dateText = stringForMessageTimestampStatus(message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings)
|
|
|
|
let statusType: ChatMessageDateAndStatusType?
|
|
switch position {
|
|
case .linear(_, .None):
|
|
if incoming {
|
|
statusType = .BubbleIncoming
|
|
} else {
|
|
if message.flags.contains(.Failed) {
|
|
statusType = .BubbleOutgoing(.Failed)
|
|
} else if message.flags.isSending && !message.isSentOrAcknowledged {
|
|
statusType = .BubbleOutgoing(.Sending)
|
|
} else {
|
|
statusType = .BubbleOutgoing(.Sent(read: item.read))
|
|
}
|
|
}
|
|
default:
|
|
statusType = nil
|
|
}
|
|
|
|
var statusSize: CGSize?
|
|
var statusApply: ((Bool) -> Void)?
|
|
|
|
if let statusType = statusType {
|
|
let (size, apply) = statusLayout(item.presentationData, edited && !sentViaBot, viewCount, dateText, statusType, textConstrainedSize)
|
|
statusSize = size
|
|
statusApply = apply
|
|
}
|
|
|
|
let rawText: String
|
|
let attributedText: NSAttributedString
|
|
var messageEntities: [MessageTextEntity]?
|
|
|
|
var mediaDuration: Double? = nil
|
|
var isUnsupportedMedia = false
|
|
for media in item.message.media {
|
|
if let file = media as? TelegramMediaFile, let duration = file.duration {
|
|
mediaDuration = Double(duration)
|
|
}
|
|
else if media is TelegramMediaUnsupported {
|
|
isUnsupportedMedia = true
|
|
}
|
|
}
|
|
|
|
if isUnsupportedMedia {
|
|
rawText = item.presentationData.strings.Conversation_UnsupportedMediaPlaceholder
|
|
messageEntities = [MessageTextEntity(range: 0..<rawText.count, type: .Italic)]
|
|
} else {
|
|
rawText = item.message.text
|
|
for attribute in item.message.attributes {
|
|
if let attribute = attribute as? TextEntitiesMessageAttribute {
|
|
messageEntities = attribute.entities
|
|
} else if mediaDuration == nil, let attribute = attribute as? ReplyMessageAttribute {
|
|
if let replyMessage = item.message.associatedMessages[attribute.messageId] {
|
|
for media in replyMessage.media {
|
|
if let file = media as? TelegramMediaFile, let duration = file.duration {
|
|
mediaDuration = Double(duration)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var entities: [MessageTextEntity]?
|
|
|
|
var updatedCachedChatMessageText: CachedChatMessageText?
|
|
if let cached = currentCachedChatMessageText, cached.matches(text: rawText, inputEntities: messageEntities) {
|
|
entities = cached.entities
|
|
} else {
|
|
entities = messageEntities
|
|
|
|
if entities == nil && mediaDuration != nil {
|
|
entities = []
|
|
}
|
|
|
|
if let entitiesValue = entities {
|
|
var enabledTypes: EnabledEntityTypes = .all
|
|
if mediaDuration != nil {
|
|
enabledTypes.insert(.timecode)
|
|
}
|
|
if let result = addLocallyGeneratedEntities(rawText, enabledTypes: enabledTypes, entities: entitiesValue, mediaDuration: mediaDuration) {
|
|
entities = result
|
|
}
|
|
} else {
|
|
var generateEntities = false
|
|
for media in message.media {
|
|
if media is TelegramMediaImage || media is TelegramMediaFile {
|
|
generateEntities = true
|
|
break
|
|
}
|
|
}
|
|
if message.id.peerId.namespace == Namespaces.Peer.SecretChat {
|
|
generateEntities = true
|
|
}
|
|
if generateEntities {
|
|
let parsedEntities = generateTextEntities(rawText, enabledTypes: .all)
|
|
if !parsedEntities.isEmpty {
|
|
entities = parsedEntities
|
|
}
|
|
}
|
|
}
|
|
updatedCachedChatMessageText = CachedChatMessageText(text: rawText, inputEntities: messageEntities, entities: entities)
|
|
}
|
|
|
|
|
|
let bubbleTheme = item.presentationData.theme.theme.chat.bubble
|
|
|
|
var textFont = item.presentationData.messageFont
|
|
var forceStatusNewline = false
|
|
if rawText.containsOnlyEmoji && item.presentationData.largeEmoji {
|
|
let emojis = rawText.emojis
|
|
switch emojis.count {
|
|
case 1:
|
|
textFont = item.presentationData.messageEmojiFont1
|
|
forceStatusNewline = true
|
|
case 2:
|
|
textFont = item.presentationData.messageEmojiFont2
|
|
forceStatusNewline = true
|
|
case 3:
|
|
textFont = item.presentationData.messageEmojiFont3
|
|
forceStatusNewline = true
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
if let entities = entities {
|
|
attributedText = stringWithAppliedEntities(rawText, entities: entities, baseColor: incoming ? bubbleTheme.incomingPrimaryTextColor : bubbleTheme.outgoingPrimaryTextColor, linkColor: incoming ? bubbleTheme.incomingLinkTextColor : bubbleTheme.outgoingLinkTextColor, baseFont: textFont, linkFont: textFont, boldFont: item.presentationData.messageBoldFont, italicFont: item.presentationData.messageItalicFont, fixedFont: item.presentationData.messageFixedFont)
|
|
} else {
|
|
attributedText = NSAttributedString(string: rawText, font: textFont, textColor: incoming ? bubbleTheme.incomingPrimaryTextColor : bubbleTheme.outgoingPrimaryTextColor)
|
|
}
|
|
|
|
var cutout: TextNodeCutout?
|
|
if let statusSize = statusSize, !forceStatusNewline {
|
|
cutout = TextNodeCutout(bottomRight: statusSize)
|
|
}
|
|
|
|
let textInsets = UIEdgeInsets(top: 2.0, left: 0.0, bottom: 5.0, right: 0.0)
|
|
|
|
let (textLayout, textApply) = textLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: cutout, insets: textInsets))
|
|
|
|
var textFrame = CGRect(origin: CGPoint(x: -textInsets.left, y: -textInsets.top), size: textLayout.size)
|
|
var textFrameWithoutInsets = CGRect(origin: CGPoint(x: textFrame.origin.x + textInsets.left, y: textFrame.origin.y + textInsets.top), size: CGSize(width: textFrame.width - textInsets.left - textInsets.right, height: textFrame.height - textInsets.top - textInsets.bottom))
|
|
|
|
var statusFrame: CGRect?
|
|
if let statusSize = statusSize {
|
|
if forceStatusNewline {
|
|
statusFrame = CGRect(origin: CGPoint(x: textFrameWithoutInsets.maxX - statusSize.width, y: textFrameWithoutInsets.maxY), size: statusSize)
|
|
} else {
|
|
statusFrame = CGRect(origin: CGPoint(x: textFrameWithoutInsets.maxX - statusSize.width, y: textFrameWithoutInsets.maxY - statusSize.height), size: statusSize)
|
|
}
|
|
}
|
|
|
|
textFrame = textFrame.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top)
|
|
textFrameWithoutInsets = textFrameWithoutInsets.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top)
|
|
statusFrame = statusFrame?.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top)
|
|
|
|
var suggestedBoundingWidth: CGFloat
|
|
if let statusFrame = statusFrame {
|
|
suggestedBoundingWidth = textFrameWithoutInsets.union(statusFrame).width
|
|
} else {
|
|
suggestedBoundingWidth = textFrameWithoutInsets.width
|
|
}
|
|
suggestedBoundingWidth += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right
|
|
|
|
return (suggestedBoundingWidth, { boundingWidth in
|
|
var boundingSize: CGSize
|
|
var adjustedStatusFrame: CGRect?
|
|
|
|
if let statusFrame = statusFrame {
|
|
let centeredTextFrame = CGRect(origin: CGPoint(x: floor((boundingWidth - textFrame.size.width) / 2.0), y: 0.0), size: textFrame.size)
|
|
let statusOverlapsCenteredText = CGRect(origin: CGPoint(), size: statusFrame.size).intersects(centeredTextFrame)
|
|
|
|
if !forceStatusNewline || statusOverlapsCenteredText {
|
|
boundingSize = textFrameWithoutInsets.union(statusFrame).size
|
|
boundingSize.width += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right
|
|
boundingSize.height += layoutConstants.text.bubbleInsets.top + layoutConstants.text.bubbleInsets.bottom
|
|
adjustedStatusFrame = CGRect(origin: CGPoint(x: boundingWidth - statusFrame.size.width - layoutConstants.text.bubbleInsets.right, y: statusFrame.origin.y), size: statusFrame.size)
|
|
} else {
|
|
boundingSize = textFrameWithoutInsets.size
|
|
boundingSize.width += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right
|
|
boundingSize.height += layoutConstants.text.bubbleInsets.top + layoutConstants.text.bubbleInsets.bottom
|
|
adjustedStatusFrame = CGRect(origin: CGPoint(x: boundingWidth - statusFrame.size.width - layoutConstants.text.bubbleInsets.right, y: boundingSize.height - statusFrame.height - layoutConstants.text.bubbleInsets.bottom), size: statusFrame.size)
|
|
}
|
|
} else {
|
|
boundingSize = textFrameWithoutInsets.size
|
|
boundingSize.width += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right
|
|
boundingSize.height += layoutConstants.text.bubbleInsets.top + layoutConstants.text.bubbleInsets.bottom
|
|
}
|
|
|
|
return (boundingSize, { [weak self] animation, _ in
|
|
if let strongSelf = self {
|
|
strongSelf.item = item
|
|
if let updatedCachedChatMessageText = updatedCachedChatMessageText {
|
|
strongSelf.cachedChatMessageText = updatedCachedChatMessageText
|
|
}
|
|
|
|
let cachedLayout = strongSelf.textNode.cachedLayout
|
|
|
|
if case .System = animation {
|
|
if let cachedLayout = cachedLayout {
|
|
if !cachedLayout.areLinesEqual(to: textLayout) {
|
|
if let textContents = strongSelf.textNode.contents {
|
|
let fadeNode = ASDisplayNode()
|
|
fadeNode.displaysAsynchronously = false
|
|
fadeNode.contents = textContents
|
|
fadeNode.frame = strongSelf.textNode.frame
|
|
fadeNode.isLayerBacked = true
|
|
strongSelf.addSubnode(fadeNode)
|
|
fadeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak fadeNode] _ in
|
|
fadeNode?.removeFromSupernode()
|
|
})
|
|
strongSelf.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let _ = textApply()
|
|
|
|
if let statusApply = statusApply, let adjustedStatusFrame = adjustedStatusFrame {
|
|
let previousStatusFrame = strongSelf.statusNode.frame
|
|
strongSelf.statusNode.frame = adjustedStatusFrame
|
|
var hasAnimation = true
|
|
if case .None = animation {
|
|
hasAnimation = false
|
|
}
|
|
statusApply(hasAnimation)
|
|
if strongSelf.statusNode.supernode == nil {
|
|
strongSelf.addSubnode(strongSelf.statusNode)
|
|
} else {
|
|
if case let .System(duration) = animation {
|
|
let delta = CGPoint(x: previousStatusFrame.maxX - adjustedStatusFrame.maxX, y: previousStatusFrame.minY - adjustedStatusFrame.minY)
|
|
let statusPosition = strongSelf.statusNode.layer.position
|
|
let previousPosition = CGPoint(x: statusPosition.x + delta.x, y: statusPosition.y + delta.y)
|
|
strongSelf.statusNode.layer.animatePosition(from: previousPosition, to: statusPosition, duration: duration, timingFunction: kCAMediaTimingFunctionSpring)
|
|
}
|
|
}
|
|
} else if strongSelf.statusNode.supernode != nil {
|
|
strongSelf.statusNode.removeFromSupernode()
|
|
}
|
|
|
|
var adjustedTextFrame = textFrame
|
|
if forceStatusNewline {
|
|
adjustedTextFrame.origin.x = floor((boundingWidth - adjustedTextFrame.width) / 2.0)
|
|
}
|
|
strongSelf.textNode.frame = adjustedTextFrame
|
|
strongSelf.textAccessibilityOverlayNode.frame = textFrame
|
|
strongSelf.textAccessibilityOverlayNode.cachedLayout = textLayout
|
|
}
|
|
})
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
override func animateInsertion(_ currentTimestamp: Double, duration: Double) {
|
|
self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
}
|
|
|
|
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
|
|
self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
}
|
|
|
|
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
|
self.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
|
self.statusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
|
}
|
|
|
|
override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture) -> ChatMessageBubbleContentTapAction {
|
|
let textNodeFrame = self.textNode.frame
|
|
if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) {
|
|
if let url = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.URL)] as? String {
|
|
var concealed = true
|
|
if let attributeText = self.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) {
|
|
concealed = !doesUrlMatchText(url: url, text: attributeText)
|
|
}
|
|
return .url(url: url, concealed: concealed)
|
|
} else if let peerMention = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention {
|
|
return .peerMention(peerMention.peerId, peerMention.mention)
|
|
} else if let peerName = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.PeerTextMention)] as? String {
|
|
return .textMention(peerName)
|
|
} else if let botCommand = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.BotCommand)] as? String {
|
|
return .botCommand(botCommand)
|
|
} else if let hashtag = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag {
|
|
return .hashtag(hashtag.peerName, hashtag.hashtag)
|
|
} else if let timecode = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.Timecode)] as? TelegramTimecode {
|
|
return .timecode(timecode.time, timecode.text)
|
|
} else {
|
|
return .none
|
|
}
|
|
} else {
|
|
return .none
|
|
}
|
|
}
|
|
|
|
override func updateTouchesAtPoint(_ point: CGPoint?) {
|
|
if let item = self.item {
|
|
var rects: [CGRect]?
|
|
if let point = point {
|
|
let textNodeFrame = self.textNode.frame
|
|
if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) {
|
|
let possibleNames: [String] = [
|
|
TelegramTextAttributes.URL,
|
|
TelegramTextAttributes.PeerMention,
|
|
TelegramTextAttributes.PeerTextMention,
|
|
TelegramTextAttributes.BotCommand,
|
|
TelegramTextAttributes.Hashtag,
|
|
TelegramTextAttributes.Timecode
|
|
]
|
|
for name in possibleNames {
|
|
if let _ = attributes[NSAttributedStringKey(rawValue: name)] {
|
|
rects = self.textNode.attributeRects(name: name, at: index)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if let rects = rects {
|
|
let linkHighlightingNode: LinkHighlightingNode
|
|
if let current = self.linkHighlightingNode {
|
|
linkHighlightingNode = current
|
|
} else {
|
|
linkHighlightingNode = LinkHighlightingNode(color: item.message.effectivelyIncoming(item.context.account.peerId) ? item.presentationData.theme.theme.chat.bubble.incomingLinkHighlightColor : item.presentationData.theme.theme.chat.bubble.outgoingLinkHighlightColor)
|
|
self.linkHighlightingNode = linkHighlightingNode
|
|
self.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode)
|
|
}
|
|
linkHighlightingNode.frame = self.textNode.frame
|
|
linkHighlightingNode.updateRects(rects)
|
|
} else if let linkHighlightingNode = self.linkHighlightingNode {
|
|
self.linkHighlightingNode = nil
|
|
linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in
|
|
linkHighlightingNode?.removeFromSupernode()
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
override func peekPreviewContent(at point: CGPoint) -> (Message, ChatMessagePeekPreviewContent)? {
|
|
if let item = self.item {
|
|
let textNodeFrame = self.textNode.frame
|
|
if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) {
|
|
if let value = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.URL)] as? String {
|
|
if let rects = self.textNode.attributeRects(name: TelegramTextAttributes.URL, at: index), !rects.isEmpty {
|
|
var rect = rects[0]
|
|
for i in 1 ..< rects.count {
|
|
rect = rect.union(rects[i])
|
|
}
|
|
return (item.message, .url(self, rect, value))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
override func updateSearchTextHighlightState(text: String?) {
|
|
guard let item = self.item else {
|
|
return
|
|
}
|
|
let rectsSet: [[CGRect]]
|
|
if let text = text, !text.isEmpty {
|
|
rectsSet = self.textNode.textRangesRects(text: text)
|
|
} else {
|
|
rectsSet = []
|
|
}
|
|
for i in 0 ..< rectsSet.count {
|
|
let rects = rectsSet[i]
|
|
let textHighlightNode: LinkHighlightingNode
|
|
if self.textHighlightingNodes.count < i {
|
|
textHighlightNode = self.textHighlightingNodes[i]
|
|
} else {
|
|
textHighlightNode = LinkHighlightingNode(color: item.message.effectivelyIncoming(item.context.account.peerId) ? item.presentationData.theme.theme.chat.bubble.incomingTextHighlightColor : item.presentationData.theme.theme.chat.bubble.outgoingTextHighlightColor)
|
|
self.textHighlightingNodes.append(textHighlightNode)
|
|
self.insertSubnode(textHighlightNode, belowSubnode: self.textNode)
|
|
}
|
|
textHighlightNode.frame = self.textNode.frame
|
|
textHighlightNode.updateRects(rects)
|
|
}
|
|
for i in (rectsSet.count ..< self.textHighlightingNodes.count).reversed() {
|
|
self.textHighlightingNodes[i].removeFromSupernode()
|
|
self.textHighlightingNodes.remove(at: i)
|
|
}
|
|
}
|
|
}
|