mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
979 lines
56 KiB
Swift
979 lines
56 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: (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 || translation.toLang.hasPrefix("\(translateToLanguage?.toLang ?? "")-") /* MARK: Swiftgram */ {
|
|
} else if let translateToLanguage {
|
|
self.translationDisposable.set(translateMessageIds(context: self.context, messageIds: [message.id], fromLang: translateToLanguage.fromLang, toLang: translateToLanguage.toLang, viaText: !self.context.isPremium).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<FetchResourceSourceType, FetchResourceError>?
|
|
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), .zero, 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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|