import Foundation import UIKit import Display import AsyncDisplayKit import Postbox import TelegramCore import SwiftSignalKit import TelegramPresentationData import TelegramUIPreferences import AccountContext import StickerResources import PhotoResources import TelegramStringFormatting import AnimatedCountLabelNode import AnimatedNavigationStripeNode import ContextUI import RadialStatusNode import InvisibleInkDustNode import TextFormat import ChatPresentationInterfaceState import TextNodeWithEntities import AnimationCache import MultiAnimationRenderer import TranslateUI import ChatControllerInteraction private enum PinnedMessageAnimation { case slideToTop case slideToBottom } private final class ButtonsContainerNode: ASDisplayNode { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if let subnodes = self.subnodes { for subnode in subnodes { if let result = subnode.view.hitTest(self.view.convert(point, to: subnode.view), with: event) { return result } } } return nil } } final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { private let context: AccountContext private let tapButton: HighlightTrackingButtonNode private let buttonsContainer: ButtonsContainerNode private let closeButton: HighlightableButtonNode private let listButton: HighlightableButtonNode private let activityIndicatorContainer: ASDisplayNode private let activityIndicator: RadialStatusNode private let contextContainer: ContextControllerSourceNode private let clippingContainer: ASDisplayNode private let contentContainer: ASDisplayNode private let contentTextContainer: ASDisplayNode private let lineNode: AnimatedNavigationStripeNode private let titleNode: AnimatedCountLabelNode private let textNode: TextNodeWithEntities private var spoilerTextNode: TextNodeWithEntities? private var dustNode: InvisibleInkDustNode? private let actionButton: HighlightableButtonNode private let actionButtonTitleNode: ImmediateTextNode private let actionButtonBackgroundNode: ASImageNode private let imageNode: TransformImageNode private let imageNodeContainer: ASDisplayNode private let separatorNode: ASDisplayNode private var currentLayout: (CGFloat, CGFloat, CGFloat)? private var currentMessage: ChatPinnedMessage? private var previousMediaReference: AnyMediaReference? private var currentTranslateToLanguage: (fromLang: String, toLang: String)? private let translationDisposable = MetaDisposable() private var isReplyThread: Bool = false private let fetchDisposable = MetaDisposable() private var statusDisposable: Disposable? private let animationCache: AnimationCache? private let animationRenderer: MultiAnimationRenderer? private let queue = Queue() override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if let buttonResult = self.buttonsContainer.hitTest(point.offsetBy(dx: -self.buttonsContainer.frame.minX, dy: -self.buttonsContainer.frame.minY), with: event) { return buttonResult } let containerResult = self.contentTextContainer.hitTest(point.offsetBy(dx: -self.contentTextContainer.frame.minX, dy: -self.contentTextContainer.frame.minY), with: event) if containerResult?.asyncdisplaykit_node === self.dustNode, self.dustNode?.isRevealed == false { return containerResult } let result = super.hitTest(point, with: event) return result } init(context: AccountContext, animationCache: AnimationCache?, animationRenderer: MultiAnimationRenderer?) { self.context = context self.animationCache = animationCache self.animationRenderer = animationRenderer self.tapButton = HighlightTrackingButtonNode() self.buttonsContainer = ButtonsContainerNode() self.closeButton = HighlightableButtonNode() self.closeButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0) self.closeButton.displaysAsynchronously = false self.actionButton = HighlightableButtonNode() self.actionButton.isHidden = true self.actionButtonTitleNode = ImmediateTextNode() self.actionButtonTitleNode.isHidden = true self.actionButtonBackgroundNode = ASImageNode() self.actionButtonBackgroundNode.isHidden = true self.actionButtonBackgroundNode.displaysAsynchronously = false self.listButton = HighlightableButtonNode() self.listButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0) self.listButton.displaysAsynchronously = false self.activityIndicatorContainer = ASDisplayNode() self.activityIndicatorContainer.isUserInteractionEnabled = false self.activityIndicator = RadialStatusNode(backgroundNodeColor: .clear) self.activityIndicator.isUserInteractionEnabled = false self.activityIndicatorContainer.addSubnode(self.activityIndicator) self.activityIndicator.alpha = 0.0 ContainedViewLayoutTransition.immediate.updateSublayerTransformScale(node: self.activityIndicatorContainer, scale: 0.1) self.separatorNode = ASDisplayNode() self.separatorNode.isLayerBacked = true self.contextContainer = ContextControllerSourceNode() self.clippingContainer = ASDisplayNode() self.clippingContainer.clipsToBounds = true self.contentContainer = ASDisplayNode() self.contentTextContainer = ASDisplayNode() self.lineNode = AnimatedNavigationStripeNode() self.titleNode = AnimatedCountLabelNode() self.titleNode.isUserInteractionEnabled = false self.titleNode.reverseAnimationDirection = true self.textNode = TextNodeWithEntities() self.textNode.textNode.displaysAsynchronously = false self.textNode.textNode.isUserInteractionEnabled = false self.imageNode = TransformImageNode() self.imageNode.contentAnimations = [.subsequentUpdates] self.imageNodeContainer = ASDisplayNode() super.init() self.tapButton.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { if highlighted { strongSelf.titleNode.layer.removeAnimation(forKey: "opacity") strongSelf.titleNode.alpha = 0.4 strongSelf.textNode.textNode.layer.removeAnimation(forKey: "opacity") strongSelf.textNode.textNode.alpha = 0.4 strongSelf.lineNode.layer.removeAnimation(forKey: "opacity") strongSelf.lineNode.alpha = 0.4 } else { strongSelf.titleNode.alpha = 1.0 strongSelf.titleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) strongSelf.textNode.textNode.alpha = 1.0 strongSelf.textNode.textNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) strongSelf.lineNode.alpha = 1.0 strongSelf.lineNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) } } } self.actionButton.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { if highlighted { strongSelf.actionButton.layer.removeAnimation(forKey: "opacity") strongSelf.actionButton.alpha = 0.4 } else { strongSelf.actionButton.alpha = 1.0 strongSelf.actionButton.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) } } } self.closeButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: [.touchUpInside]) self.listButton.addTarget(self, action: #selector(self.listPressed), forControlEvents: [.touchUpInside]) self.actionButton.addTarget(self, action: #selector(self.actionButtonPressed), forControlEvents: [.touchUpInside]) self.addSubnode(self.contextContainer) self.contextContainer.addSubnode(self.clippingContainer) self.clippingContainer.addSubnode(self.contentContainer) self.contextContainer.addSubnode(self.lineNode) self.contentTextContainer.addSubnode(self.titleNode) self.contentTextContainer.addSubnode(self.textNode.textNode) self.contentContainer.addSubnode(self.contentTextContainer) self.imageNodeContainer.addSubnode(self.imageNode) self.contentContainer.addSubnode(self.imageNodeContainer) self.actionButton.addSubnode(self.actionButtonBackgroundNode) self.actionButton.addSubnode(self.actionButtonTitleNode) self.buttonsContainer.addSubnode(self.actionButton) self.buttonsContainer.addSubnode(self.closeButton) self.buttonsContainer.addSubnode(self.listButton) self.contextContainer.addSubnode(self.buttonsContainer) self.contextContainer.addSubnode(self.activityIndicatorContainer) self.tapButton.addTarget(self, action: #selector(self.tapped), forControlEvents: [.touchUpInside]) self.contextContainer.addSubnode(self.tapButton) self.addSubnode(self.separatorNode) self.contextContainer.activated = { [weak self] gesture, _ in guard let strongSelf = self else { return } if let interfaceInteraction = strongSelf.interfaceInteraction, let _ = strongSelf.currentMessage, !strongSelf.isReplyThread { interfaceInteraction.activatePinnedListPreview(strongSelf.contextContainer, gesture) } } } deinit { self.fetchDisposable.dispose() self.statusDisposable?.dispose() self.translationDisposable.dispose() } private var theme: PresentationTheme? override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> LayoutResult { let panelHeight: CGFloat = 50.0 var themeUpdated = false self.contextContainer.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: panelHeight)) if self.theme !== interfaceState.theme { themeUpdated = true self.theme = interfaceState.theme self.closeButton.setImage(PresentationResourcesChat.chatInputPanelCloseIconImage(interfaceState.theme), for: []) self.listButton.setImage(PresentationResourcesChat.chatInputPanelPinnedListIconImage(interfaceState.theme), for: []) self.separatorNode.backgroundColor = interfaceState.theme.rootController.navigationBar.separatorColor self.actionButtonBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 14.0 * 2.0, color: interfaceState.theme.list.itemCheckColors.fillColor, strokeColor: nil, strokeWidth: nil, backgroundColor: nil) } if self.statusDisposable == nil, let interfaceInteraction = self.interfaceInteraction, let statuses = interfaceInteraction.statuses { self.statusDisposable = (statuses.loadingMessage |> map { status -> Bool in return status == .pinnedMessage } |> deliverOnMainQueue).startStrict(next: { [weak self] isLoading in guard let strongSelf = self else { return } let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .easeInOut) if isLoading { if strongSelf.activityIndicator.alpha.isZero { transition.updateAlpha(node: strongSelf.activityIndicator, alpha: 1.0) transition.updateSublayerTransformScale(node: strongSelf.activityIndicatorContainer, scale: 1.0) transition.updateAlpha(node: strongSelf.buttonsContainer, alpha: 0.0) transition.updateSublayerTransformScale(node: strongSelf.buttonsContainer, scale: 0.1) if let theme = strongSelf.theme { strongSelf.activityIndicator.transitionToState(.progress(color: theme.chat.inputPanel.panelControlAccentColor, lineWidth: nil, value: nil, cancelEnabled: false, animateRotation: true), animated: false, completion: { }) } } } else { if !strongSelf.activityIndicator.alpha.isZero { transition.updateAlpha(node: strongSelf.activityIndicator, alpha: 0.0, completion: { [weak self] completed in if completed { self?.activityIndicator.transitionToState(.none, animated: false, completion: { }) } }) transition.updateSublayerTransformScale(node: strongSelf.activityIndicatorContainer, scale: 0.1) transition.updateAlpha(node: strongSelf.buttonsContainer, alpha: 1.0) transition.updateSublayerTransformScale(node: strongSelf.buttonsContainer, scale: 1.0) } } }) } let isReplyThread: Bool if case let .replyThread(message) = interfaceState.chatLocation, !message.isForumPost { isReplyThread = true } else { isReplyThread = false } self.isReplyThread = isReplyThread self.contextContainer.isGestureEnabled = !isReplyThread var actionTitle: String? var messageUpdated = false var messageUpdatedAnimation: PinnedMessageAnimation? if let currentMessage = self.currentMessage, let pinnedMessage = interfaceState.pinnedMessage { if currentMessage != pinnedMessage { messageUpdated = true } if currentMessage.message.id != pinnedMessage.message.id { if currentMessage.message.id < pinnedMessage.message.id { messageUpdatedAnimation = .slideToTop } else { messageUpdatedAnimation = .slideToBottom } } } else if (self.currentMessage != nil) != (interfaceState.pinnedMessage != nil) { messageUpdated = true } var isStarsPayment = false if let message = interfaceState.pinnedMessage, !message.message.isRestricted(platform: "ios", contentSettings: self.context.currentContentSettings.with { $0 }) { for attribute in message.message.attributes { if let attribute = attribute as? ReplyMarkupMessageAttribute, attribute.flags.contains(.inline), attribute.rows.count == 1, attribute.rows[0].buttons.count == 1 { let title = attribute.rows[0].buttons[0].title actionTitle = title if case .payment = attribute.rows[0].buttons[0].action, title.contains("⭐️") { isStarsPayment = true } } } } else { actionTitle = nil } var displayCloseButton = false var displayListButton = false if isReplyThread || actionTitle != nil { displayCloseButton = false displayListButton = false } else if let message = interfaceState.pinnedMessage { if message.totalCount > 1 { displayCloseButton = false displayListButton = true } else { displayCloseButton = true displayListButton = false } } else { displayCloseButton = false displayListButton = true } if displayCloseButton != !self.closeButton.isHidden { if transition.isAnimated { if displayCloseButton { self.closeButton.isHidden = false self.closeButton.layer.removeAllAnimations() self.closeButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.closeButton.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2) } else { self.closeButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak self] completed in guard let strongSelf = self, completed else { return } strongSelf.closeButton.isHidden = true strongSelf.closeButton.layer.removeAllAnimations() }) self.closeButton.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) } } else { self.closeButton.isHidden = !displayCloseButton self.closeButton.layer.removeAllAnimations() } } if displayListButton != !self.listButton.isHidden { if transition.isAnimated { if displayListButton { self.listButton.isHidden = false self.listButton.layer.removeAllAnimations() self.listButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.listButton.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2) } else { self.listButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak self] completed in guard let strongSelf = self, completed else { return } strongSelf.listButton.isHidden = true strongSelf.listButton.layer.removeAllAnimations() }) self.listButton.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) } } else { self.listButton.isHidden = !displayListButton self.listButton.layer.removeAllAnimations() } } let rightInset: CGFloat = 18.0 + rightInset var tapButtonRightInset: CGFloat = rightInset let buttonsContainerSize = CGSize(width: 16.0, height: panelHeight) self.buttonsContainer.frame = CGRect(origin: CGPoint(x: width - buttonsContainerSize.width - rightInset, y: 0.0), size: buttonsContainerSize) let closeButtonSize = self.closeButton.measure(CGSize(width: 100.0, height: 100.0)) if let actionTitle = actionTitle { var actionButtonTransition = transition var animateButtonIn = false if self.actionButton.isHidden { actionButtonTransition = .immediate animateButtonIn = true } else if transition.isAnimated, messageUpdated, actionTitle != self.actionButtonTitleNode.attributedText?.string { if let buttonSnapshot = self.actionButton.view.snapshotView(afterScreenUpdates: false) { animateButtonIn = true buttonSnapshot.frame = self.actionButton.frame self.actionButton.view.superview?.insertSubview(buttonSnapshot, belowSubview: self.actionButton.view) buttonSnapshot.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak buttonSnapshot] _ in buttonSnapshot?.removeFromSuperview() }) buttonSnapshot.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2) } } self.actionButton.isHidden = false self.actionButtonBackgroundNode.isHidden = false self.actionButtonTitleNode.isHidden = false let attributedTitle: NSAttributedString if isStarsPayment { let updatedTitle = actionTitle.replacingOccurrences(of: "⭐️", with: " # ") let buttonAttributedString = NSMutableAttributedString(string: updatedTitle, font: Font.with(size: 15.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: interfaceState.theme.list.itemCheckColors.foregroundColor) if let range = buttonAttributedString.string.range(of: "#"), let starImage = UIImage(bundleImageName: "Item List/PremiumIcon") { buttonAttributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: buttonAttributedString.string)) buttonAttributedString.addAttribute(.foregroundColor, value: interfaceState.theme.list.itemCheckColors.foregroundColor, range: NSRange(range, in: buttonAttributedString.string)) buttonAttributedString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: buttonAttributedString.string)) } attributedTitle = buttonAttributedString } else { attributedTitle = NSAttributedString(string: actionTitle, font: Font.with(size: 15.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: interfaceState.theme.list.itemCheckColors.foregroundColor) } self.actionButtonTitleNode.attributedText = attributedTitle let actionButtonTitleSize = self.actionButtonTitleNode.updateLayout(CGSize(width: 150.0, height: .greatestFiniteMagnitude)) let actionButtonSize = CGSize(width: max(actionButtonTitleSize.width + 20.0, 40.0), height: 28.0) let actionButtonFrame = CGRect(origin: CGPoint(x: buttonsContainerSize.width + 11.0 - actionButtonSize.width, y: floor((panelHeight - actionButtonSize.height) / 2.0)), size: actionButtonSize) actionButtonTransition.updateFrame(node: self.actionButton, frame: actionButtonFrame) actionButtonTransition.updateFrame(node: self.actionButtonBackgroundNode, frame: CGRect(origin: CGPoint(), size: actionButtonFrame.size)) actionButtonTransition.updateFrame(node: self.actionButtonTitleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((actionButtonFrame.width - actionButtonTitleSize.width) / 2.0), y: floorToScreenPixels((actionButtonFrame.height - actionButtonTitleSize.height) / 2.0)), size: actionButtonTitleSize)) tapButtonRightInset = 18.0 + actionButtonFrame.width if animateButtonIn { self.actionButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.actionButton.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2) } } else if !self.actionButton.isHidden { self.actionButton.isHidden = true self.actionButtonBackgroundNode.isHidden = true self.actionButtonTitleNode.isHidden = true if transition.isAnimated { if let buttonSnapshot = self.actionButton.view.snapshotView(afterScreenUpdates: false) { buttonSnapshot.frame = self.actionButton.frame self.actionButton.view.superview?.insertSubview(buttonSnapshot, belowSubview: self.actionButton.view) buttonSnapshot.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak buttonSnapshot] _ in buttonSnapshot?.removeFromSuperview() }) buttonSnapshot.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2) } } } transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(x: buttonsContainerSize.width - closeButtonSize.width + 1.0, y: 19.0), size: closeButtonSize)) let listButtonSize = self.listButton.measure(CGSize(width: 100.0, height: 100.0)) transition.updateFrame(node: self.listButton, frame: CGRect(origin: CGPoint(x: buttonsContainerSize.width - listButtonSize.width + 4.0, y: 13.0), size: listButtonSize)) let indicatorSize = CGSize(width: 22.0, height: 22.0) transition.updateFrame(node: self.activityIndicatorContainer, frame: CGRect(origin: CGPoint(x: width - rightInset - indicatorSize.width + 5.0, y: 15.0), size: indicatorSize)) transition.updateFrame(node: self.activityIndicator, frame: CGRect(origin: CGPoint(), size: indicatorSize)) transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel))) self.tapButton.frame = CGRect(origin: CGPoint(), size: CGSize(width: width - tapButtonRightInset, height: panelHeight)) self.clippingContainer.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: panelHeight)) self.contentContainer.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: panelHeight)) var translateToLanguage: (fromLang: String, toLang: String)? if let translationState = interfaceState.translationState, translationState.isEnabled { translateToLanguage = (normalizeTranslationLanguage(translationState.fromLang), normalizeTranslationLanguage(translationState.toLang)) } var currentTranslateToLanguageUpdated = false if self.currentTranslateToLanguage?.fromLang != translateToLanguage?.fromLang || self.currentTranslateToLanguage?.toLang != translateToLanguage?.toLang { self.currentTranslateToLanguage = translateToLanguage currentTranslateToLanguageUpdated = true } if currentTranslateToLanguageUpdated || messageUpdated, let message = interfaceState.pinnedMessage?.message { if let translation = message.attributes.first(where: { $0 is TranslationMessageAttribute }) as? TranslationMessageAttribute, translation.toLang == translateToLanguage?.toLang { } else if let translateToLanguage { self.translationDisposable.set(translateMessageIds(context: self.context, messageIds: [message.id], fromLang: translateToLanguage.fromLang, toLang: translateToLanguage.toLang).startStrict()) } } if self.currentLayout?.0 != width || self.currentLayout?.1 != leftInset || self.currentLayout?.2 != rightInset || messageUpdated || themeUpdated || currentTranslateToLanguageUpdated { self.currentLayout = (width, leftInset, rightInset) let previousMessageWasNil = self.currentMessage == nil self.currentMessage = interfaceState.pinnedMessage if let currentMessage = self.currentMessage, let currentLayout = self.currentLayout { self.dustNode?.update(revealed: false, animated: false) self.enqueueTransition(width: currentLayout.0, panelHeight: panelHeight, leftInset: currentLayout.1, rightInset: currentLayout.2, transition: .immediate, animation: messageUpdatedAnimation, pinnedMessage: currentMessage, theme: interfaceState.theme, strings: interfaceState.strings, nameDisplayOrder: interfaceState.nameDisplayOrder, dateTimeFormat: interfaceState.dateTimeFormat, accountPeerId: self.context.account.peerId, firstTime: previousMessageWasNil, isReplyThread: isReplyThread, translateToLanguage: translateToLanguage?.toLang) } } self.currentLayout = (width, leftInset, rightInset) /*if self.currentLayout?.0 != width || self.currentLayout?.1 != leftInset || self.currentLayout?.2 != rightInset || messageUpdated { self.currentLayout = (width, leftInset, rightInset) if let currentMessage = self.currentMessage { self.enqueueTransition(width: width, panelHeight: panelHeight, leftInset: leftInset, rightInset: rightInset, transition: .immediate, animation: .none, pinnedMessage: currentMessage, theme: interfaceState.theme, strings: interfaceState.strings, nameDisplayOrder: interfaceState.nameDisplayOrder, dateTimeFormat: interfaceState.dateTimeFormat, accountPeerId: interfaceState.accountPeerId, firstTime: true, isReplyThread: isReplyThread) } }*/ return LayoutResult(backgroundHeight: panelHeight, insetHeight: panelHeight, hitTestSlop: 0.0) } private func enqueueTransition(width: CGFloat, panelHeight: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, animation: PinnedMessageAnimation?, pinnedMessage: ChatPinnedMessage, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, accountPeerId: PeerId, firstTime: Bool, isReplyThread: Bool, translateToLanguage: String?) { let message = pinnedMessage.message var animationTransition: ContainedViewLayoutTransition = .immediate if let animation = animation { animationTransition = .animated(duration: 0.2, curve: .easeInOut) if let copyView = self.textNode.textNode.view.snapshotView(afterScreenUpdates: false) { let offset: CGFloat switch animation { case .slideToTop: offset = -10.0 case .slideToBottom: offset = 10.0 } copyView.frame = self.textNode.textNode.frame self.textNode.textNode.view.superview?.addSubview(copyView) copyView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: offset), duration: 0.2, removeOnCompletion: false, additive: true) copyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak copyView] _ in copyView?.removeFromSuperview() }) self.textNode.textNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -offset), to: CGPoint(), duration: 0.2, additive: true) self.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } } let makeTitleLayout = self.titleNode.asyncLayout() let makeTextLayout = TextNodeWithEntities.asyncLayout(self.textNode) let makeSpoilerTextLayout = TextNodeWithEntities.asyncLayout(self.spoilerTextNode) let imageNodeLayout = self.imageNode.asyncLayout() let previousMediaReference = self.previousMediaReference let context = self.context let targetQueue: Queue if firstTime { targetQueue = Queue.mainQueue() } else { targetQueue = self.queue } let contentLeftInset: CGFloat = leftInset + 10.0 var textLineInset: CGFloat = 10.0 var rightInset: CGFloat = 14.0 + rightInset let textRightInset: CGFloat = 0.0 if !self.actionButton.isHidden { rightInset += self.actionButton.bounds.width - 14.0 } targetQueue.async { [weak self] in var updatedMediaReference: AnyMediaReference? var imageDimensions: CGSize? let giveaway = pinnedMessage.message.media.first(where: { $0 is TelegramMediaGiveaway }) as? TelegramMediaGiveaway var titleStrings: [AnimatedCountLabelNode.Segment] = [] if let _ = giveaway { titleStrings.append(.text(0, NSAttributedString(string: "\(strings.Conversation_PinnedGiveaway) ", font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor))) } else { if pinnedMessage.totalCount == 2 { if pinnedMessage.index == 0 { titleStrings.append(.text(0, NSAttributedString(string: "\(strings.Conversation_PinnedPreviousMessage) ", font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor))) } else { titleStrings.append(.text(0, NSAttributedString(string: "\(strings.Conversation_PinnedMessage) ", font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor))) } } else if pinnedMessage.totalCount > 1 && pinnedMessage.index != pinnedMessage.totalCount - 1 { titleStrings.append(.text(0, NSAttributedString(string: "\(strings.Conversation_PinnedMessage)", font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor))) titleStrings.append(.text(1, NSAttributedString(string: " #", font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor))) titleStrings.append(.number(pinnedMessage.index + 1, NSAttributedString(string: "\(pinnedMessage.index + 1)", font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor))) } else { titleStrings.append(.text(0, NSAttributedString(string: "\(strings.Conversation_PinnedMessage) ", font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor))) } } if !message.containsSecretMedia { for media in message.media { if let image = media as? TelegramMediaImage { updatedMediaReference = .message(message: MessageReference(message), media: image) if let representation = largestRepresentationForPhoto(image) { imageDimensions = representation.dimensions.cgSize } break } else if let file = media as? TelegramMediaFile { updatedMediaReference = .message(message: MessageReference(message), media: file) if !file.isInstantVideo && !file.isSticker, let representation = largestImageRepresentation(file.previewRepresentations) { imageDimensions = representation.dimensions.cgSize } else if file.isAnimated, let dimensions = file.dimensions { imageDimensions = dimensions.cgSize } break } else if let paidContent = media as? TelegramMediaPaidContent, let firstMedia = paidContent.extendedMedia.first { switch firstMedia { case let .preview(dimensions, immediateThumbnailData, _): let thumbnailMedia = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [], immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: []) if let dimensions { imageDimensions = dimensions.cgSize } updatedMediaReference = .standalone(media: thumbnailMedia) case let .full(fullMedia): updatedMediaReference = .message(message: MessageReference(message), media: fullMedia) if let image = fullMedia as? TelegramMediaImage { if let representation = largestRepresentationForPhoto(image) { imageDimensions = representation.dimensions.cgSize } break } else if let file = fullMedia as? TelegramMediaFile { if let dimensions = file.dimensions { imageDimensions = dimensions.cgSize } break } } } } } if isReplyThread { let titleString: String if let author = message.effectiveAuthor { titleString = EnginePeer(author).displayTitle(strings: strings, displayOrder: nameDisplayOrder) } else { titleString = "" } titleStrings = [.text(0, NSAttributedString(string: titleString, font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor))] } else { for media in message.media { if let media = media as? TelegramMediaInvoice { titleStrings = [.text(0, NSAttributedString(string: media.title, font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor))] break } } } var applyImage: (() -> Void)? if let imageDimensions = imageDimensions { let boundingSize = CGSize(width: 35.0, height: 35.0) applyImage = imageNodeLayout(TransformImageArguments(corners: ImageCorners(radius: 2.0), imageSize: imageDimensions.aspectFilled(boundingSize), boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets())) textLineInset += 9.0 + 35.0 } var mediaUpdated = false if let updatedMediaReference = updatedMediaReference, let previousMediaReference = previousMediaReference { mediaUpdated = !updatedMediaReference.media.isEqual(to: previousMediaReference.media) } else if (updatedMediaReference != nil) != (previousMediaReference != nil) { mediaUpdated = true } let hasSpoiler = message.attributes.contains(where: { $0 is MediaSpoilerMessageAttribute }) var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? var updatedFetchMediaSignal: Signal? if mediaUpdated { if let updatedMediaReference = updatedMediaReference, imageDimensions != nil { if let imageReference = updatedMediaReference.concrete(TelegramMediaImage.self) { if imageReference.media.representations.isEmpty { updateImageSignal = chatSecretPhoto(account: context.account, userLocation: .peer(message.id.peerId), photoReference: imageReference, ignoreFullSize: true, synchronousLoad: true) } else { updateImageSignal = chatMessagePhotoThumbnail(account: context.account, userLocation: .peer(message.id.peerId), photoReference: imageReference, blurred: hasSpoiler) } } else if let fileReference = updatedMediaReference.concrete(TelegramMediaFile.self) { if fileReference.media.isAnimatedSticker { let dimensions = fileReference.media.dimensions ?? PixelDimensions(width: 512, height: 512) updateImageSignal = chatMessageAnimatedSticker(postbox: context.account.postbox, userLocation: .peer(message.id.peerId), file: fileReference.media, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0))) updatedFetchMediaSignal = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .peer(message.id.peerId), userContentType: MediaResourceUserContentType(file: fileReference.media), reference: fileReference.resourceReference(fileReference.media.resource)) } else if fileReference.media.isVideo || fileReference.media.isAnimated { updateImageSignal = chatMessageVideoThumbnail(account: context.account, userLocation: .peer(message.id.peerId), fileReference: fileReference, blurred: hasSpoiler) } else if let iconImageRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) { updateImageSignal = chatWebpageSnippetFile(account: context.account, userLocation: .peer(message.id.peerId), mediaReference: fileReference.abstract, representation: iconImageRepresentation) } } } else { updateImageSignal = .single({ _ in return nil }) } } let (titleLayout, titleApply) = makeTitleLayout(CGSize(width: width - textLineInset - contentLeftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), titleStrings) let (textString, _, isText) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: accountPeerId) let messageText: NSAttributedString let textFont = Font.regular(15.0) if let giveaway { let dateString = stringForDateWithoutYear(date: Date(timeIntervalSince1970: TimeInterval(giveaway.untilDate)), timeZone: .current, strings: strings) let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) let isFinished = currentTime >= giveaway.untilDate let text: String if isFinished { let winnersString = strings.Conversation_PinnedGiveaway_Finished_Winners(giveaway.quantity) text = strings.Conversation_PinnedGiveaway_Finished(winnersString, dateString).string } else { let winnersString = strings.Conversation_PinnedGiveaway_Ongoing_Winners(giveaway.quantity) text = strings.Conversation_PinnedGiveaway_Ongoing(winnersString, dateString).string } messageText = NSAttributedString(string: text, font: textFont, textColor: theme.chat.inputPanel.primaryTextColor) } else if isText { var text = message.text var messageEntities = message.textEntitiesAttribute?.entities ?? [] if let translateToLanguage = translateToLanguage, !text.isEmpty { for attribute in message.attributes { if let attribute = attribute as? TranslationMessageAttribute, !attribute.text.isEmpty, attribute.toLang == translateToLanguage { text = attribute.text messageEntities = attribute.entities break } } } let entities = messageEntities.filter { entity in switch entity.type { case .Spoiler, .CustomEmoji: return true default: return false } } let textColor = theme.chat.inputPanel.primaryTextColor if entities.count > 0 { messageText = stringWithAppliedEntities(trimToLineCount(text, lineCount: 1), entities: entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: message) } else { messageText = NSAttributedString(string: foldLineBreaks(text), font: textFont, textColor: textColor) } } else { messageText = NSAttributedString(string: foldLineBreaks(textString.string), font: textFont, textColor: message.media.isEmpty || message.media.first is TelegramMediaWebpage ? theme.chat.inputPanel.primaryTextColor : theme.chat.inputPanel.secondaryTextColor) } let textConstrainedSize = CGSize(width: width - textLineInset - contentLeftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude) let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: messageText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0))) let spoilerTextLayoutAndApply: (TextNodeLayout, (TextNodeWithEntities.Arguments?) -> TextNodeWithEntities)? if !textLayout.spoilers.isEmpty { spoilerTextLayoutAndApply = makeSpoilerTextLayout(TextNodeLayoutArguments(attributedString: messageText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0), displaySpoilers: true, displayEmbeddedItemsUnderSpoilers: true)) } else { spoilerTextLayoutAndApply = nil } Queue.mainQueue().async { if let strongSelf = self { let _ = titleApply(animation != nil) var textArguments: TextNodeWithEntities.Arguments? if let cache = strongSelf.animationCache, let renderer = strongSelf.animationRenderer { textArguments = TextNodeWithEntities.Arguments( context: strongSelf.context, cache: cache, renderer: renderer, placeholderColor: theme.list.mediaPlaceholderColor, attemptSynchronous: false ) } let _ = textApply(textArguments) strongSelf.previousMediaReference = updatedMediaReference animationTransition.updateFrameAdditive(node: strongSelf.contentTextContainer, frame: CGRect(origin: CGPoint(x: contentLeftInset + textLineInset, y: 0.0), size: CGSize(width: width, height: panelHeight))) strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 5.0), size: titleLayout.size) let textFrame = CGRect(origin: CGPoint(x: 0.0, y: 23.0), size: textLayout.size) strongSelf.textNode.textNode.frame = textFrame if let (_, spoilerTextApply) = spoilerTextLayoutAndApply { let spoilerTextNode = spoilerTextApply(textArguments) if strongSelf.spoilerTextNode == nil { spoilerTextNode.textNode.alpha = 0.0 spoilerTextNode.textNode.isUserInteractionEnabled = false spoilerTextNode.textNode.contentMode = .topLeft spoilerTextNode.textNode.contentsScale = UIScreenScale spoilerTextNode.textNode.displaysAsynchronously = false strongSelf.contentTextContainer.insertSubnode(spoilerTextNode.textNode, aboveSubnode: strongSelf.textNode.textNode) strongSelf.spoilerTextNode = spoilerTextNode } strongSelf.spoilerTextNode?.textNode.frame = textFrame let dustNode: InvisibleInkDustNode if let current = strongSelf.dustNode { dustNode = current } else { dustNode = InvisibleInkDustNode(textNode: spoilerTextNode.textNode, enableAnimations: strongSelf.context.sharedContext.energyUsageSettings.fullTranslucency) strongSelf.dustNode = dustNode strongSelf.contentTextContainer.insertSubnode(dustNode, aboveSubnode: spoilerTextNode.textNode) } dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0) dustNode.update(size: dustNode.frame.size, color: theme.chat.inputPanel.secondaryTextColor, textColor: theme.chat.inputPanel.primaryTextColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: textLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }) } else if let spoilerTextNode = strongSelf.spoilerTextNode { strongSelf.spoilerTextNode = nil spoilerTextNode.textNode.removeFromSupernode() if let dustNode = strongSelf.dustNode { strongSelf.dustNode = nil dustNode.removeFromSupernode() } } strongSelf.textNode.visibilityRect = CGRect.infinite strongSelf.spoilerTextNode?.visibilityRect = CGRect.infinite let lineFrame = CGRect(origin: CGPoint(x: contentLeftInset, y: 0.0), size: CGSize(width: 2.0, height: panelHeight)) animationTransition.updateFrame(node: strongSelf.lineNode, frame: lineFrame) strongSelf.lineNode.update( colors: AnimatedNavigationStripeNode.Colors( foreground: theme.chat.inputPanel.panelControlAccentColor, background: theme.chat.inputPanel.panelControlAccentColor.withAlphaComponent(0.5), clearBackground: theme.chat.inputPanel.panelBackgroundColor ), configuration: AnimatedNavigationStripeNode.Configuration( height: panelHeight, index: pinnedMessage.index, count: pinnedMessage.totalCount ), transition: animationTransition ) strongSelf.imageNodeContainer.frame = CGRect(origin: CGPoint(x: contentLeftInset + 9.0, y: 7.0), size: CGSize(width: 35.0, height: 35.0)) strongSelf.imageNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 35.0, height: 35.0)) if let applyImage = applyImage { applyImage() animationTransition.updateSublayerTransformScale(node: strongSelf.imageNodeContainer, scale: 1.0) animationTransition.updateAlpha(node: strongSelf.imageNodeContainer, alpha: 1.0, beginWithCurrentState: true) } else { animationTransition.updateSublayerTransformScale(node: strongSelf.imageNodeContainer, scale: 0.1) animationTransition.updateAlpha(node: strongSelf.imageNodeContainer, alpha: 0.0, beginWithCurrentState: true) } if let updateImageSignal = updateImageSignal { strongSelf.imageNode.setSignal(updateImageSignal) } if let updatedFetchMediaSignal = updatedFetchMediaSignal { strongSelf.fetchDisposable.set(updatedFetchMediaSignal.startStrict()) } } } } } @objc func tapped() { if let interfaceInteraction = self.interfaceInteraction, let message = self.currentMessage { if self.isReplyThread { interfaceInteraction.scrollToTop() } else { interfaceInteraction.navigateToMessage(message.message.id, false, true, .pinnedMessage) } } } @objc func closePressed() { if let interfaceInteraction = self.interfaceInteraction, let message = self.currentMessage { interfaceInteraction.unpinMessage(message.message.id, true, nil) } } @objc func listPressed() { if let interfaceInteraction = self.interfaceInteraction, let message = self.currentMessage { interfaceInteraction.openPinnedList(message.message.id) } } @objc private func actionButtonPressed() { if let interfaceInteraction = self.interfaceInteraction, let controller = interfaceInteraction.chatController() as? ChatControllerImpl, let controllerInteraction = controller.controllerInteraction, let message = self.currentMessage?.message { for attribute in message.attributes { if let attribute = attribute as? ReplyMarkupMessageAttribute, attribute.flags.contains(.inline), attribute.rows.count == 1, attribute.rows[0].buttons.count == 1 { let button = attribute.rows[0].buttons[0] switch button.action { case .text: controllerInteraction.sendMessage(button.title) case let .url(url): var isConcealed = true if url.hasPrefix("tg://") { isConcealed = false } controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: url, concealed: isConcealed, progress: Promise())) case .requestMap: controllerInteraction.shareCurrentLocation() case .requestPhone: controllerInteraction.shareAccountContact() case .openWebApp: controllerInteraction.requestMessageActionCallback(message.id, nil, true, false) case let .callback(requiresPassword, data): controllerInteraction.requestMessageActionCallback(message.id, data, false, requiresPassword) case let .switchInline(samePeer, query, peerTypes): var botPeer: Peer? var found = false for attribute in message.attributes { if let attribute = attribute as? InlineBotMessageAttribute { if let peerId = attribute.peerId { botPeer = message.peers[peerId] found = true } } } if !found { botPeer = message.author } var peerId: PeerId? if samePeer { peerId = message.id.peerId } if let botPeer = botPeer, let addressName = botPeer.addressName { controllerInteraction.activateSwitchInline(peerId, "@\(addressName) \(query)", peerTypes) } case .payment: controllerInteraction.openCheckoutOrReceipt(message.id, nil) case let .urlAuth(url, buttonId): controllerInteraction.requestMessageActionUrlAuth(url, .message(id: message.id, buttonId: buttonId)) case .setupPoll: break case let .openUserProfile(peerId): let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) |> deliverOnMainQueue).startStandalone(next: { peer in if let peer = peer { controllerInteraction.openPeer(peer, .info(nil), nil, .default) } }) case let .openWebView(url, simple): controllerInteraction.openWebView(button.title, url, simple, .generic) case .requestPeer: break case let .copyText(payload): controllerInteraction.copyText(payload) } break } } } } }