Swiftgram/submodules/TelegramUI/Sources/ChatButtonKeyboardInputNode.swift
2023-02-28 23:25:35 +04:00

457 lines
21 KiB
Swift

import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
import AccountContext
import ChatPresentationInterfaceState
import WallpaperBackgroundNode
import ChatControllerInteraction
import ChatInputNode
private final class ChatButtonKeyboardInputButtonNode: HighlightTrackingButtonNode {
var button: ReplyMarkupButton? {
didSet {
self.updateIcon()
}
}
private let backgroundContainerNode: ASDisplayNode
private var backgroundNode: WallpaperBubbleBackgroundNode?
private let backgroundColorNode: ASDisplayNode
private let backgroundAdditionalColorNode: ASDisplayNode
private let shadowNode: ASImageNode
private let highlightNode: ASImageNode
private let textNode: ImmediateTextNode
private var iconNode: ASImageNode?
private var theme: PresentationTheme?
init() {
self.backgroundContainerNode = ASDisplayNode()
self.backgroundContainerNode.clipsToBounds = true
self.backgroundContainerNode.allowsGroupOpacity = true
self.backgroundContainerNode.isUserInteractionEnabled = false
self.backgroundContainerNode.cornerRadius = 5.0
if #available(iOS 13.0, *) {
self.backgroundContainerNode.layer.cornerCurve = .continuous
}
self.backgroundColorNode = ASDisplayNode()
self.backgroundColorNode.cornerRadius = 5.0
if #available(iOS 13.0, *) {
self.backgroundColorNode.layer.cornerCurve = .continuous
}
self.backgroundAdditionalColorNode = ASDisplayNode()
self.backgroundAdditionalColorNode.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.1)
self.backgroundAdditionalColorNode.isHidden = true
self.shadowNode = ASImageNode()
self.shadowNode.isUserInteractionEnabled = false
self.highlightNode = ASImageNode()
self.highlightNode.isUserInteractionEnabled = false
self.textNode = ImmediateTextNode()
self.textNode.isUserInteractionEnabled = false
self.textNode.textAlignment = .center
self.textNode.maximumNumberOfLines = 2
super.init()
self.accessibilityTraits = [.button]
self.addSubnode(self.backgroundContainerNode)
self.backgroundContainerNode.addSubnode(self.backgroundColorNode)
self.backgroundContainerNode.addSubnode(self.backgroundAdditionalColorNode)
self.addSubnode(self.textNode)
self.backgroundContainerNode.addSubnode(self.shadowNode)
self.backgroundContainerNode.addSubnode(self.highlightNode)
self.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted, !strongSelf.bounds.width.isZero {
let scale = (strongSelf.bounds.width - 10.0) / strongSelf.bounds.width
strongSelf.layer.animateScale(from: 1.0, to: scale, duration: 0.15, removeOnCompletion: false)
strongSelf.backgroundContainerNode.layer.removeAnimation(forKey: "opacity")
strongSelf.backgroundContainerNode.alpha = 0.6
} 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)
strongSelf.backgroundContainerNode.alpha = 1.0
strongSelf.backgroundContainerNode.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2)
}
}
}
}
override func setAttributedTitle(_ title: NSAttributedString, for state: UIControl.State) {
self.textNode.attributedText = title
self.accessibilityLabel = title.string
}
private var absoluteRect: (CGRect, CGSize)?
func update(rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition) {
self.absoluteRect = (rect, containerSize)
if let backgroundNode = self.backgroundNode {
var backgroundFrame = backgroundNode.frame
backgroundFrame.origin.x += rect.minX
backgroundFrame.origin.y += rect.minY
backgroundNode.update(rect: backgroundFrame, within: containerSize, transition: transition)
}
}
private func updateIcon() {
guard let theme = self.theme else {
return
}
var iconImage: UIImage?
if let button = self.button {
switch button.action {
case .openWebView:
iconImage = PresentationResourcesChat.chatKeyboardActionButtonWebAppIconImage(theme)
default:
iconImage = nil
}
}
if iconImage != nil {
if self.iconNode == nil {
let iconNode = ASImageNode()
iconNode.contentMode = .center
self.iconNode = iconNode
self.addSubnode(iconNode)
}
self.iconNode?.image = iconImage
} else if let iconNode = self.iconNode {
iconNode.removeFromSupernode()
self.iconNode = nil
}
self.setNeedsLayout()
}
func updateTheme(theme: PresentationTheme, wallpaperBackgroundNode: WallpaperBackgroundNode?) {
if theme !== self.theme {
self.theme = theme
self.highlightNode.image = PresentationResourcesChat.chatInputButtonPanelButtonHighlightImage(theme)
self.shadowNode.image = PresentationResourcesChat.chatInputButtonPanelButtonShadowImage(theme)
self.updateIcon()
}
self.backgroundColorNode.backgroundColor = theme.chat.inputButtonPanel.buttonFillColor
if let alpha = self.backgroundColorNode.backgroundColor?.alpha, alpha < 1.0 {
self.backgroundColorNode.layer.compositingFilter = "softLightBlendMode"
self.backgroundAdditionalColorNode.isHidden = false
} else {
self.backgroundColorNode.layer.compositingFilter = nil
self.backgroundAdditionalColorNode.isHidden = true
}
if wallpaperBackgroundNode?.hasExtraBubbleBackground() == true {
if self.backgroundNode == nil, let backgroundContent = wallpaperBackgroundNode?.makeBubbleBackground(for: .free) {
self.backgroundNode = backgroundContent
self.backgroundContainerNode.insertSubnode(backgroundContent, at: 0)
self.setNeedsLayout()
}
} else {
self.backgroundNode?.removeFromSupernode()
self.backgroundNode = nil
}
}
override func layout() {
super.layout()
self.backgroundContainerNode.frame = self.bounds
self.backgroundColorNode.frame = CGRect(origin: .zero, size: CGSize(width: self.bounds.width, height: self.bounds.height - 1.0))
self.backgroundAdditionalColorNode.frame = self.backgroundColorNode.frame
self.backgroundNode?.frame = self.backgroundColorNode.frame
self.highlightNode.frame = self.bounds
self.shadowNode.frame = self.bounds
if let (rect, containerSize) = self.absoluteRect {
self.update(rect: rect, within: containerSize, transition: .immediate)
}
if let iconNode = self.iconNode {
iconNode.frame = CGRect(x: self.frame.width - 16.0, y: 4.0, width: 12.0, height: 12.0)
}
let textSize = self.textNode.updateLayout(CGSize(width: self.bounds.width - 16.0, height: self.bounds.height))
self.textNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((self.bounds.width - textSize.width) / 2.0), y: floorToScreenPixels((self.bounds.height - textSize.height) / 2.0)), size: textSize)
}
}
final class ChatButtonKeyboardInputNode: ChatInputNode {
private let context: AccountContext
private let controllerInteraction: ChatControllerInteraction
private let separatorNode: ASDisplayNode
private let scrollNode: ASScrollNode
private var backgroundNode: WallpaperBubbleBackgroundNode?
private let backgroundColorNode: ASDisplayNode
private var buttonNodes: [ChatButtonKeyboardInputButtonNode] = []
private var message: Message?
private var theme: PresentationTheme?
init(context: AccountContext, controllerInteraction: ChatControllerInteraction) {
self.context = context
self.controllerInteraction = controllerInteraction
self.scrollNode = ASScrollNode()
self.backgroundColorNode = ASDisplayNode()
self.separatorNode = ASDisplayNode()
self.separatorNode.isLayerBacked = true
super.init()
self.addSubnode(self.backgroundColorNode)
self.addSubnode(self.scrollNode)
self.scrollNode.view.delaysContentTouches = false
self.scrollNode.view.canCancelContentTouches = true
self.scrollNode.view.alwaysBounceHorizontal = false
self.scrollNode.view.alwaysBounceVertical = false
self.addSubnode(self.separatorNode)
}
override func didLoad() {
super.didLoad()
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
}
}
private var absoluteRect: (CGRect, CGSize)?
override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition) {
self.absoluteRect = (rect, containerSize)
if let backgroundNode = self.backgroundNode {
var backgroundFrame = backgroundNode.frame
backgroundFrame.origin.x += rect.minX
backgroundFrame.origin.y += rect.minY
backgroundNode.update(rect: backgroundFrame, within: containerSize, transition: transition)
}
for buttonNode in self.buttonNodes {
var buttonFrame = buttonNode.frame
buttonFrame.origin.x += rect.minX
buttonFrame.origin.y += rect.minY
buttonNode.update(rect: buttonFrame, within: containerSize, transition: transition)
}
}
override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, layoutMetrics: LayoutMetrics, deviceMetrics: DeviceMetrics, isVisible: Bool, isExpanded: Bool) -> (CGFloat, CGFloat) {
transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: UIScreenPixel)))
if self.backgroundNode == nil {
if let backgroundNode = self.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) {
self.backgroundNode = backgroundNode
self.insertSubnode(backgroundNode, at: 0)
}
}
let updatedTheme = self.theme !== interfaceState.theme
if updatedTheme {
self.theme = interfaceState.theme
self.separatorNode.backgroundColor = interfaceState.theme.chat.inputButtonPanel.panelSeparatorColor
self.backgroundColorNode.backgroundColor = interfaceState.theme.chat.inputButtonPanel.panelBackgroundColor
}
var validatedMarkup: ReplyMarkupMessageAttribute?
if let message = interfaceState.keyboardButtonsMessage {
for attribute in message.attributes {
if let replyMarkup = attribute as? ReplyMarkupMessageAttribute {
if !replyMarkup.rows.isEmpty {
validatedMarkup = replyMarkup
}
break
}
}
}
self.message = interfaceState.keyboardButtonsMessage
if let markup = validatedMarkup {
let verticalInset: CGFloat = 10.0
let sideInset: CGFloat = 6.0 + leftInset
var buttonHeight: CGFloat = 43.0
let columnSpacing: CGFloat = 6.0
let rowSpacing: CGFloat = 5.0
var panelHeight = standardInputHeight
let previousRowsHeight = self.scrollNode.view.contentSize.height
let rowsHeight = verticalInset + CGFloat(markup.rows.count) * buttonHeight + CGFloat(max(0, markup.rows.count - 1)) * rowSpacing + verticalInset
if !markup.flags.contains(.fit) && rowsHeight < panelHeight {
buttonHeight = floor((panelHeight - bottomInset - verticalInset * 2.0 - CGFloat(max(0, markup.rows.count - 1)) * rowSpacing) / CGFloat(markup.rows.count))
}
var verticalOffset = verticalInset
var buttonIndex = 0
for row in markup.rows {
let buttonWidth = floor(((width - sideInset - sideInset) + columnSpacing - CGFloat(row.buttons.count) * columnSpacing) / CGFloat(row.buttons.count))
var columnIndex = 0
for button in row.buttons {
let buttonNode: ChatButtonKeyboardInputButtonNode
if buttonIndex < self.buttonNodes.count {
buttonNode = self.buttonNodes[buttonIndex]
} else {
buttonNode = ChatButtonKeyboardInputButtonNode()
buttonNode.titleNode.maximumNumberOfLines = 2
buttonNode.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: [.touchUpInside])
self.scrollNode.addSubnode(buttonNode)
self.buttonNodes.append(buttonNode)
}
buttonNode.updateTheme(theme: interfaceState.theme, wallpaperBackgroundNode: self.controllerInteraction.presentationContext.backgroundNode)
buttonIndex += 1
if buttonNode.button != button || updatedTheme {
buttonNode.button = button
buttonNode.setAttributedTitle(NSAttributedString(string: button.title, font: Font.regular(16.0), textColor: interfaceState.theme.chat.inputButtonPanel.buttonTextColor, paragraphAlignment: .center), for: [])
}
buttonNode.frame = CGRect(origin: CGPoint(x: sideInset + CGFloat(columnIndex) * (buttonWidth + columnSpacing), y: verticalOffset), size: CGSize(width: buttonWidth, height: buttonHeight))
columnIndex += 1
}
verticalOffset += buttonHeight + rowSpacing
}
for i in (buttonIndex ..< self.buttonNodes.count).reversed() {
self.buttonNodes[i].removeFromSupernode()
self.buttonNodes.remove(at: i)
}
if markup.flags.contains(.fit) {
panelHeight = min(panelHeight + bottomInset, rowsHeight + bottomInset)
}
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: panelHeight)))
self.scrollNode.view.contentSize = CGSize(width: width, height: rowsHeight)
self.scrollNode.view.contentInset = UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomInset, right: 0.0)
if previousRowsHeight != rowsHeight {
self.scrollNode.view.setContentOffset(CGPoint(), animated: false)
}
if let backgroundNode = self.backgroundNode {
backgroundNode.frame = CGRect(origin: .zero, size: CGSize(width: width, height: panelHeight))
}
self.backgroundColorNode.frame = CGRect(origin: .zero, size: CGSize(width: width, height: panelHeight))
if let (rect, containerSize) = self.absoluteRect {
self.updateAbsoluteRect(rect, within: containerSize, transition: transition)
}
return (panelHeight, 0.0)
} else {
return (0.0, 0.0)
}
}
@objc func buttonPressed(_ button: ASButtonNode) {
if let button = button as? ChatButtonKeyboardInputButtonNode, let markupButton = button.button {
var dismissIfOnce = false
switch markupButton.action {
case .text:
self.controllerInteraction.sendMessage(markupButton.title)
dismissIfOnce = true
case let .url(url):
self.controllerInteraction.openUrl(url, true, nil, nil)
case .requestMap:
self.controllerInteraction.shareCurrentLocation()
case .requestPhone:
self.controllerInteraction.shareAccountContact()
case .openWebApp:
if let message = self.message {
self.controllerInteraction.requestMessageActionCallback(message.id, nil, true, false)
}
case let .callback(requiresPassword, data):
if let message = self.message {
self.controllerInteraction.requestMessageActionCallback(message.id, data, false, requiresPassword)
}
case let .switchInline(samePeer, query):
if let message = message {
var botPeer: Peer?
var found = false
for attribute in message.attributes {
if let attribute = attribute as? InlineBotMessageAttribute, let peerId = attribute.peerId {
botPeer = message.peers[peerId]
found = true
}
}
if !found {
botPeer = message.author
}
var peer: Peer?
if samePeer {
peer = message.peers[message.id.peerId]
} else {
peer = botPeer
}
if let peer = peer, let botPeer = botPeer, let addressName = botPeer.addressName {
self.controllerInteraction.openPeer(EnginePeer(peer), .chat(textInputState: ChatTextInputState(inputText: NSAttributedString(string: "@\(addressName) \(query)")), subject: nil, peekData: nil), nil, .default)
}
}
case .payment:
break
case let .urlAuth(url, buttonId):
if let message = self.message {
self.controllerInteraction.requestMessageActionUrlAuth(url, .message(id: message.id, buttonId: buttonId))
}
case let .setupPoll(isQuiz):
self.controllerInteraction.openPollCreation(isQuiz)
case let .openUserProfile(peerId):
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let self, let peer else {
return
}
self.controllerInteraction.openPeer(peer, .info, nil, .default)
})
case let .openWebView(url, simple):
self.controllerInteraction.openWebView(markupButton.title, url, simple, .generic)
case let .requestPeer(peerType, buttonId):
if let message = self.message {
self.controllerInteraction.openRequestedPeerSelection(message.id, peerType, buttonId)
}
}
if dismissIfOnce {
if let message = self.message {
for attribute in message.attributes {
if let attribute = attribute as? ReplyMarkupMessageAttribute {
if attribute.flags.contains(.once) {
self.controllerInteraction.dismissReplyMarkupMessage(message)
}
break
}
}
}
}
}
}
}