import Foundation import UIKit import Display import AsyncDisplayKit import TelegramPresentationData import ChatPresentationInterfaceState import ShimmerEffect private let buttonFont = Font.semibold(14.0) private let sharedBackgroundImage = generateStretchableFilledCircleImage(radius: 4.0, color: UIColor.white)?.withRenderingMode(.alwaysTemplate) public final class ChatMessageAttachedContentButtonNode: HighlightTrackingButtonNode { private let textNode: TextNode private var iconView: UIImageView? private let shimmerEffectNode: ShimmerEffectForegroundNode private var backgroundView: UIImageView? private var regularIconImage: UIImage? public var pressed: (() -> Void)? private var titleColor: UIColor? public init() { self.textNode = TextNode() self.textNode.isUserInteractionEnabled = false self.shimmerEffectNode = ShimmerEffectForegroundNode() self.shimmerEffectNode.cornerRadius = 5.0 super.init() self.addSubnode(self.shimmerEffectNode) self.addSubnode(self.textNode) self.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { if highlighted { let scale = (strongSelf.bounds.width - 10.0) / strongSelf.bounds.width strongSelf.layer.animateScale(from: 1.0, to: scale, duration: 0.15, removeOnCompletion: false) } else { if let presentationLayer = strongSelf.layer.presentation() { strongSelf.layer.animateScale(from: CGFloat((presentationLayer.value(forKeyPath: "transform.scale.y") as? NSNumber)?.floatValue ?? 1.0), to: 1.0, duration: 0.25, removeOnCompletion: false) } } } } self.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) } @objc private func buttonPressed() { self.pressed?() } public func startShimmering() { guard let titleColor = self.titleColor else { return } self.shimmerEffectNode.isHidden = false self.shimmerEffectNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) let backgroundFrame = self.bounds self.shimmerEffectNode.frame = backgroundFrame self.shimmerEffectNode.updateAbsoluteRect(CGRect(origin: .zero, size: backgroundFrame.size), within: backgroundFrame.size) self.shimmerEffectNode.update(backgroundColor: .clear, foregroundColor: titleColor.withAlphaComponent(0.3), horizontal: true, effectSize: nil, globalTimeOffset: false, duration: nil) } public func stopShimmering() { self.shimmerEffectNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in self?.shimmerEffectNode.isHidden = true }) } public typealias AsyncLayout = (_ width: CGFloat, _ iconImage: UIImage?, _ cornerIcon: Bool, _ title: String, _ titleColor: UIColor, _ inProgress: Bool, _ drawBackground: Bool) -> (CGFloat, (CGFloat, CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageAttachedContentButtonNode)) public static func asyncLayout(_ current: ChatMessageAttachedContentButtonNode?) -> AsyncLayout { let previousRegularIconImage = current?.regularIconImage let maybeMakeTextLayout = (current?.textNode).flatMap(TextNode.asyncLayout) return { width, iconImage, cornerIcon, title, titleColor, inProgress, drawBackground in let targetNode: ChatMessageAttachedContentButtonNode if let current = current { targetNode = current } else { targetNode = ChatMessageAttachedContentButtonNode() } let makeTextLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode) if let maybeMakeTextLayout = maybeMakeTextLayout { makeTextLayout = maybeMakeTextLayout } else { makeTextLayout = TextNode.asyncLayout(targetNode.textNode) } var updatedRegularIconImage: UIImage? if iconImage !== previousRegularIconImage { updatedRegularIconImage = iconImage } var iconWidth: CGFloat = 0.0 if let iconImage = iconImage { iconWidth = iconImage.size.width + 5.0 } let labelInset: CGFloat = 8.0 let (textSize, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: buttonFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(1.0, width - labelInset * 2.0 - iconWidth), height: CGFloat.greatestFiniteMagnitude), alignment: .left, cutout: nil, insets: UIEdgeInsets())) return (textSize.size.width + labelInset * 2.0, { refinedWidth, refinedHeight in let size = CGSize(width: refinedWidth, height: refinedHeight) return (size, { animation in targetNode.accessibilityLabel = title targetNode.titleColor = titleColor let iconView: UIImageView if let current = targetNode.iconView { iconView = current } else { iconView = UIImageView() targetNode.iconView = iconView targetNode.view.addSubview(iconView) } iconView.tintColor = titleColor if let updatedRegularIconImage = updatedRegularIconImage { targetNode.regularIconImage = updatedRegularIconImage if !targetNode.textNode.isHidden { iconView.image = updatedRegularIconImage.withRenderingMode(.alwaysTemplate) } } let _ = textApply() let backgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: refinedWidth, height: size.height)) var textFrame = CGRect(origin: CGPoint(x: floor((refinedWidth - textSize.size.width) / 2.0), y: floor((backgroundFrame.height - textSize.size.height) / 2.0)), size: textSize.size) if drawBackground { textFrame.origin.y += 1.0 } if let image = iconView.image { let iconFrame: CGRect if cornerIcon { iconFrame = CGRect(origin: CGPoint(x: backgroundFrame.maxX - image.size.width - 5.0, y: 5.0), size: image.size) } else { textFrame.origin.x += floor(image.size.width / 2.0) iconFrame = CGRect(origin: CGPoint(x: textFrame.minX - image.size.width - 5.0, y: textFrame.minY + floorToScreenPixels((textFrame.height - image.size.height) * 0.5)), size: image.size) } animation.animator.updateFrame(layer: iconView.layer, frame: iconFrame, completion: nil) } targetNode.textNode.bounds = CGRect(origin: CGPoint(), size: textFrame.size) animation.animator.updatePosition(layer: targetNode.textNode.layer, position: textFrame.center, completion: nil) if drawBackground { let backgroundView: UIImageView if let current = targetNode.backgroundView { backgroundView = current animation.animator.updateFrame(layer: backgroundView.layer, frame: backgroundFrame, completion: nil) } else { backgroundView = UIImageView() backgroundView.image = sharedBackgroundImage targetNode.backgroundView = backgroundView targetNode.view.insertSubview(backgroundView, at: 0) backgroundView.frame = backgroundFrame } backgroundView.tintColor = titleColor.withMultipliedAlpha(0.1) } else if let backgroundView = targetNode.backgroundView { targetNode.backgroundView = nil backgroundView.removeFromSuperview() } return targetNode }) }) } } }