import Foundation import UIKit import AsyncDisplayKit import Display import SwiftSignalKit import TelegramCore import AccountContext import TelegramPresentationData import TelegramUIPreferences import TextFormat import LocalizedPeerData import UrlEscaping import TelegramStringFormatting import WallpaperBackgroundNode import ReactionSelectionNode import AnimatedStickerNode import TelegramAnimatedStickerNode import ChatControllerInteraction import ShimmerEffect import Markdown import ChatMessageBubbleContentNode import ChatMessageItemCommon private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: EngineMessage, accountPeerId: EnginePeer.Id) -> NSAttributedString? { return universalServiceMessageString(presentationData: (theme.theme, theme.wallpaper), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: false, forForumOverview: false) } public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { private let labelNode: TextNode private var backgroundNode: WallpaperBubbleBackgroundNode? private let backgroundMaskNode: ASImageNode private var linkHighlightingNode: LinkHighlightingNode? private var mediaBackgroundContent: WallpaperBubbleBackgroundNode? private let mediaBackgroundNode: NavigationBackgroundNode private let titleNode: TextNode private let subtitleNode: TextNode private let placeholderNode: StickerShimmerEffectNode private let animationNode: AnimatedStickerNode private var shimmerEffectNode: ShimmerEffectForegroundNode? private let buttonNode: HighlightTrackingButtonNode private let buttonStarsNode: PremiumStarsNode private let buttonTitleNode: TextNode private var cachedMaskBackgroundImage: (CGPoint, UIImage, [CGRect])? private var absoluteRect: (CGRect, CGSize)? private var isPlaying: Bool = false private var currentProgressDisposable: Disposable? override public var visibility: ListViewItemNodeVisibility { didSet { let wasVisible = oldValue != .none let isVisible = self.visibility != .none if wasVisible != isVisible { self.visibilityStatus = isVisible } } } private var visibilityStatus: Bool? { didSet { if self.visibilityStatus != oldValue { self.updateVisibility() } } } private var fetchDisposable: Disposable? private var setupTimestamp: Double? required public init() { self.labelNode = TextNode() self.labelNode.isUserInteractionEnabled = false self.labelNode.displaysAsynchronously = false self.backgroundMaskNode = ASImageNode() self.mediaBackgroundNode = NavigationBackgroundNode(color: .clear) self.mediaBackgroundNode.clipsToBounds = true self.mediaBackgroundNode.cornerRadius = 24.0 self.titleNode = TextNode() self.titleNode.isUserInteractionEnabled = false self.titleNode.displaysAsynchronously = false self.subtitleNode = TextNode() self.subtitleNode.isUserInteractionEnabled = false self.subtitleNode.displaysAsynchronously = false self.buttonNode = HighlightTrackingButtonNode() self.buttonNode.clipsToBounds = true self.buttonNode.cornerRadius = 17.0 self.placeholderNode = StickerShimmerEffectNode() self.placeholderNode.isUserInteractionEnabled = false self.placeholderNode.alpha = 0.75 self.animationNode = DefaultAnimatedStickerNodeImpl() self.buttonStarsNode = PremiumStarsNode() self.buttonTitleNode = TextNode() self.buttonTitleNode.isUserInteractionEnabled = false self.buttonTitleNode.displaysAsynchronously = false super.init() self.addSubnode(self.labelNode) self.addSubnode(self.mediaBackgroundNode) self.addSubnode(self.titleNode) self.addSubnode(self.subtitleNode) self.addSubnode(self.animationNode) self.addSubnode(self.buttonNode) self.buttonNode.addSubnode(self.buttonStarsNode) self.addSubnode(self.buttonTitleNode) self.buttonNode.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { if highlighted { strongSelf.buttonNode.layer.removeAnimation(forKey: "opacity") strongSelf.buttonNode.alpha = 0.4 strongSelf.buttonTitleNode.layer.removeAnimation(forKey: "opacity") strongSelf.buttonTitleNode.alpha = 0.4 } else { strongSelf.buttonNode.alpha = 1.0 strongSelf.buttonNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) strongSelf.buttonTitleNode.alpha = 1.0 strongSelf.buttonTitleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) } } } self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) } required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { self.fetchDisposable?.dispose() self.currentProgressDisposable?.dispose() } @objc private func buttonPressed() { guard let item = self.item else { return } let _ = item.controllerInteraction.openMessage(item.message, OpenMessageParams(mode: .default, progress: self.makeProgress())) } private func makeProgress() -> Promise { let progress = Promise() self.currentProgressDisposable?.dispose() self.currentProgressDisposable = (progress.get() |> distinctUntilChanged |> deliverOnMainQueue).start(next: { [weak self] hasProgress in guard let self else { return } self.displayProgress = hasProgress }) return progress } private var displayProgress = false { didSet { if self.displayProgress != oldValue { if self.displayProgress { self.startShimmering() } else { self.stopShimmering() } } } } private func startShimmering() { let shimmerEffectNode: ShimmerEffectForegroundNode if let current = self.shimmerEffectNode { shimmerEffectNode = current } else { shimmerEffectNode = ShimmerEffectForegroundNode() shimmerEffectNode.cornerRadius = 17.0 self.buttonNode.insertSubnode(shimmerEffectNode, at: 0) self.shimmerEffectNode = shimmerEffectNode } shimmerEffectNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) let backgroundFrame = self.buttonNode.frame shimmerEffectNode.frame = CGRect(origin: .zero, size: backgroundFrame.size) shimmerEffectNode.updateAbsoluteRect(CGRect(origin: .zero, size: backgroundFrame.size), within: backgroundFrame.size) shimmerEffectNode.update(backgroundColor: .clear, foregroundColor: UIColor.white.withAlphaComponent(0.15), horizontal: true, effectSize: nil, globalTimeOffset: false, duration: nil) } private func stopShimmering() { guard let shimmerEffectNode = self.shimmerEffectNode else { return } self.shimmerEffectNode = nil shimmerEffectNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak shimmerEffectNode] _ in shimmerEffectNode?.removeFromSupernode() }) } private func removePlaceholder(animated: Bool) { self.placeholderNode.alpha = 0.0 if !animated { self.placeholderNode.removeFromSupernode() } else { self.placeholderNode.layer.animateAlpha(from: self.placeholderNode.alpha, to: 0.0, duration: 0.2, completion: { [weak self] _ in self?.placeholderNode.removeFromSupernode() }) } } override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, unboundSize: CGSize?, maxWidth: CGFloat, layout: (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { let makeLabelLayout = TextNode.asyncLayout(self.labelNode) let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode) let makeButtonTitleLayout = TextNode.asyncLayout(self.buttonTitleNode) let cachedMaskBackgroundImage = self.cachedMaskBackgroundImage return { item, layoutConstants, _, _, _, _ in let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .always, forceFullCorners: false, forceAlignment: .center) return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in var giftSize = CGSize(width: 220.0, height: 240.0) let incoming = item.message.effectivelyIncoming(item.context.account.peerId) let attributedString = attributedServiceMessageString(theme: item.presentationData.theme, strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, message: EngineMessage(item.message), accountPeerId: item.context.account.peerId) let primaryTextColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText var months: Int32 = 3 var animationName: String = "" var animationFile: TelegramMediaFile? var title = item.presentationData.strings.Notification_PremiumGift_Title var text = "" var buttonTitle = item.presentationData.strings.Notification_PremiumGift_View var hasServiceMessage = true var textSpacing: CGFloat = 0.0 for media in item.message.media { if let action = media as? TelegramMediaAction { switch action.action { case let .giftPremium(_, _, monthsValue, _, _): months = monthsValue text = item.presentationData.strings.Notification_PremiumGift_Subtitle(item.presentationData.strings.Notification_PremiumGift_Months(months)).string case let .giftStars(_, _, count, _, _, _): if count <= 1000 { months = 3 } else if count < 2500 { months = 6 } else { months = 12 } var peerName = "" if let peer = item.message.peers[item.message.id.peerId] { peerName = EnginePeer(peer).compactDisplayTitle } title = item.presentationData.strings.Notification_StarsGift_Title(Int32(count)) text = incoming ? item.presentationData.strings.Notification_StarsGift_Subtitle : item.presentationData.strings.Notification_StarsGift_SubtitleYou(peerName).string case let .prizeStars(count, _, channelId, _, _): if count <= 1000 { months = 3 } else if count < 2500 { months = 6 } else { months = 12 } var peerName = "" if let channelId, let channel = item.message.peers[channelId] { peerName = EnginePeer(channel).compactDisplayTitle } title = item.presentationData.strings.Notification_StarsGiveaway_Title text = item.presentationData.strings.Notification_StarsGiveaway_Subtitle(peerName, item.presentationData.strings.Notification_StarsGiveaway_Subtitle_Stars(Int32(count))).string case let .giftCode(_, fromGiveaway, unclaimed, channelId, monthsValue, _, _, _, _): if channelId == nil { months = monthsValue text = item.presentationData.strings.Notification_PremiumGift_Subtitle(item.presentationData.strings.Notification_PremiumGift_Months(months)).string if item.message.author?.id != item.context.account.peerId { buttonTitle = item.presentationData.strings.Notification_PremiumGift_UseGift } } else { giftSize.width += 34.0 textSpacing += 13.0 if unclaimed { title = item.presentationData.strings.Notification_PremiumPrize_Unclaimed } else { title = item.presentationData.strings.Notification_PremiumPrize_Title } var peerName = "" if let channelId, let channel = item.message.peers[channelId] { peerName = EnginePeer(channel).compactDisplayTitle } if unclaimed { text = item.presentationData.strings.Notification_PremiumPrize_UnclaimedText(peerName, item.presentationData.strings.Notification_PremiumPrize_Months(monthsValue)).string } else if fromGiveaway { text = item.presentationData.strings.Notification_PremiumPrize_GiveawayText(peerName, item.presentationData.strings.Notification_PremiumPrize_Months(monthsValue)).string } else { text = item.presentationData.strings.Notification_PremiumPrize_GiftText(peerName, item.presentationData.strings.Notification_PremiumPrize_Months(monthsValue)).string } months = monthsValue buttonTitle = item.presentationData.strings.Notification_PremiumPrize_View hasServiceMessage = false } case let .starGift(gift, convertStars, giftText, entities, nameHidden, savedToProfile, converted)://(amount, giftId, nameHidden, limitNumber, limitTotal, giftText, _): let _ = nameHidden let authorName = item.message.author.flatMap { EnginePeer($0) }?.compactDisplayTitle ?? "" title = "Gift from \(authorName)" if let giftText, !giftText.isEmpty { text = giftText let _ = entities } else { if incoming { if converted { text = "You converted this gift to \(convertStars) Stars." } else if savedToProfile { text = "You are displaying this gift on your page. You can also convert it to \(convertStars) Stars." } else { text = "Display this gift on your page or convert it to \(convertStars) Stars." } } else { var peerName = "" if let peer = item.message.peers[item.message.id.peerId] { peerName = EnginePeer(peer).compactDisplayTitle } if peerName.isEmpty { text = "Display this gift on your page or convert it to \(convertStars) Stars." } else { text = "\(peerName) can keep this gift on their page or convert it to \(convertStars) Stars." } } } animationFile = gift.file default: break } } } switch months { case 12: animationName = "Gift12" case 6: animationName = "Gift6" case 3: animationName = "Gift3" default: animationName = "Gift3" } let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: attributedString, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: constrainedSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: Font.semibold(15.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes( body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: primaryTextColor), bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: primaryTextColor), link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: primaryTextColor), linkAttribute: { url in return ("URL", url) } ), textAlignment: .center) let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) let (buttonTitleLayout, buttonTitleApply) = makeButtonTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: buttonTitle, font: Font.semibold(15.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) giftSize.height = titleLayout.size.height + textSpacing + subtitleLayout.size.height + 212.0 var labelRects = labelLayout.linesRects() if labelRects.count > 1 { let sortedIndices = (0 ..< labelRects.count).sorted(by: { labelRects[$0].width > labelRects[$1].width }) for i in 0 ..< sortedIndices.count { let index = sortedIndices[i] for j in -1 ... 1 { if j != 0 && index + j >= 0 && index + j < sortedIndices.count { if abs(labelRects[index + j].width - labelRects[index].width) < 40.0 { labelRects[index + j].size.width = max(labelRects[index + j].width, labelRects[index].width) labelRects[index].size.width = labelRects[index + j].size.width } } } } } for i in 0 ..< labelRects.count { labelRects[i] = labelRects[i].insetBy(dx: -6.0, dy: floor((labelRects[i].height - 20.0) / 2.0)) labelRects[i].size.height = 20.0 labelRects[i].origin.x = floor((labelLayout.size.width - labelRects[i].width) / 2.0) } let backgroundMaskImage: (CGPoint, UIImage)? var backgroundMaskUpdated = false if hasServiceMessage { if let (currentOffset, currentImage, currentRects) = cachedMaskBackgroundImage, currentRects == labelRects { backgroundMaskImage = (currentOffset, currentImage) } else { backgroundMaskImage = LinkHighlightingNode.generateImage(color: .black, inset: 0.0, innerRadius: 10.0, outerRadius: 10.0, rects: labelRects, useModernPathCalculation: false) backgroundMaskUpdated = true } } else { backgroundMaskImage = nil } var backgroundSize = CGSize(width: labelLayout.size.width + 8.0 + 8.0, height: giftSize.height) if hasServiceMessage { backgroundSize.height += labelLayout.size.height + 18.0 } else { backgroundSize.height += 4.0 } return (backgroundSize.width, { boundingWidth in return (backgroundSize, { [weak self] animation, synchronousLoads, _ in if let strongSelf = self { if strongSelf.item == nil { strongSelf.animationNode.autoplay = true if let file = animationFile { strongSelf.animationNode.setup(source: AnimatedStickerResourceSource(account: item.context.account, resource: file.resource), width: 384, height: 384, playbackMode: .once, mode: .direct(cachePathPrefix: nil)) if strongSelf.fetchDisposable == nil { strongSelf.fetchDisposable = freeMediaFileResourceInteractiveFetched(postbox: item.context.account.postbox, userLocation: .other, fileReference: .message(message: MessageReference(item.message), media: file), resource: file.resource).start() } } else if animationName.hasPrefix("Gift") { strongSelf.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: animationName), width: 384, height: 384, playbackMode: .still(.end), mode: .direct(cachePathPrefix: nil)) } } strongSelf.item = item strongSelf.updateVisibility() strongSelf.labelNode.isHidden = !hasServiceMessage let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - giftSize.width) / 2.0), y: hasServiceMessage ? labelLayout.size.height + 16.0 : 0.0), size: giftSize) let mediaBackgroundFrame = imageFrame.insetBy(dx: -2.0, dy: -2.0) strongSelf.mediaBackgroundNode.frame = mediaBackgroundFrame strongSelf.mediaBackgroundNode.updateColor(color: selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), enableBlur: item.controllerInteraction.enableFullTranslucency && dateFillNeedsBlur(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), transition: .immediate) strongSelf.mediaBackgroundNode.update(size: mediaBackgroundFrame.size, transition: .immediate) strongSelf.buttonNode.backgroundColor = item.presentationData.theme.theme.overallDarkAppearance ? UIColor(rgb: 0xffffff, alpha: 0.12) : UIColor(rgb: 0x000000, alpha: 0.12) var iconSize = CGSize(width: 160.0, height: 160.0) var iconOffset: CGFloat = 0.0 if let _ = animationFile { iconSize = CGSize(width: 120.0, height: 120.0) iconOffset = 32.0 } strongSelf.animationNode.frame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - iconSize.width) / 2.0), y: mediaBackgroundFrame.minY - 16.0 + iconOffset), size: iconSize) strongSelf.animationNode.updateLayout(size: iconSize) let _ = labelApply() let _ = titleApply() let _ = subtitleApply() let _ = buttonTitleApply() let labelFrame = CGRect(origin: CGPoint(x: 8.0, y: 2.0), size: labelLayout.size) strongSelf.labelNode.frame = labelFrame let titleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - titleLayout.size.width) / 2.0) , y: mediaBackgroundFrame.minY + 151.0), size: titleLayout.size) strongSelf.titleNode.frame = titleFrame let subtitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0) , y: titleFrame.maxY + textSpacing), size: subtitleLayout.size) strongSelf.subtitleNode.frame = subtitleFrame let buttonTitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonTitleLayout.size.width) / 2.0), y: subtitleFrame.maxY + 18.0), size: buttonTitleLayout.size) strongSelf.buttonTitleNode.frame = buttonTitleFrame let buttonSize = CGSize(width: buttonTitleLayout.size.width + 38.0, height: 34.0) strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonSize.width) / 2.0), y: subtitleFrame.maxY + 10.0), size: buttonSize) strongSelf.buttonStarsNode.frame = CGRect(origin: .zero, size: buttonSize) if item.controllerInteraction.presentationContext.backgroundNode?.hasExtraBubbleBackground() == true { if strongSelf.mediaBackgroundContent == nil, let backgroundContent = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) { strongSelf.mediaBackgroundNode.isHidden = true backgroundContent.clipsToBounds = true backgroundContent.allowsGroupOpacity = true backgroundContent.cornerRadius = 24.0 strongSelf.mediaBackgroundContent = backgroundContent strongSelf.insertSubnode(backgroundContent, at: 0) } strongSelf.mediaBackgroundContent?.frame = mediaBackgroundFrame } else { strongSelf.mediaBackgroundNode.isHidden = false strongSelf.mediaBackgroundContent?.removeFromSupernode() strongSelf.mediaBackgroundContent = nil } let baseBackgroundFrame = labelFrame.offsetBy(dx: 0.0, dy: -11.0) if let (offset, image) = backgroundMaskImage { if strongSelf.backgroundNode == nil { if let backgroundNode = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) { strongSelf.backgroundNode = backgroundNode strongSelf.insertSubnode(backgroundNode, at: 0) } } if backgroundMaskUpdated, let backgroundNode = strongSelf.backgroundNode { if labelRects.count == 1 { backgroundNode.clipsToBounds = true backgroundNode.cornerRadius = labelRects[0].height / 2.0 backgroundNode.view.mask = nil } else { backgroundNode.clipsToBounds = false backgroundNode.cornerRadius = 0.0 backgroundNode.view.mask = strongSelf.backgroundMaskNode.view } } if let backgroundNode = strongSelf.backgroundNode { backgroundNode.frame = CGRect(origin: CGPoint(x: baseBackgroundFrame.minX + offset.x, y: baseBackgroundFrame.minY + offset.y), size: image.size) } strongSelf.backgroundMaskNode.image = image strongSelf.backgroundMaskNode.frame = CGRect(origin: CGPoint(), size: image.size) strongSelf.cachedMaskBackgroundImage = (offset, image, labelRects) } if let (rect, size) = strongSelf.absoluteRect { strongSelf.updateAbsoluteRect(rect, within: size) } } }) }) }) } } override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { self.absoluteRect = (rect, containerSize) if let mediaBackgroundContent = self.mediaBackgroundContent { var backgroundFrame = mediaBackgroundContent.frame backgroundFrame.origin.x += rect.minX backgroundFrame.origin.y += rect.minY mediaBackgroundContent.update(rect: backgroundFrame, within: containerSize, transition: .immediate) } self.placeholderNode.updateAbsoluteRect(CGRect(origin: CGPoint(x: rect.minX + self.placeholderNode.frame.minX, y: rect.minY + self.placeholderNode.frame.minY), size: self.placeholderNode.frame.size), within: containerSize) if let backgroundNode = self.backgroundNode { var backgroundFrame = backgroundNode.frame backgroundFrame.origin.x += rect.minX backgroundFrame.origin.y += rect.minY backgroundNode.update(rect: backgroundFrame, within: containerSize, transition: .immediate) } } override public func applyAbsoluteOffset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { if let backgroundNode = self.backgroundNode { backgroundNode.offset(value: value, animationCurve: animationCurve, duration: duration) } } override public func applyAbsoluteOffsetSpring(value: CGFloat, duration: Double, damping: CGFloat) { if let backgroundNode = self.backgroundNode { backgroundNode.offsetSpring(value: value, duration: duration, damping: damping) } } override public func updateTouchesAtPoint(_ point: CGPoint?) { if let item = self.item { var rects: [(CGRect, CGRect)]? let textNodeFrame = self.labelNode.frame if let point = point { if let (index, attributes) = self.labelNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY - 10.0)) { let possibleNames: [String] = [ TelegramTextAttributes.URL, TelegramTextAttributes.PeerMention, TelegramTextAttributes.PeerTextMention, TelegramTextAttributes.BotCommand, TelegramTextAttributes.Hashtag ] for name in possibleNames { if let _ = attributes[NSAttributedString.Key(rawValue: name)] { rects = self.labelNode.lineAndAttributeRects(name: name, at: index) break } } } } if let rects = rects { var mappedRects: [CGRect] = [] for i in 0 ..< rects.count { let lineRect = rects[i].0 var itemRect = rects[i].1 itemRect.origin.x = floor((textNodeFrame.size.width - lineRect.width) / 2.0) + itemRect.origin.x mappedRects.append(itemRect) } let linkHighlightingNode: LinkHighlightingNode if let current = self.linkHighlightingNode { linkHighlightingNode = current } else { let serviceColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper) linkHighlightingNode = LinkHighlightingNode(color: serviceColor.linkHighlight) linkHighlightingNode.inset = 2.5 self.linkHighlightingNode = linkHighlightingNode self.insertSubnode(linkHighlightingNode, belowSubnode: self.labelNode) } linkHighlightingNode.frame = self.labelNode.frame.offsetBy(dx: 0.0, dy: 1.5) linkHighlightingNode.updateRects(mappedRects) } 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 public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { let textNodeFrame = self.labelNode.frame if let (index, attributes) = self.labelNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY - 10.0)), gesture == .tap { if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { var concealed = true if let (attributeText, fullText) = self.labelNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) { concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText) } return ChatMessageBubbleContentTapAction(content: .url(ChatMessageBubbleContentTapAction.Url(url: url, concealed: concealed))) } else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention { return ChatMessageBubbleContentTapAction(content: .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false)) } else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { return ChatMessageBubbleContentTapAction(content: .textMention(peerName)) } else if let botCommand = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String { return ChatMessageBubbleContentTapAction(content: .botCommand(botCommand)) } else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag { return ChatMessageBubbleContentTapAction(content: .hashtag(hashtag.peerName, hashtag.hashtag)) } } if self.buttonNode.frame.contains(point) { return ChatMessageBubbleContentTapAction(content: .ignore) } else if let backgroundNode = self.backgroundNode, backgroundNode.frame.contains(point) { return ChatMessageBubbleContentTapAction(content: .openMessage) } else if self.mediaBackgroundNode.frame.contains(point) { return ChatMessageBubbleContentTapAction(content: .openMessage) } else { return ChatMessageBubbleContentTapAction(content: .none) } } override public func unreadMessageRangeUpdated() { self.updateVisibility() } private func updateVisibility() { guard let item = self.item else { return } let isPlaying = self.visibilityStatus == true if self.isPlaying != isPlaying { self.isPlaying = isPlaying self.animationNode.visibility = isPlaying } if isPlaying && self.setupTimestamp == nil { self.setupTimestamp = CACurrentMediaTime() } if isPlaying { var alreadySeen = true if item.message.flags.contains(.Incoming) { if let unreadRange = item.controllerInteraction.unreadMessageRange[UnreadMessageRangeKey(peerId: item.message.id.peerId, namespace: item.message.id.namespace)] { if unreadRange.contains(item.message.id.id) { alreadySeen = false } } } else { if item.controllerInteraction.playNextOutgoingGift && !item.controllerInteraction.seenOneTimeAnimatedMedia.contains(item.message.id) { alreadySeen = false } } if !item.controllerInteraction.seenOneTimeAnimatedMedia.contains(item.message.id) { item.controllerInteraction.seenOneTimeAnimatedMedia.insert(item.message.id) self.animationNode.playOnce() Queue.mainQueue().after(0.05) { if let itemNode = self.itemNode, let supernode = itemNode.supernode { supernode.addSubnode(itemNode) } } } if !alreadySeen && self.animationNode.isPlaying { item.controllerInteraction.playNextOutgoingGift = false Queue.mainQueue().after(1.0) { item.controllerInteraction.animateDiceSuccess(false, true) } } } } }