Swiftgram/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift
2023-11-25 17:42:18 +04:00

938 lines
53 KiB
Swift

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: 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
}
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 {
actionTitle = attribute.rows[0].buttons[0].title
}
}
} 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
self.actionButtonTitleNode.attributedText = NSAttributedString(string: actionTitle, font: Font.with(size: 15.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: interfaceState.theme.list.itemCheckColors.foregroundColor)
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: String?
if let translationState = interfaceState.translationState, translationState.isEnabled {
translateToLanguage = translationState.toLang
if translateToLanguage == "nb" {
translateToLanguage = "no"
} else if translateToLanguage == "pt-br" {
translateToLanguage = "pt"
}
}
var currentTranslateToLanguageUpdated = false
if self.currentTranslateToLanguage != translateToLanguage {
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 {
} else if let translateToLanguage {
self.translationDisposable.set(translateMessageIds(context: self.context, messageIds: [message.id], toLang: translateToLanguage).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)
}
}
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
}
}
}
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<FetchResourceSourceType, FetchResourceError>?
if mediaUpdated {
if let updatedMediaReference = updatedMediaReference, imageDimensions != nil {
if let imageReference = updatedMediaReference.concrete(TelegramMediaImage.self) {
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))
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)
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
}
break
}
}
}
}
}