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 statusNode: ChatMessageDateAndStatusNode private var linkHighlightingNode: LinkHighlightingNode? private var cachedChatMessageText: CachedChatMessageText? required init() { self.textNode = TextNode() self.statusNode = ChatMessageDateAndStatusNode() super.init() self.textNode.isUserInteractionEnabled = false self.textNode.contentMode = .topLeft self.textNode.contentsScale = UIScreenScale self.textNode.displaysAsynchronously = true self.addSubnode(self.textNode) } 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) let horizontalInset = layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right let textConstrainedSize = CGSize(width: 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.theme, item.presentationData.strings, edited && !sentViaBot, viewCount, dateText, statusType, textConstrainedSize) statusSize = size statusApply = apply } let rawText: String let attributedText: NSAttributedString var messageEntities: [MessageTextEntity]? var isUnsupportedMedia = false for media in item.message.media { if let _ = media as? TelegramMediaUnsupported { isUnsupportedMedia = true } } if isUnsupportedMedia { rawText = item.presentationData.strings.Conversation_UnsupportedMediaPlaceholder messageEntities = [MessageTextEntity(range: 0.. 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 { 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 ] 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 } }