import Foundation import AsyncDisplayKit import Display import TelegramCore import Postbox private let messageFont: UIFont = UIFont.systemFont(ofSize: 17.0) private let messageBoldFont: UIFont = UIFont.boldSystemFont(ofSize: 17.0) private let messageFixedFont: UIFont = UIFont(name: "Menlo-Regular", size: 16.0) ?? UIFont.systemFont(ofSize: 17.0) class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { private let textNode: TextNode private let statusNode: ChatMessageDateAndStatusNode private var linkHighlightingNode: LinkHighlightingNode? private var item: ChatMessageItem? required init() { self.textNode = TextNode() self.statusNode = ChatMessageDateAndStatusNode() super.init() self.textNode.isLayerBacked = true 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: ChatMessageItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ position: ChatMessageBubbleContentPosition, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))) { let textLayout = TextNode.asyncLayout(self.textNode) let statusLayout = self.statusNode.asyncLayout() return { item, layoutConstants, position, _ in return (CGFloat.greatestFiniteMagnitude, { constrainedSize in let message = item.message let incoming = item.message.effectivelyIncoming let horizontalInset = layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right let textConstrainedSize = CGSize(width: constrainedSize.width - horizontalInset, height: constrainedSize.height) var t = Int(item.message.timestamp) var timeinfo = tm() localtime_r(&t, &timeinfo) 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 } } var dateText = String(format: "%02d:%02d", arguments: [Int(timeinfo.tm_hour), Int(timeinfo.tm_min)]) if let author = item.message.author as? TelegramUser { if author.botInfo != nil { sentViaBot = true } if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { dateText = "\(author.displayTitle), \(dateText)" } } let statusType: ChatMessageDateAndStatusType? if case .None = position.bottom { if incoming { statusType = .BubbleIncoming } else { if message.flags.contains(.Failed) { statusType = .BubbleOutgoing(.Failed) } else if message.flags.isSending { statusType = .BubbleOutgoing(.Sending) } else { statusType = .BubbleOutgoing(.Sent(read: item.read)) } } } else { statusType = nil } var statusSize: CGSize? var statusApply: ((Bool) -> Void)? if let statusType = statusType { let (size, apply) = statusLayout(item.theme, edited && !sentViaBot, viewCount, dateText, statusType, textConstrainedSize) statusSize = size statusApply = apply } let attributedText: NSAttributedString var entities: TextEntitiesMessageAttribute? for attribute in item.message.attributes { if let attribute = attribute as? TextEntitiesMessageAttribute { entities = attribute break } } if entities == nil { var generateEntities = false for media in message.media { if media is TelegramMediaImage || media is TelegramMediaFile { generateEntities = true break } } if generateEntities { let parsedEntities = generateTextEntities(message.text) if !parsedEntities.isEmpty { entities = TextEntitiesMessageAttribute(entities: parsedEntities) } } } let bubbleTheme = item.theme.chat.bubble if let entities = entities { attributedText = stringWithAppliedEntities(message.text, entities: entities.entities, baseColor: incoming ? bubbleTheme.incomingPrimaryTextColor : bubbleTheme.outgoingPrimaryTextColor, linkColor: incoming ? bubbleTheme.incomingLinkTextColor : bubbleTheme.outgoingLinkTextColor, baseFont: messageFont, boldFont: messageBoldFont, fixedFont: messageFixedFont) } else { attributedText = NSAttributedString(string: message.text, font: messageFont, textColor: incoming ? bubbleTheme.incomingPrimaryTextColor : bubbleTheme.outgoingPrimaryTextColor) } let (textLayout, textApply) = textLayout(attributedText, nil, 0, .end, textConstrainedSize, .natural, nil, UIEdgeInsets()) var textFrame = CGRect(origin: CGPoint(), size: textLayout.size) let textSize = textLayout.size var statusFrame: CGRect? if let statusSize = statusSize { var frame = CGRect(origin: CGPoint(), size: statusSize) let trailingLineWidth = textLayout.trailingLineWidth if textSize.width - trailingLineWidth >= statusSize.width { frame.origin = CGPoint(x: textFrame.maxX - statusSize.width, y: textFrame.maxY - statusSize.height) } else if trailingLineWidth + statusSize.width < textConstrainedSize.width { frame.origin = CGPoint(x: textFrame.minX + trailingLineWidth, y: textFrame.maxY - statusSize.height) } else { frame.origin = CGPoint(x: textFrame.maxX - statusSize.width, y: textFrame.maxY) } statusFrame = frame } textFrame = textFrame.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 boundingSize: CGSize if let statusFrame = statusFrame { boundingSize = textFrame.union(statusFrame).size } else { boundingSize = textFrame.size } boundingSize.width += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right boundingSize.height += layoutConstants.text.bubbleInsets.top + layoutConstants.text.bubbleInsets.bottom return (boundingSize.width, { boundingWidth in var adjustedStatusFrame: CGRect? if let statusFrame = statusFrame { adjustedStatusFrame = CGRect(origin: CGPoint(x: boundingWidth - statusFrame.size.width - layoutConstants.text.bubbleInsets.right, y: statusFrame.origin.y), size: statusFrame.size) } return (boundingSize, { [weak self] animation in if let strongSelf = self { strongSelf.item = item let cachedLayout = strongSelf.textNode.cachedLayout if case .System = animation { if let cachedLayout = cachedLayout { if cachedLayout != 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() } strongSelf.textNode.frame = textFrame } }) }) }) } } 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) -> ChatMessageBubbleContentTapAction { let textNodeFrame = self.textNode.frame if let (_, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { if let url = attributes[NSAttributedStringKey(rawValue: TextNode.UrlAttribute)] as? String { return .url(url) } else if let peerMention = attributes[NSAttributedStringKey(rawValue: TextNode.TelegramPeerMentionAttribute)] as? TelegramPeerMention { return .peerMention(peerMention.peerId, peerMention.mention) } else if let peerName = attributes[NSAttributedStringKey(rawValue: TextNode.TelegramPeerTextMentionAttribute)] as? String { return .textMention(peerName) } else if let botCommand = attributes[NSAttributedStringKey(rawValue: TextNode.TelegramBotCommandAttribute)] as? String { return .botCommand(botCommand) } else if let hashtag = attributes[NSAttributedStringKey(rawValue: TextNode.TelegramHashtagAttribute)] 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] = [ TextNode.UrlAttribute, TextNode.TelegramPeerMentionAttribute, TextNode.TelegramPeerTextMentionAttribute, TextNode.TelegramBotCommandAttribute, TextNode.TelegramHashtagAttribute ] 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.theme.chat.bubble.incomingLinkHighlightColor : item.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() }) } } } }