mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
576 lines
31 KiB
Swift
576 lines
31 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import AsyncDisplayKit
|
|
import TelegramCore
|
|
import Postbox
|
|
import Display
|
|
import TelegramPresentationData
|
|
import AccountContext
|
|
import WallpaperBackgroundNode
|
|
import UrlHandling
|
|
|
|
private let titleFont = Font.medium(16.0)
|
|
|
|
private extension UIBezierPath {
|
|
convenience init(roundRect rect: CGRect, topLeftRadius: CGFloat = 0.0, topRightRadius: CGFloat = 0.0, bottomLeftRadius: CGFloat = 0.0, bottomRightRadius: CGFloat = 0.0) {
|
|
self.init()
|
|
|
|
let path = CGMutablePath()
|
|
|
|
let topLeft = rect.origin
|
|
let topRight = CGPoint(x: rect.maxX, y: rect.minY)
|
|
let bottomRight = CGPoint(x: rect.maxX, y: rect.maxY)
|
|
let bottomLeft = CGPoint(x: rect.minX, y: rect.maxY)
|
|
|
|
if topLeftRadius != .zero {
|
|
path.move(to: CGPoint(x: topLeft.x+topLeftRadius, y: topLeft.y))
|
|
} else {
|
|
path.move(to: CGPoint(x: topLeft.x, y: topLeft.y))
|
|
}
|
|
|
|
if topRightRadius != .zero {
|
|
path.addLine(to: CGPoint(x: topRight.x-topRightRadius, y: topRight.y))
|
|
path.addCurve(to: CGPoint(x: topRight.x, y: topRight.y+topRightRadius), control1: CGPoint(x: topRight.x, y: topRight.y), control2:CGPoint(x: topRight.x, y: topRight.y + topRightRadius))
|
|
} else {
|
|
path.addLine(to: CGPoint(x: topRight.x, y: topRight.y))
|
|
}
|
|
|
|
if bottomRightRadius != .zero {
|
|
path.addLine(to: CGPoint(x: bottomRight.x, y: bottomRight.y-bottomRightRadius))
|
|
path.addCurve(to: CGPoint(x: bottomRight.x-bottomRightRadius, y: bottomRight.y), control1: CGPoint(x: bottomRight.x, y: bottomRight.y), control2: CGPoint(x: bottomRight.x-bottomRightRadius, y: bottomRight.y))
|
|
} else {
|
|
path.addLine(to: CGPoint(x: bottomRight.x, y: bottomRight.y))
|
|
}
|
|
|
|
if bottomLeftRadius != .zero {
|
|
path.addLine(to: CGPoint(x: bottomLeft.x+bottomLeftRadius, y: bottomLeft.y))
|
|
path.addCurve(to: CGPoint(x: bottomLeft.x, y: bottomLeft.y-bottomLeftRadius), control1: CGPoint(x: bottomLeft.x, y: bottomLeft.y), control2: CGPoint(x: bottomLeft.x, y: bottomLeft.y-bottomLeftRadius))
|
|
} else {
|
|
path.addLine(to: CGPoint(x: bottomLeft.x, y: bottomLeft.y))
|
|
}
|
|
|
|
if topLeftRadius != .zero {
|
|
path.addLine(to: CGPoint(x: topLeft.x, y: topLeft.y+topLeftRadius))
|
|
path.addCurve(to: CGPoint(x: topLeft.x+topLeftRadius, y: topLeft.y) , control1: CGPoint(x: topLeft.x, y: topLeft.y) , control2: CGPoint(x: topLeft.x+topLeftRadius, y: topLeft.y))
|
|
} else {
|
|
path.addLine(to: CGPoint(x: topLeft.x, y: topLeft.y))
|
|
}
|
|
|
|
path.closeSubpath()
|
|
self.cgPath = path
|
|
}
|
|
}
|
|
|
|
private final class ChatMessageActionButtonNode: ASDisplayNode {
|
|
//private let backgroundBlurNode: NavigationBackgroundNode
|
|
private var backgroundBlurView: PortalView?
|
|
|
|
private var titleNode: TextNode?
|
|
private var iconNode: ASImageNode?
|
|
private var buttonView: HighlightTrackingButton?
|
|
|
|
private var wallpaperBackgroundNode: WallpaperBackgroundNode?
|
|
private var backgroundContent: WallpaperBubbleBackgroundNode?
|
|
private var backgroundColorNode: ASDisplayNode?
|
|
|
|
private var absolutePosition: (CGRect, CGSize)?
|
|
|
|
private var button: ReplyMarkupButton?
|
|
var pressed: ((ReplyMarkupButton) -> Void)?
|
|
var longTapped: ((ReplyMarkupButton) -> Void)?
|
|
|
|
var longTapRecognizer: UILongPressGestureRecognizer?
|
|
|
|
private let accessibilityArea: AccessibilityAreaNode
|
|
|
|
override init() {
|
|
//self.backgroundBlurNode = NavigationBackgroundNode(color: .clear)
|
|
//self.backgroundBlurNode.isUserInteractionEnabled = false
|
|
|
|
self.accessibilityArea = AccessibilityAreaNode()
|
|
self.accessibilityArea.accessibilityTraits = .button
|
|
|
|
super.init()
|
|
|
|
//self.addSubnode(self.backgroundBlurNode)
|
|
self.addSubnode(self.accessibilityArea)
|
|
|
|
self.accessibilityArea.activate = { [weak self] in
|
|
self?.buttonPressed()
|
|
return true
|
|
}
|
|
}
|
|
|
|
override func didLoad() {
|
|
super.didLoad()
|
|
|
|
let buttonView = HighlightTrackingButton(frame: self.bounds)
|
|
buttonView.addTarget(self, action: #selector(self.buttonPressed), for: [.touchUpInside])
|
|
self.buttonView = buttonView
|
|
buttonView.isAccessibilityElement = false
|
|
self.view.addSubview(buttonView)
|
|
buttonView.highligthedChanged = { [weak self] highlighted in
|
|
if let strongSelf = self {
|
|
if highlighted {
|
|
//strongSelf.backgroundBlurNode.layer.removeAnimation(forKey: "opacity")
|
|
//strongSelf.backgroundBlurNode.alpha = 0.55
|
|
|
|
if let backgroundBlurView = strongSelf.backgroundBlurView {
|
|
backgroundBlurView.view.layer.removeAnimation(forKey: "opacity")
|
|
backgroundBlurView.view.alpha = 0.55
|
|
}
|
|
|
|
strongSelf.backgroundContent?.layer.removeAnimation(forKey: "opacity")
|
|
strongSelf.backgroundContent?.alpha = 0.55
|
|
} else {
|
|
//strongSelf.backgroundBlurNode.alpha = 1.0
|
|
//strongSelf.backgroundBlurNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2)
|
|
|
|
if let backgroundBlurView = strongSelf.backgroundBlurView {
|
|
backgroundBlurView.view.alpha = 1.0
|
|
backgroundBlurView.view.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2)
|
|
}
|
|
|
|
strongSelf.backgroundContent?.alpha = 1.0
|
|
strongSelf.backgroundContent?.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2)
|
|
}
|
|
}
|
|
}
|
|
|
|
let longTapRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.longTapGesture(_:)))
|
|
longTapRecognizer.minimumPressDuration = 0.3
|
|
buttonView.addGestureRecognizer(longTapRecognizer)
|
|
self.longTapRecognizer = longTapRecognizer
|
|
}
|
|
|
|
@objc func buttonPressed() {
|
|
if let button = self.button, let pressed = self.pressed {
|
|
pressed(button)
|
|
}
|
|
}
|
|
|
|
@objc func longTapGesture(_ recognizer: UILongPressGestureRecognizer) {
|
|
if let button = self.button, let longTapped = self.longTapped, recognizer.state == .began {
|
|
longTapped(button)
|
|
}
|
|
}
|
|
|
|
func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
|
|
self.absolutePosition = (rect, containerSize)
|
|
|
|
if let backgroundContent = self.backgroundContent {
|
|
var backgroundFrame = backgroundContent.frame
|
|
backgroundFrame.origin.x += rect.minX
|
|
backgroundFrame.origin.y += rect.minY
|
|
backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: .immediate)
|
|
}
|
|
}
|
|
|
|
class func asyncLayout(_ maybeNode: ChatMessageActionButtonNode?) -> (_ context: AccountContext, _ theme: ChatPresentationThemeData, _ bubbleCorners: PresentationChatBubbleCorners, _ strings: PresentationStrings, _ backgroundNode: WallpaperBackgroundNode?, _ message: Message, _ button: ReplyMarkupButton, _ constrainedWidth: CGFloat, _ position: MessageBubbleActionButtonPosition) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonNode))) {
|
|
let titleLayout = TextNode.asyncLayout(maybeNode?.titleNode)
|
|
|
|
return { context, theme, bubbleCorners, strings, backgroundNode, message, button, constrainedWidth, position in
|
|
let incoming = message.effectivelyIncoming(context.account.peerId)
|
|
let graphics = PresentationResourcesChat.additionalGraphics(theme.theme, wallpaper: theme.wallpaper, bubbleCorners: bubbleCorners)
|
|
|
|
var isStarsPayment = false
|
|
let iconImage: UIImage?
|
|
switch button.action {
|
|
case .text:
|
|
iconImage = incoming ? graphics.chatBubbleActionButtonIncomingMessageIconImage : graphics.chatBubbleActionButtonOutgoingMessageIconImage
|
|
case let .url(value):
|
|
var isApp = false
|
|
if isTelegramMeLink(value), let internalUrl = parseFullInternalUrl(sharedContext: context.sharedContext, url: value) {
|
|
if case .peer(_, .appStart) = internalUrl {
|
|
isApp = true
|
|
} else if case .peer(_, .attachBotStart) = internalUrl {
|
|
isApp = true
|
|
} else if case .startAttach = internalUrl {
|
|
isApp = true
|
|
}
|
|
}
|
|
if isApp {
|
|
iconImage = incoming ? graphics.chatBubbleActionButtonIncomingWebAppIconImage : graphics.chatBubbleActionButtonOutgoingWebAppIconImage
|
|
} else if value.lowercased().contains("?startgroup=") {
|
|
iconImage = incoming ? graphics.chatBubbleActionButtonIncomingAddToChatIconImage : graphics.chatBubbleActionButtonOutgoingAddToChatIconImage
|
|
} else {
|
|
iconImage = incoming ? graphics.chatBubbleActionButtonIncomingLinkIconImage : graphics.chatBubbleActionButtonOutgoingLinkIconImage
|
|
}
|
|
case .urlAuth:
|
|
iconImage = incoming ? graphics.chatBubbleActionButtonIncomingLinkIconImage : graphics.chatBubbleActionButtonOutgoingLinkIconImage
|
|
case .requestPhone:
|
|
iconImage = incoming ? graphics.chatBubbleActionButtonIncomingPhoneIconImage : graphics.chatBubbleActionButtonOutgoingPhoneIconImage
|
|
case .requestMap:
|
|
iconImage = incoming ? graphics.chatBubbleActionButtonIncomingLocationIconImage : graphics.chatBubbleActionButtonOutgoingLocationIconImage
|
|
case .switchInline:
|
|
iconImage = incoming ? graphics.chatBubbleActionButtonIncomingShareIconImage : graphics.chatBubbleActionButtonOutgoingShareIconImage
|
|
case .payment:
|
|
if button.title.contains("⭐️") {
|
|
isStarsPayment = true
|
|
iconImage = nil
|
|
} else {
|
|
iconImage = incoming ? graphics.chatBubbleActionButtonIncomingPaymentIconImage : graphics.chatBubbleActionButtonOutgoingPaymentIconImage
|
|
}
|
|
case .openUserProfile:
|
|
iconImage = incoming ? graphics.chatBubbleActionButtonIncomingProfileIconImage : graphics.chatBubbleActionButtonOutgoingProfileIconImage
|
|
case .openWebView:
|
|
iconImage = incoming ? graphics.chatBubbleActionButtonIncomingWebAppIconImage : graphics.chatBubbleActionButtonOutgoingWebAppIconImage
|
|
case .copyText:
|
|
iconImage = incoming ? graphics.chatBubbleActionButtonIncomingCopyIconImage : graphics.chatBubbleActionButtonOutgoingCopyIconImage
|
|
default:
|
|
iconImage = nil
|
|
}
|
|
|
|
let sideInset: CGFloat = 8.0
|
|
let minimumSideInset: CGFloat = 4.0 + (iconImage?.size.width ?? 0.0)
|
|
|
|
var title = button.title
|
|
if case .payment = button.action {
|
|
for media in message.media {
|
|
if let invoice = media as? TelegramMediaInvoice {
|
|
if invoice.receiptMessageId != nil {
|
|
title = strings.Message_ReplyActionButtonShowReceipt
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let messageTheme = incoming ? theme.theme.chat.message.incoming : theme.theme.chat.message.outgoing
|
|
|
|
let titleColor = bubbleVariableColor(variableColor: messageTheme.actionButtonsTextColor, wallpaper: theme.wallpaper)
|
|
let attributedTitle: NSAttributedString
|
|
if isStarsPayment {
|
|
let updatedTitle = title.replacingOccurrences(of: "⭐️", with: " # ")
|
|
let buttonAttributedString = NSMutableAttributedString(string: updatedTitle, font: titleFont, textColor: titleColor, paragraphAlignment: .center)
|
|
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: titleColor, range: NSRange(range, in: buttonAttributedString.string))
|
|
buttonAttributedString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: buttonAttributedString.string))
|
|
}
|
|
attributedTitle = buttonAttributedString
|
|
} else {
|
|
attributedTitle = NSAttributedString(string: title, font: titleFont, textColor: titleColor)
|
|
}
|
|
|
|
let (titleSize, titleApply) = titleLayout(TextNodeLayoutArguments(attributedString: attributedTitle, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(44.0, constrainedWidth - minimumSideInset - minimumSideInset), height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets(top: 1.0, left: 0.0, bottom: 1.0, right: 0.0)))
|
|
|
|
return (titleSize.size.width + sideInset + sideInset, { width in
|
|
return (CGSize(width: width, height: 42.0), { animation in
|
|
var animation = animation
|
|
|
|
let node: ChatMessageActionButtonNode
|
|
if let maybeNode = maybeNode {
|
|
node = maybeNode
|
|
} else {
|
|
node = ChatMessageActionButtonNode()
|
|
animation = .None
|
|
}
|
|
|
|
node.wallpaperBackgroundNode = backgroundNode
|
|
|
|
node.button = button
|
|
|
|
switch button.action {
|
|
case .url:
|
|
node.longTapRecognizer?.isEnabled = true
|
|
default:
|
|
node.longTapRecognizer?.isEnabled = false
|
|
}
|
|
|
|
//animation.animator.updateFrame(layer: node.backgroundBlurNode.layer, frame: CGRect(origin: CGPoint(), size: CGSize(width: max(0.0, width), height: 42.0)), completion: nil)
|
|
|
|
if node.backgroundBlurView == nil {
|
|
if let backgroundBlurView = backgroundNode?.makeFreeBackground() {
|
|
node.backgroundBlurView = backgroundBlurView
|
|
node.view.insertSubview(backgroundBlurView.view, at: 0)
|
|
}
|
|
}
|
|
if let backgroundBlurView = node.backgroundBlurView {
|
|
animation.animator.updateFrame(layer: backgroundBlurView.view.layer, frame: CGRect(origin: CGPoint(), size: CGSize(width: max(0.0, width), height: 42.0)), completion: nil)
|
|
}
|
|
|
|
/*node.backgroundBlurNode.update(size: node.backgroundBlurNode.bounds.size, cornerRadius: 0.0, animator: animation.animator)
|
|
node.backgroundBlurNode.updateColor(color: selectDateFillStaticColor(theme: theme.theme, wallpaper: theme.wallpaper), enableBlur: context.sharedContext.energyUsageSettings.fullTranslucency && dateFillNeedsBlur(theme: theme.theme, wallpaper: theme.wallpaper), transition: .immediate)*/
|
|
|
|
if backgroundNode?.hasExtraBubbleBackground() == true {
|
|
if node.backgroundContent == nil, let backgroundContent = backgroundNode?.makeBubbleBackground(for: .free) {
|
|
backgroundContent.clipsToBounds = true
|
|
backgroundContent.allowsGroupOpacity = true
|
|
|
|
node.backgroundContent = backgroundContent
|
|
node.insertSubnode(backgroundContent, at: 0)
|
|
|
|
let backgroundColorNode = ASDisplayNode()
|
|
backgroundColorNode.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.08)
|
|
backgroundContent.addSubnode(backgroundColorNode)
|
|
node.backgroundColorNode = backgroundColorNode
|
|
}
|
|
} else {
|
|
node.backgroundContent?.removeFromSupernode()
|
|
node.backgroundContent = nil
|
|
|
|
node.backgroundColorNode?.removeFromSupernode()
|
|
node.backgroundColorNode = nil
|
|
}
|
|
|
|
node.cornerRadius = bubbleCorners.auxiliaryRadius
|
|
node.clipsToBounds = true
|
|
|
|
if let backgroundContent = node.backgroundContent {
|
|
//node.backgroundBlurNode.isHidden = true
|
|
node.backgroundBlurView?.view.isHidden = true
|
|
backgroundContent.frame = CGRect(origin: CGPoint(), size: CGSize(width: max(0.0, width), height: 42.0))
|
|
|
|
node.backgroundColorNode?.frame = backgroundContent.bounds
|
|
|
|
if let (rect, containerSize) = node.absolutePosition {
|
|
var backgroundFrame = backgroundContent.frame
|
|
backgroundFrame.origin.x += rect.minX
|
|
backgroundFrame.origin.y += rect.minY
|
|
backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: .immediate)
|
|
}
|
|
} else {
|
|
node.backgroundBlurView?.view.isHidden = false
|
|
}
|
|
|
|
|
|
let rect = CGRect(origin: CGPoint(), size: CGSize(width: max(0.0, width), height: 42.0))
|
|
let maskPath: CGPath?
|
|
switch position {
|
|
case .bottomSingle:
|
|
maskPath = UIBezierPath(roundRect: rect, topLeftRadius: bubbleCorners.auxiliaryRadius, topRightRadius: bubbleCorners.auxiliaryRadius, bottomLeftRadius: bubbleCorners.mainRadius, bottomRightRadius: bubbleCorners.mainRadius).cgPath
|
|
case .bottomLeft:
|
|
maskPath = UIBezierPath(roundRect: rect, topLeftRadius: bubbleCorners.auxiliaryRadius, topRightRadius: bubbleCorners.auxiliaryRadius, bottomLeftRadius: bubbleCorners.mainRadius, bottomRightRadius: bubbleCorners.auxiliaryRadius).cgPath
|
|
case .bottomRight:
|
|
maskPath = UIBezierPath(roundRect: rect, topLeftRadius: bubbleCorners.auxiliaryRadius, topRightRadius: bubbleCorners.auxiliaryRadius, bottomLeftRadius: bubbleCorners.auxiliaryRadius, bottomRightRadius: bubbleCorners.mainRadius).cgPath
|
|
default:
|
|
maskPath = nil
|
|
}
|
|
|
|
let currentMaskPath = (node.layer.mask as? CAShapeLayer)?.path
|
|
if currentMaskPath != maskPath {
|
|
if let maskPath = maskPath {
|
|
let shapeLayer = CAShapeLayer()
|
|
shapeLayer.path = maskPath
|
|
node.layer.mask = shapeLayer
|
|
} else {
|
|
node.layer.mask = nil
|
|
}
|
|
}
|
|
|
|
if iconImage != nil {
|
|
if node.iconNode == nil {
|
|
let iconNode = ASImageNode()
|
|
iconNode.contentMode = .center
|
|
node.iconNode = iconNode
|
|
node.addSubnode(iconNode)
|
|
}
|
|
node.iconNode?.image = iconImage
|
|
} else if node.iconNode != nil {
|
|
node.iconNode?.removeFromSupernode()
|
|
node.iconNode = nil
|
|
}
|
|
|
|
let titleNode = titleApply()
|
|
if node.titleNode !== titleNode {
|
|
node.titleNode = titleNode
|
|
node.addSubnode(titleNode)
|
|
titleNode.isUserInteractionEnabled = false
|
|
}
|
|
|
|
let titleFrame = CGRect(origin: CGPoint(x: floor((width - titleSize.size.width) / 2.0), y: floor((42.0 - titleSize.size.height) / 2.0) + 1.0), size: titleSize.size)
|
|
titleNode.layer.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
|
|
animation.animator.updatePosition(layer: titleNode.layer, position: CGPoint(x: titleFrame.midX, y: titleFrame.midY), completion: nil)
|
|
|
|
if let buttonView = node.buttonView {
|
|
buttonView.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: 42.0))
|
|
}
|
|
if let iconNode = node.iconNode {
|
|
animation.animator.updateFrame(layer: iconNode.layer, frame: CGRect(x: width - 16.0, y: 4.0, width: 12.0, height: 12.0), completion: nil)
|
|
}
|
|
|
|
if let (rect, size) = node.absolutePosition {
|
|
node.updateAbsoluteRect(rect, within: size)
|
|
}
|
|
|
|
node.accessibilityArea.accessibilityLabel = title
|
|
node.accessibilityArea.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: 42.0))
|
|
|
|
return node
|
|
})
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
public final class ChatMessageActionButtonsNode: ASDisplayNode {
|
|
private var buttonNodes: [ChatMessageActionButtonNode] = []
|
|
|
|
private var buttonPressedWrapper: ((ReplyMarkupButton) -> Void)?
|
|
private var buttonLongTappedWrapper: ((ReplyMarkupButton) -> Void)?
|
|
public var buttonPressed: ((ReplyMarkupButton) -> Void)?
|
|
public var buttonLongTapped: ((ReplyMarkupButton) -> Void)?
|
|
|
|
private var absolutePosition: (CGRect, CGSize)?
|
|
|
|
override public init() {
|
|
super.init()
|
|
|
|
self.buttonPressedWrapper = { [weak self] button in
|
|
if let buttonPressed = self?.buttonPressed {
|
|
buttonPressed(button)
|
|
}
|
|
}
|
|
|
|
self.buttonLongTappedWrapper = { [weak self] button in
|
|
if let buttonLongTapped = self?.buttonLongTapped {
|
|
buttonLongTapped(button)
|
|
}
|
|
}
|
|
}
|
|
|
|
public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
|
|
self.absolutePosition = (rect, containerSize)
|
|
|
|
for button in self.buttonNodes {
|
|
var buttonFrame = button.frame
|
|
buttonFrame.origin.x += rect.minX
|
|
buttonFrame.origin.y += rect.minY
|
|
button.updateAbsoluteRect(buttonFrame, within: containerSize)
|
|
}
|
|
}
|
|
|
|
public class func asyncLayout(_ maybeNode: ChatMessageActionButtonsNode?) -> (_ context: AccountContext, _ theme: ChatPresentationThemeData, _ chatBubbleCorners: PresentationChatBubbleCorners, _ strings: PresentationStrings, _ backgroundNode: WallpaperBackgroundNode?, _ replyMarkup: ReplyMarkupMessageAttribute, _ message: Message, _ constrainedWidth: CGFloat) -> (minWidth: CGFloat, layout: (CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode)) {
|
|
let currentButtonLayouts = maybeNode?.buttonNodes.map { ChatMessageActionButtonNode.asyncLayout($0) } ?? []
|
|
|
|
return { context, theme, chatBubbleCorners, strings, backgroundNode, replyMarkup, message, constrainedWidth in
|
|
let buttonHeight: CGFloat = 42.0
|
|
let buttonSpacing: CGFloat = 2.0
|
|
|
|
var overallMinimumRowWidth: CGFloat = 0.0
|
|
|
|
var finalizeRowLayouts: [[((CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonNode))]] = []
|
|
|
|
var rowIndex = 0
|
|
var buttonIndex = 0
|
|
for row in replyMarkup.rows {
|
|
var maximumRowButtonWidth: CGFloat = 0.0
|
|
let maximumButtonWidth: CGFloat = max(1.0, floor((constrainedWidth - CGFloat(max(0, row.buttons.count - 1)) * buttonSpacing) / CGFloat(row.buttons.count)))
|
|
var finalizeRowButtonLayouts: [((CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonNode))] = []
|
|
var rowButtonIndex = 0
|
|
for button in row.buttons {
|
|
let buttonPosition: MessageBubbleActionButtonPosition
|
|
if rowIndex == replyMarkup.rows.count - 1 {
|
|
if row.buttons.count == 1 {
|
|
buttonPosition = .bottomSingle
|
|
} else if rowButtonIndex == 0 {
|
|
buttonPosition = .bottomLeft
|
|
} else if rowButtonIndex == row.buttons.count - 1 {
|
|
buttonPosition = .bottomRight
|
|
} else {
|
|
buttonPosition = .middle
|
|
}
|
|
} else {
|
|
buttonPosition = .middle
|
|
}
|
|
|
|
let prepareButtonLayout: (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonNode)))
|
|
if buttonIndex < currentButtonLayouts.count {
|
|
prepareButtonLayout = currentButtonLayouts[buttonIndex](context, theme, chatBubbleCorners, strings, backgroundNode, message, button, maximumButtonWidth, buttonPosition)
|
|
} else {
|
|
prepareButtonLayout = ChatMessageActionButtonNode.asyncLayout(nil)(context, theme, chatBubbleCorners, strings, backgroundNode, message, button, maximumButtonWidth, buttonPosition)
|
|
}
|
|
|
|
maximumRowButtonWidth = max(maximumRowButtonWidth, prepareButtonLayout.minimumWidth)
|
|
finalizeRowButtonLayouts.append(prepareButtonLayout.layout)
|
|
|
|
buttonIndex += 1
|
|
rowButtonIndex += 1
|
|
}
|
|
|
|
overallMinimumRowWidth = max(overallMinimumRowWidth, maximumRowButtonWidth * CGFloat(row.buttons.count) + buttonSpacing * max(0.0, CGFloat(row.buttons.count - 1)))
|
|
finalizeRowLayouts.append(finalizeRowButtonLayouts)
|
|
|
|
rowIndex += 1
|
|
}
|
|
|
|
return (min(constrainedWidth, overallMinimumRowWidth), { constrainedWidth in
|
|
var buttonFramesAndApply: [(CGRect, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonNode)] = []
|
|
|
|
var verticalRowOffset: CGFloat = 0.0
|
|
verticalRowOffset += buttonSpacing * 0.5
|
|
|
|
var rowIndex = 0
|
|
for finalizeRowButtonLayouts in finalizeRowLayouts {
|
|
let actualButtonWidth: CGFloat = max(1.0, floor((constrainedWidth - CGFloat(max(0, finalizeRowButtonLayouts.count - 1)) * buttonSpacing) / CGFloat(finalizeRowButtonLayouts.count)))
|
|
var horizontalButtonOffset: CGFloat = 0.0
|
|
for finalizeButtonLayout in finalizeRowButtonLayouts {
|
|
let (buttonSize, buttonApply) = finalizeButtonLayout(actualButtonWidth)
|
|
let buttonFrame = CGRect(origin: CGPoint(x: horizontalButtonOffset, y: verticalRowOffset), size: buttonSize)
|
|
buttonFramesAndApply.append((buttonFrame, buttonApply))
|
|
horizontalButtonOffset += buttonSize.width + buttonSpacing
|
|
}
|
|
|
|
verticalRowOffset += buttonHeight + buttonSpacing
|
|
rowIndex += 1
|
|
}
|
|
if verticalRowOffset > 0.0 {
|
|
verticalRowOffset = max(0.0, verticalRowOffset - buttonSpacing)
|
|
}
|
|
|
|
return (CGSize(width: constrainedWidth, height: verticalRowOffset), { animation in
|
|
let node: ChatMessageActionButtonsNode
|
|
if let maybeNode = maybeNode {
|
|
node = maybeNode
|
|
} else {
|
|
node = ChatMessageActionButtonsNode()
|
|
}
|
|
|
|
var updatedButtons: [ChatMessageActionButtonNode] = []
|
|
var index = 0
|
|
for (buttonFrame, buttonApply) in buttonFramesAndApply {
|
|
let buttonNode = buttonApply(animation)
|
|
updatedButtons.append(buttonNode)
|
|
if buttonNode.supernode == nil {
|
|
buttonNode.pressed = node.buttonPressedWrapper
|
|
buttonNode.longTapped = node.buttonLongTappedWrapper
|
|
buttonNode.frame = buttonFrame
|
|
|
|
node.addSubnode(buttonNode)
|
|
} else {
|
|
animation.animator.updateFrame(layer: buttonNode.layer, frame: buttonFrame, completion: nil)
|
|
}
|
|
index += 1
|
|
}
|
|
|
|
var buttonsUpdated = false
|
|
if node.buttonNodes.count != updatedButtons.count {
|
|
buttonsUpdated = true
|
|
} else {
|
|
for i in 0 ..< updatedButtons.count {
|
|
if updatedButtons[i] !== node.buttonNodes[i] {
|
|
buttonsUpdated = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if buttonsUpdated {
|
|
for currentButton in node.buttonNodes {
|
|
if !updatedButtons.contains(currentButton) {
|
|
currentButton.removeFromSupernode()
|
|
}
|
|
}
|
|
}
|
|
node.buttonNodes = updatedButtons
|
|
|
|
if let (rect, size) = node.absolutePosition {
|
|
node.updateAbsoluteRect(rect, within: size)
|
|
}
|
|
|
|
return node
|
|
})
|
|
})
|
|
}
|
|
}
|
|
}
|