Swiftgram/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift
2020-06-26 21:05:21 +04:00

1935 lines
102 KiB
Swift

import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
import SyncCore
import MobileCoreServices
import TelegramPresentationData
import TextFormat
import AccountContext
import TouchDownGesture
import ImageTransparency
import ActivityIndicator
import AnimationUI
private let accessoryButtonFont = Font.medium(14.0)
private final class AccessoryItemIconButton: HighlightTrackingButton {
private let item: ChatTextInputAccessoryItem
private var width: CGFloat
private let imageNode: ASImageNode
init(item: ChatTextInputAccessoryItem, theme: PresentationTheme, strings: PresentationStrings) {
self.imageNode = ASImageNode()
self.imageNode.isLayerBacked = true
self.imageNode.displaysAsynchronously = false
self.imageNode.displayWithoutProcessing = true
self.item = item
let (image, text, alpha, insets) = AccessoryItemIconButton.imageAndInsets(item: item, theme: theme, strings: strings)
self.width = AccessoryItemIconButton.calculateWidth(item: item, image: image, text: text, strings: strings)
super.init(frame: CGRect())
self.addSubnode(self.imageNode)
if let text = text {
self.titleLabel?.font = accessoryButtonFont
self.setTitleColor(theme.chat.inputPanel.inputControlColor, for: [])
self.setTitle(text, for: [])
}
self.imageNode.image = image
self.imageNode.alpha = alpha
self.imageEdgeInsets = insets
self.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.layer.removeAnimation(forKey: "opacity")
strongSelf.alpha = 0.4
} else {
strongSelf.alpha = 1.0
strongSelf.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
}
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
let (image, text, alpha, insets) = AccessoryItemIconButton.imageAndInsets(item: item, theme: theme, strings: strings)
self.width = AccessoryItemIconButton.calculateWidth(item: item, image: image, text: text, strings: strings)
if let text = text {
self.titleLabel?.font = accessoryButtonFont
self.setTitleColor(theme.chat.inputPanel.inputControlColor, for: [])
self.setTitle(text, for: [])
} else {
self.setTitle("", for: [])
}
self.imageNode.image = image
self.imageEdgeInsets = insets
self.imageNode.alpha = alpha
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
static func imageAndInsets(item: ChatTextInputAccessoryItem, theme: PresentationTheme, strings: PresentationStrings) -> (UIImage?, String?, CGFloat, UIEdgeInsets) {
switch item {
case .keyboard:
return (PresentationResourcesChat.chatInputTextFieldKeyboardImage(theme), nil, 1.0, UIEdgeInsets())
case let .stickers(enabled):
return (PresentationResourcesChat.chatInputTextFieldStickersImage(theme), nil, enabled ? 1.0 : 0.4, UIEdgeInsets())
case .inputButtons:
return (PresentationResourcesChat.chatInputTextFieldInputButtonsImage(theme), nil, 1.0, UIEdgeInsets())
case .commands:
return (PresentationResourcesChat.chatInputTextFieldCommandsImage(theme), nil, 1.0, UIEdgeInsets())
case let .silentPost(value):
if value {
return (PresentationResourcesChat.chatInputTextFieldSilentPostOnImage(theme), nil, 1.0, UIEdgeInsets())
} else {
return (PresentationResourcesChat.chatInputTextFieldSilentPostOffImage(theme), nil, 1.0, UIEdgeInsets())
}
case let .messageAutoremoveTimeout(timeout):
if let timeout = timeout {
return (nil, shortTimeIntervalString(strings: strings, value: timeout), 1.0, UIEdgeInsets())
} else {
return (PresentationResourcesChat.chatInputTextFieldTimerImage(theme), nil, 1.0, UIEdgeInsets(top: 0.0, left: 0.0, bottom: 1.0, right: 0.0))
}
case .scheduledMessages:
return (PresentationResourcesChat.chatInputTextFieldScheduleImage(theme), nil, 1.0, UIEdgeInsets())
}
}
static func calculateWidth(item: ChatTextInputAccessoryItem, image: UIImage?, text: String?, strings: PresentationStrings) -> CGFloat {
switch item {
case .keyboard, .stickers, .inputButtons, .silentPost, .commands, .scheduledMessages:
return (image?.size.width ?? 0.0) + CGFloat(8.0)
case let .messageAutoremoveTimeout(timeout):
var imageWidth = (image?.size.width ?? 0.0) + CGFloat(8.0)
if let _ = timeout, let text = text {
imageWidth = ceil((text as NSString).size(withAttributes: [.font: accessoryButtonFont]).width) + 10.0
}
return max(imageWidth, 24.0)
}
}
func updateLayout(size: CGSize) {
if let image = self.imageNode.image {
self.imageNode.frame = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0) - self.imageEdgeInsets.bottom), size: image.size)
}
}
var buttonWidth: CGFloat {
return self.width
}
}
private func calclulateTextFieldMinHeight(_ presentationInterfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat {
let baseFontSize = max(17.0, presentationInterfaceState.fontSize.baseDisplaySize)
var result: CGFloat
if baseFontSize.isEqual(to: 26.0) {
result = 42.0
} else if baseFontSize.isEqual(to: 23.0) {
result = 38.0
} else if baseFontSize.isEqual(to: 17.0) {
result = 31.0
} else if baseFontSize.isEqual(to: 19.0) {
result = 33.0
} else if baseFontSize.isEqual(to: 21.0) {
result = 35.0
} else {
result = 31.0
}
if case .regular = metrics.widthClass {
result = max(33.0, result)
}
return result
}
private var currentTextInputBackgroundImage: (UIColor, UIColor, CGFloat, UIImage)?
private func textInputBackgroundImage(backgroundColor: UIColor, strokeColor: UIColor, diameter: CGFloat) -> UIImage? {
if let current = currentTextInputBackgroundImage {
if current.0.isEqual(backgroundColor) && current.1.isEqual(strokeColor) && current.2.isEqual(to: diameter) {
return current.3
}
}
let image = generateImage(CGSize(width: diameter, height: diameter), rotatedContext: { size, context in
context.setFillColor(backgroundColor.cgColor)
context.fill(CGRect(x: 0.0, y: 0.0, width: diameter, height: diameter))
context.setBlendMode(.clear)
context.setFillColor(UIColor.clear.cgColor)
context.fillEllipse(in: CGRect(x: 0.0, y: 0.0, width: diameter, height: diameter))
context.setBlendMode(.normal)
context.setStrokeColor(strokeColor.cgColor)
let strokeWidth: CGFloat = 1.0
context.setLineWidth(strokeWidth)
context.strokeEllipse(in: CGRect(x: strokeWidth / 2.0, y: strokeWidth / 2.0, width: diameter - strokeWidth, height: diameter - strokeWidth))
})?.stretchableImage(withLeftCapWidth: Int(diameter) / 2, topCapHeight: Int(diameter) / 2)
if let image = image {
currentTextInputBackgroundImage = (backgroundColor, strokeColor, diameter, image)
return image
} else {
return nil
}
}
enum ChatTextInputPanelPasteData {
case images([UIImage])
case gif(Data)
case sticker(UIImage, Bool)
}
class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
var textPlaceholderNode: ImmediateTextNode
var contextPlaceholderNode: TextNode?
var slowmodePlaceholderNode: ChatTextInputSlowmodePlaceholderNode?
let textInputContainer: ASDisplayNode
var textInputNode: EditableTextNode?
let textInputBackgroundNode: ASImageNode
let actionButtons: ChatTextInputActionButtonsNode
var mediaRecordingAccessibilityArea: AccessibilityAreaNode?
let attachmentButton: HighlightableButtonNode
let attachmentButtonDisabledNode: HighlightableButtonNode
let searchLayoutClearButton: HighlightableButton
private let searchLayoutClearImageNode: ASImageNode
private var searchActivityIndicator: ActivityIndicator?
var audioRecordingInfoContainerNode: ASDisplayNode?
var audioRecordingDotNode: AnimationNode?
var audioRecordingTimeNode: ChatTextInputAudioRecordingTimeNode?
var audioRecordingCancelIndicator: ChatTextInputAudioRecordingCancelIndicator?
private var accessoryItemButtons: [(ChatTextInputAccessoryItem, AccessoryItemIconButton)] = []
private var validLayout: (CGFloat, CGFloat, CGFloat, CGFloat, LayoutMetrics, Bool)?
var displayAttachmentMenu: () -> Void = { }
var sendMessage: () -> Void = { }
var paste: (ChatTextInputPanelPasteData) -> Void = { _ in }
var updateHeight: (Bool) -> Void = { _ in }
var updateActivity: () -> Void = { }
private var updatingInputState = false
private var currentPlaceholder: String?
private var presentationInterfaceState: ChatPresentationInterfaceState?
private var initializedPlaceholder = false
private var keepSendButtonEnabled = false
private var extendedSearchLayout = false
var isMediaDeleted: Bool = false
private let inputMenu = ChatTextInputMenu()
private var theme: PresentationTheme?
private var strings: PresentationStrings?
var inputTextState: ChatTextInputState {
if let textInputNode = self.textInputNode {
let selectionRange: Range<Int> = textInputNode.selectedRange.location ..< (textInputNode.selectedRange.location + textInputNode.selectedRange.length)
return ChatTextInputState(inputText: stateAttributedStringForText(textInputNode.attributedText ?? NSAttributedString()), selectionRange: selectionRange)
} else {
return ChatTextInputState()
}
}
var storedInputLanguage: String?
var effectiveInputLanguage: String? {
if let textInputNode = textInputNode, textInputNode.isFirstResponder() {
return textInputNode.textInputMode.primaryLanguage
} else {
return self.storedInputLanguage
}
}
var enablePredictiveInput: Bool = true {
didSet {
if let textInputNode = self.textInputNode {
textInputNode.textView.autocorrectionType = self.enablePredictiveInput ? .default : .no
}
}
}
override var context: AccountContext? {
didSet {
self.actionButtons.micButton.account = self.context?.account
}
}
private let statusDisposable = MetaDisposable()
override var interfaceInteraction: ChatPanelInterfaceInteraction? {
didSet {
if let statuses = self.interfaceInteraction?.statuses {
self.statusDisposable.set((statuses.inlineSearch
|> distinctUntilChanged
|> deliverOnMainQueue).start(next: { [weak self] value in
self?.updateIsProcessingInlineRequest(value)
}))
}
}
}
func updateInputTextState(_ state: ChatTextInputState, keepSendButtonEnabled: Bool, extendedSearchLayout: Bool, accessoryItems: [ChatTextInputAccessoryItem], animated: Bool) {
if state.inputText.length != 0 && self.textInputNode == nil {
self.loadTextInputNode()
}
if let textInputNode = self.textInputNode, let currentState = self.presentationInterfaceState {
self.updatingInputState = true
var updateAccessoryButtons = false
if accessoryItems.count == self.accessoryItemButtons.count {
for i in 0 ..< accessoryItems.count {
if accessoryItems[i] != self.accessoryItemButtons[i].0 {
updateAccessoryButtons = true
break
}
}
} else {
updateAccessoryButtons = true
}
if updateAccessoryButtons {
var updatedButtons: [(ChatTextInputAccessoryItem, AccessoryItemIconButton)] = []
for item in accessoryItems {
var itemAndButton: (ChatTextInputAccessoryItem, AccessoryItemIconButton)?
for i in 0 ..< self.accessoryItemButtons.count {
if self.accessoryItemButtons[i].0 == item {
itemAndButton = self.accessoryItemButtons[i]
self.accessoryItemButtons.remove(at: i)
break
}
}
if itemAndButton == nil {
let button = AccessoryItemIconButton(item: item, theme: currentState.theme, strings: currentState.strings)
button.addTarget(self, action: #selector(self.accessoryItemButtonPressed(_:)), for: [.touchUpInside])
itemAndButton = (item, button)
}
updatedButtons.append(itemAndButton!)
}
for (_, button) in self.accessoryItemButtons {
button.removeFromSuperview()
}
self.accessoryItemButtons = updatedButtons
}
var textColor: UIColor = .black
var accentTextColor: UIColor = .blue
var baseFontSize: CGFloat = 17.0
if let presentationInterfaceState = self.presentationInterfaceState {
textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor
accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor
baseFontSize = max(17.0, presentationInterfaceState.fontSize.baseDisplaySize)
}
textInputNode.attributedText = textAttributedStringForStateText(state.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil)
textInputNode.selectedRange = NSMakeRange(state.selectionRange.lowerBound, state.selectionRange.count)
self.updatingInputState = false
self.keepSendButtonEnabled = keepSendButtonEnabled
self.extendedSearchLayout = extendedSearchLayout
self.updateTextNodeText(animated: animated)
}
}
func updateKeepSendButtonEnabled(keepSendButtonEnabled: Bool, extendedSearchLayout: Bool, animated: Bool) {
if keepSendButtonEnabled != self.keepSendButtonEnabled || extendedSearchLayout != self.extendedSearchLayout {
self.keepSendButtonEnabled = keepSendButtonEnabled
self.extendedSearchLayout = extendedSearchLayout
self.updateTextNodeText(animated: animated)
}
}
var text: String {
get {
return self.textInputNode?.attributedText?.string ?? ""
} set(value) {
if let textInputNode = self.textInputNode {
var textColor: UIColor = .black
var baseFontSize: CGFloat = 17.0
if let presentationInterfaceState = self.presentationInterfaceState {
textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor
baseFontSize = max(17.0, presentationInterfaceState.fontSize.baseDisplaySize)
}
textInputNode.attributedText = NSAttributedString(string: value, font: Font.regular(baseFontSize), textColor: textColor)
self.editableTextNodeDidUpdateText(textInputNode)
}
}
}
private let textInputViewInternalInsets = UIEdgeInsets(top: 1.0, left: 13.0, bottom: 1.0, right: 13.0)
private let textInputViewRealInsets = UIEdgeInsets(top: 4.5, left: 0.0, bottom: 5.5, right: 0.0)
private let accessoryButtonSpacing: CGFloat = 0.0
private let accessoryButtonInset: CGFloat = 2.0
init(presentationInterfaceState: ChatPresentationInterfaceState, presentController: @escaping (ViewController) -> Void) {
self.presentationInterfaceState = presentationInterfaceState
self.textInputContainer = ASDisplayNode()
self.textInputContainer.clipsToBounds = true
self.textInputContainer.backgroundColor = presentationInterfaceState.theme.chat.inputPanel.inputBackgroundColor
self.textInputBackgroundNode = ASImageNode()
self.textInputBackgroundNode.displaysAsynchronously = false
self.textInputBackgroundNode.displayWithoutProcessing = true
self.textPlaceholderNode = ImmediateTextNode()
self.textPlaceholderNode.maximumNumberOfLines = 1
self.textPlaceholderNode.isUserInteractionEnabled = false
self.attachmentButton = HighlightableButtonNode(pointerStyle: .circle)
self.attachmentButton.accessibilityLabel = presentationInterfaceState.strings.VoiceOver_AttachMedia
self.attachmentButton.isAccessibilityElement = true
self.attachmentButtonDisabledNode = HighlightableButtonNode()
self.searchLayoutClearButton = HighlightableButton()
self.searchLayoutClearImageNode = ASImageNode()
self.searchLayoutClearImageNode.isUserInteractionEnabled = false
self.searchLayoutClearButton.addSubnode(self.searchLayoutClearImageNode)
self.actionButtons = ChatTextInputActionButtonsNode(theme: presentationInterfaceState.theme, strings: presentationInterfaceState.strings, presentController: presentController)
super.init()
self.attachmentButton.addTarget(self, action: #selector(self.attachmentButtonPressed), forControlEvents: .touchUpInside)
self.attachmentButtonDisabledNode.addTarget(self, action: #selector(self.attachmentButtonPressed), forControlEvents: .touchUpInside)
self.actionButtons.sendButtonLongPressed = { [weak self] node, gesture in
self?.interfaceInteraction?.displaySendMessageOptions(node, gesture)
}
self.actionButtons.micButton.recordingDisabled = { [weak self] in
self?.interfaceInteraction?.displayRestrictedInfo(.mediaRecording, .tooltip)
}
self.actionButtons.micButton.beginRecording = { [weak self] in
if let strongSelf = self, let presentationInterfaceState = strongSelf.presentationInterfaceState, let interfaceInteraction = strongSelf.interfaceInteraction {
let isVideo: Bool
switch presentationInterfaceState.interfaceState.mediaRecordingMode {
case .audio:
isVideo = false
case .video:
isVideo = true
}
interfaceInteraction.beginMediaRecording(isVideo)
}
}
self.actionButtons.micButton.endRecording = { [weak self] sendMedia in
if let strongSelf = self, let interfaceState = strongSelf.presentationInterfaceState, let interfaceInteraction = strongSelf.interfaceInteraction {
if let _ = interfaceState.inputTextPanelState.mediaRecordingState {
if sendMedia {
interfaceInteraction.finishMediaRecording(.send)
} else {
interfaceInteraction.finishMediaRecording(.dismiss)
}
} else {
interfaceInteraction.finishMediaRecording(.dismiss)
}
}
}
self.actionButtons.micButton.offsetRecordingControls = { [weak self] in
if let strongSelf = self, let presentationInterfaceState = strongSelf.presentationInterfaceState {
if let (width, leftInset, rightInset, maxHeight, metrics, isSecondary) = strongSelf.validLayout {
let _ = strongSelf.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, maxHeight: maxHeight, isSecondary: isSecondary, transition: .immediate, interfaceState: presentationInterfaceState, metrics: metrics)
}
}
}
self.actionButtons.micButton.updateCancelTranslation = { [weak self] in
if let strongSelf = self, let presentationInterfaceState = strongSelf.presentationInterfaceState {
if let (width, leftInset, rightInset, maxHeight, metrics, isSecondary) = strongSelf.validLayout {
let _ = strongSelf.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, maxHeight: maxHeight, isSecondary: isSecondary, transition: .immediate, interfaceState: presentationInterfaceState, metrics: metrics)
}
}
}
self.actionButtons.micButton.stopRecording = { [weak self] in
if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction {
interfaceInteraction.stopMediaRecording()
}
}
self.actionButtons.micButton.updateLocked = { [weak self] _ in
if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction {
interfaceInteraction.lockMediaRecording()
}
}
self.actionButtons.micButton.switchMode = { [weak self] in
if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction {
interfaceInteraction.switchMediaRecordingMode()
}
}
self.actionButtons.sendButton.addTarget(self, action: #selector(self.sendButtonPressed), forControlEvents: .touchUpInside)
self.actionButtons.sendButton.alpha = 0.0
self.actionButtons.updateAccessibility()
self.actionButtons.expandMediaInputButton.addTarget(self, action: #selector(self.expandButtonPressed), forControlEvents: .touchUpInside)
self.actionButtons.expandMediaInputButton.alpha = 0.0
self.searchLayoutClearButton.addTarget(self, action: #selector(self.searchLayoutClearButtonPressed), for: .touchUpInside)
self.searchLayoutClearButton.alpha = 0.0
self.addSubnode(self.textInputContainer)
self.addSubnode(self.textInputBackgroundNode)
self.addSubnode(self.textPlaceholderNode)
self.addSubnode(self.attachmentButton)
self.addSubnode(self.attachmentButtonDisabledNode)
self.addSubnode(self.actionButtons)
self.view.addSubview(self.searchLayoutClearButton)
self.textInputBackgroundNode.clipsToBounds = true
let recognizer = TouchDownGestureRecognizer(target: self, action: #selector(self.textInputBackgroundViewTap(_:)))
recognizer.touchDown = { [weak self] in
if let strongSelf = self {
strongSelf.ensureFocused()
}
}
self.textInputBackgroundNode.isUserInteractionEnabled = true
self.textInputBackgroundNode.view.addGestureRecognizer(recognizer)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.statusDisposable.dispose()
}
func loadTextInputNodeIfNeeded() {
if self.textInputNode == nil {
self.loadTextInputNode()
}
}
private func loadTextInputNode() {
let textInputNode = EditableTextNode()
textInputNode.initialPrimaryLanguage = self.presentationInterfaceState?.interfaceState.inputLanguage
var textColor: UIColor = .black
var tintColor: UIColor = .blue
var baseFontSize: CGFloat = 17.0
var keyboardAppearance: UIKeyboardAppearance = UIKeyboardAppearance.default
if let presentationInterfaceState = self.presentationInterfaceState {
textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor
tintColor = presentationInterfaceState.theme.list.itemAccentColor
baseFontSize = max(17.0, presentationInterfaceState.fontSize.baseDisplaySize)
keyboardAppearance = presentationInterfaceState.theme.rootController.keyboardColor.keyboardAppearance
}
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = 1.0
paragraphStyle.lineHeightMultiple = 1.0
paragraphStyle.paragraphSpacing = 1.0
paragraphStyle.maximumLineHeight = 20.0
paragraphStyle.minimumLineHeight = 20.0
textInputNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(max(17.0, baseFontSize)), NSAttributedString.Key.foregroundColor.rawValue: textColor, NSAttributedString.Key.paragraphStyle.rawValue: paragraphStyle]
textInputNode.clipsToBounds = false
textInputNode.textView.clipsToBounds = false
textInputNode.delegate = self
textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0)
textInputNode.keyboardAppearance = keyboardAppearance
textInputNode.textContainerInset = UIEdgeInsets(top: self.textInputViewRealInsets.top, left: 0.0, bottom: self.textInputViewRealInsets.bottom, right: 0.0)
textInputNode.tintColor = tintColor
textInputNode.textView.scrollIndicatorInsets = UIEdgeInsets(top: 9.0, left: 0.0, bottom: 9.0, right: -13.0)
self.textInputContainer.addSubnode(textInputNode)
textInputNode.view.disablesInteractiveTransitionGestureRecognizer = true
self.textInputNode = textInputNode
if let presentationInterfaceState = self.presentationInterfaceState {
refreshChatTextInputTypingAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize)
}
if !self.textInputContainer.bounds.size.width.isZero {
let textInputFrame = self.textInputContainer.frame
var accessoryButtonsWidth: CGFloat = 0.0
var firstButton = true
for (_, button) in self.accessoryItemButtons {
if firstButton {
firstButton = false
accessoryButtonsWidth += accessoryButtonInset
} else {
accessoryButtonsWidth += accessoryButtonSpacing
}
accessoryButtonsWidth += button.buttonWidth
}
textInputNode.frame = CGRect(origin: CGPoint(x: self.textInputViewInternalInsets.left, y: self.textInputViewInternalInsets.top), size: CGSize(width: textInputFrame.size.width - (self.textInputViewInternalInsets.left + self.textInputViewInternalInsets.right + accessoryButtonsWidth), height: textInputFrame.size.height - self.textInputViewInternalInsets.top - self.textInputViewInternalInsets.bottom))
}
self.textInputBackgroundNode.isUserInteractionEnabled = false
self.textInputBackgroundNode.view.removeGestureRecognizer(self.textInputBackgroundNode.view.gestureRecognizers![0])
let recognizer = TouchDownGestureRecognizer(target: self, action: #selector(self.textInputBackgroundViewTap(_:)))
recognizer.touchDown = { [weak self] in
if let strongSelf = self {
strongSelf.ensureFocused()
}
}
textInputNode.view.addGestureRecognizer(recognizer)
textInputNode.textView.accessibilityHint = self.textPlaceholderNode.attributedText?.string
}
private func textFieldMaxHeight(_ maxHeight: CGFloat, metrics: LayoutMetrics) -> CGFloat {
let textFieldInsets = self.textFieldInsets(metrics: metrics)
return max(33.0, maxHeight - (textFieldInsets.top + textFieldInsets.bottom + self.textInputViewInternalInsets.top + self.textInputViewInternalInsets.bottom))
}
private func calculateTextFieldMetrics(width: CGFloat, maxHeight: CGFloat, metrics: LayoutMetrics) -> (accessoryButtonsWidth: CGFloat, textFieldHeight: CGFloat) {
let accessoryButtonInset = self.accessoryButtonInset
let accessoryButtonSpacing = self.accessoryButtonSpacing
let textFieldInsets = self.textFieldInsets(metrics: metrics)
let fieldMaxHeight = textFieldMaxHeight(maxHeight, metrics: metrics)
var accessoryButtonsWidth: CGFloat = 0.0
var firstButton = true
for (_, button) in self.accessoryItemButtons {
if firstButton {
firstButton = false
accessoryButtonsWidth += accessoryButtonInset
} else {
accessoryButtonsWidth += accessoryButtonSpacing
}
accessoryButtonsWidth += button.buttonWidth
}
var textFieldMinHeight: CGFloat = 35.0
if let presentationInterfaceState = self.presentationInterfaceState {
textFieldMinHeight = calclulateTextFieldMinHeight(presentationInterfaceState, metrics: metrics)
}
let textFieldHeight: CGFloat
if let textInputNode = self.textInputNode {
let measuredHeight = textInputNode.measure(CGSize(width: width - textFieldInsets.left - textFieldInsets.right - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right - accessoryButtonsWidth, height: CGFloat.greatestFiniteMagnitude))
let unboundTextFieldHeight = max(textFieldMinHeight, ceil(measuredHeight.height))
let maxNumberOfLines = min(12, (Int(fieldMaxHeight - 11.0) - 33) / 22)
let updatedMaxHeight = (CGFloat(maxNumberOfLines) * 22.0 + 10.0)
textFieldHeight = max(textFieldMinHeight, min(updatedMaxHeight, unboundTextFieldHeight))
} else {
textFieldHeight = textFieldMinHeight
}
return (accessoryButtonsWidth, textFieldHeight)
}
private func textFieldInsets(metrics: LayoutMetrics) -> UIEdgeInsets {
var insets = UIEdgeInsets(top: 6.0, left: 42.0, bottom: 6.0, right: 42.0)
if case .regular = metrics.widthClass, case .regular = metrics.heightClass {
insets.top += 1.0
insets.bottom += 1.0
}
return insets
}
private func panelHeight(textFieldHeight: CGFloat, metrics: LayoutMetrics) -> CGFloat {
let textFieldInsets = self.textFieldInsets(metrics: metrics)
let result = textFieldHeight + textFieldInsets.top + textFieldInsets.bottom + self.textInputViewInternalInsets.top + self.textInputViewInternalInsets.bottom
return result
}
override func minimalHeight(interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat {
let textFieldMinHeight = calclulateTextFieldMinHeight(interfaceState, metrics: metrics)
var minimalHeight: CGFloat = 14.0 + textFieldMinHeight
if case .regular = metrics.widthClass, case .regular = metrics.heightClass {
minimalHeight += 2.0
}
return minimalHeight
}
override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat {
self.validLayout = (width, leftInset, rightInset, maxHeight, metrics, isSecondary)
let baseWidth = width - leftInset - rightInset
var wasEditingMedia = false
if let interfaceState = self.presentationInterfaceState, let editMessageState = interfaceState.editMessageState {
if case let .media(value) = editMessageState.content {
wasEditingMedia = !value.isEmpty
}
}
var isMediaEnabled = true
var isEditingMedia = false
if let editMessageState = interfaceState.editMessageState {
if case let .media(value) = editMessageState.content {
isEditingMedia = !value.isEmpty
isMediaEnabled = !value.isEmpty
} else {
isMediaEnabled = false
}
}
var isSlowmodeActive = false
if interfaceState.slowmodeState != nil && !interfaceState.isScheduledMessages {
isSlowmodeActive = true
if !isEditingMedia {
isMediaEnabled = false
}
}
transition.updateAlpha(layer: self.attachmentButton.layer, alpha: isMediaEnabled ? 1.0 : 0.4)
self.attachmentButton.isEnabled = isMediaEnabled
self.attachmentButtonDisabledNode.isHidden = !isSlowmodeActive || isMediaEnabled
if self.presentationInterfaceState != interfaceState {
let previousState = self.presentationInterfaceState
self.presentationInterfaceState = interfaceState
let themeUpdated = previousState?.theme !== interfaceState.theme
var updateSendButtonIcon = false
if (previousState?.interfaceState.editMessage != nil) != (interfaceState.interfaceState.editMessage != nil) {
updateSendButtonIcon = true
}
if self.theme !== interfaceState.theme {
updateSendButtonIcon = true
if self.theme == nil || !self.theme!.chat.inputPanel.inputTextColor.isEqual(interfaceState.theme.chat.inputPanel.inputTextColor) {
let textColor = interfaceState.theme.chat.inputPanel.inputTextColor
let baseFontSize = max(17.0, interfaceState.fontSize.baseDisplaySize)
if let textInputNode = self.textInputNode {
if let text = textInputNode.attributedText?.string {
let range = textInputNode.selectedRange
textInputNode.attributedText = NSAttributedString(string: text, font: Font.regular(baseFontSize), textColor: textColor)
textInputNode.selectedRange = range
}
textInputNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(baseFontSize), NSAttributedString.Key.foregroundColor.rawValue: textColor]
}
}
let keyboardAppearance = interfaceState.theme.rootController.keyboardColor.keyboardAppearance
if let textInputNode = self.textInputNode, textInputNode.keyboardAppearance != keyboardAppearance, textInputNode.isFirstResponder() {
if textInputNode.isCurrentlyEmoji() {
textInputNode.initialPrimaryLanguage = "emoji"
textInputNode.resetInitialPrimaryLanguage()
}
textInputNode.keyboardAppearance = keyboardAppearance
}
self.textInputContainer.backgroundColor = interfaceState.theme.chat.inputPanel.inputBackgroundColor
self.theme = interfaceState.theme
if isEditingMedia {
self.attachmentButton.setImage(PresentationResourcesChat.chatInputPanelEditAttachmentButtonImage(interfaceState.theme), for: [])
} else {
self.attachmentButton.setImage(PresentationResourcesChat.chatInputPanelAttachmentButtonImage(interfaceState.theme), for: [])
}
self.actionButtons.updateTheme(theme: interfaceState.theme)
let textFieldMinHeight = calclulateTextFieldMinHeight(interfaceState, metrics: metrics)
let minimalInputHeight: CGFloat = 2.0 + textFieldMinHeight
let backgroundColor: UIColor
if case let .color(color) = interfaceState.chatWallpaper, UIColor(rgb: color).isEqual(interfaceState.theme.chat.inputPanel.panelBackgroundColorNoWallpaper) {
backgroundColor = interfaceState.theme.chat.inputPanel.panelBackgroundColorNoWallpaper
} else {
backgroundColor = interfaceState.theme.chat.inputPanel.panelBackgroundColor
}
self.textInputBackgroundNode.image = textInputBackgroundImage(backgroundColor: backgroundColor, strokeColor: interfaceState.theme.chat.inputPanel.inputStrokeColor, diameter: minimalInputHeight)
self.searchLayoutClearImageNode.image = PresentationResourcesChat.chatInputTextFieldClearImage(interfaceState.theme)
self.audioRecordingTimeNode?.updateTheme(theme: interfaceState.theme)
self.audioRecordingCancelIndicator?.updateTheme(theme: interfaceState.theme)
for (_, button) in self.accessoryItemButtons {
button.updateThemeAndStrings(theme: interfaceState.theme, strings: interfaceState.strings)
}
} else {
if self.strings !== interfaceState.strings {
self.strings = interfaceState.strings
self.inputMenu.updateStrings(interfaceState.strings)
for (_, button) in self.accessoryItemButtons {
button.updateThemeAndStrings(theme: interfaceState.theme, strings: interfaceState.strings)
}
}
if wasEditingMedia != isEditingMedia {
if isEditingMedia {
self.attachmentButton.setImage(PresentationResourcesChat.chatInputPanelEditAttachmentButtonImage(interfaceState.theme), for: [])
} else {
self.attachmentButton.setImage(PresentationResourcesChat.chatInputPanelAttachmentButtonImage(interfaceState.theme), for: [])
}
}
}
if let peer = interfaceState.renderedPeer?.peer, previousState?.renderedPeer?.peer == nil || !peer.isEqual(previousState!.renderedPeer!.peer!) || previousState?.interfaceState.silentPosting != interfaceState.interfaceState.silentPosting || themeUpdated || !self.initializedPlaceholder {
self.initializedPlaceholder = true
let placeholder: String
if let channel = peer as? TelegramChannel, case .broadcast = channel.info {
if interfaceState.interfaceState.silentPosting {
placeholder = interfaceState.strings.Conversation_InputTextSilentBroadcastPlaceholder
} else {
placeholder = interfaceState.strings.Conversation_InputTextBroadcastPlaceholder
}
} else {
placeholder = interfaceState.strings.Conversation_InputTextPlaceholder
}
if self.currentPlaceholder != placeholder || themeUpdated {
self.currentPlaceholder = placeholder
let baseFontSize = max(17.0, interfaceState.fontSize.baseDisplaySize)
self.textPlaceholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(baseFontSize), textColor: interfaceState.theme.chat.inputPanel.inputPlaceholderColor)
self.textInputNode?.textView.accessibilityHint = placeholder
let placeholderSize = self.textPlaceholderNode.updateLayout(CGSize(width: 320.0, height: CGFloat.greatestFiniteMagnitude))
if transition.isAnimated, let snapshotLayer = self.textPlaceholderNode.layer.snapshotContentTree() {
self.textPlaceholderNode.supernode?.layer.insertSublayer(snapshotLayer, above: self.textPlaceholderNode.layer)
snapshotLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.22, removeOnCompletion: false, completion: { [weak snapshotLayer] _ in
snapshotLayer?.removeFromSuperlayer()
})
self.textPlaceholderNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18)
}
self.textPlaceholderNode.frame = CGRect(origin: self.textPlaceholderNode.frame.origin, size: placeholderSize)
}
self.actionButtons.sendButtonLongPressEnabled = peer.id.namespace != Namespaces.Peer.SecretChat && !interfaceState.isScheduledMessages
}
let sendButtonHasApplyIcon = interfaceState.interfaceState.editMessage != nil
if updateSendButtonIcon {
if !self.actionButtons.animatingSendButton {
let imageNode = self.actionButtons.sendButton.imageNode
if transition.isAnimated && !self.actionButtons.sendButton.alpha.isZero && self.actionButtons.sendButton.layer.animation(forKey: "opacity") == nil, let previousImage = imageNode.image {
let tempView = UIImageView(image: previousImage)
self.actionButtons.sendButton.view.addSubview(tempView)
tempView.frame = imageNode.frame
tempView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak tempView] _ in
tempView?.removeFromSuperview()
})
tempView.layer.animateScale(from: 1.0, to: 0.2, duration: 0.2, removeOnCompletion: false)
imageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
imageNode.layer.animateScale(from: 0.2, to: 1.0, duration: 0.2)
}
self.actionButtons.sendButtonHasApplyIcon = sendButtonHasApplyIcon
if self.actionButtons.sendButtonHasApplyIcon {
self.actionButtons.sendButton.setImage(PresentationResourcesChat.chatInputPanelApplyButtonImage(interfaceState.theme), for: [])
} else {
if interfaceState.isScheduledMessages {
self.actionButtons.sendButton.setImage(PresentationResourcesChat.chatInputPanelScheduleButtonImage(interfaceState.theme), for: [])
} else {
self.actionButtons.sendButton.setImage(PresentationResourcesChat.chatInputPanelSendButtonImage(interfaceState.theme), for: [])
}
}
}
}
}
var textFieldMinHeight: CGFloat = 33.0
if let presentationInterfaceState = self.presentationInterfaceState {
textFieldMinHeight = calclulateTextFieldMinHeight(presentationInterfaceState, metrics: metrics)
}
let minimalHeight: CGFloat = 14.0 + textFieldMinHeight
let minimalInputHeight: CGFloat = 2.0 + textFieldMinHeight
var animatedTransition = true
if case .immediate = transition {
animatedTransition = false
}
var updateAccessoryButtons = false
if self.presentationInterfaceState?.inputTextPanelState.accessoryItems.count == self.accessoryItemButtons.count {
for i in 0 ..< interfaceState.inputTextPanelState.accessoryItems.count {
if interfaceState.inputTextPanelState.accessoryItems[i] != self.accessoryItemButtons[i].0 {
updateAccessoryButtons = true
break
}
}
} else {
updateAccessoryButtons = true
}
var removeAccessoryButtons: [AccessoryItemIconButton]?
if updateAccessoryButtons {
var updatedButtons: [(ChatTextInputAccessoryItem, AccessoryItemIconButton)] = []
for item in interfaceState.inputTextPanelState.accessoryItems {
var itemAndButton: (ChatTextInputAccessoryItem, AccessoryItemIconButton)?
for i in 0 ..< self.accessoryItemButtons.count {
if self.accessoryItemButtons[i].0 == item {
itemAndButton = self.accessoryItemButtons[i]
self.accessoryItemButtons.remove(at: i)
break
}
}
if itemAndButton == nil {
let button = AccessoryItemIconButton(item: item, theme: interfaceState.theme, strings: interfaceState.strings)
button.addTarget(self, action: #selector(self.accessoryItemButtonPressed(_:)), for: [.touchUpInside])
itemAndButton = (item, button)
}
updatedButtons.append(itemAndButton!)
}
for (_, button) in self.accessoryItemButtons {
if animatedTransition {
if removeAccessoryButtons == nil {
removeAccessoryButtons = []
}
removeAccessoryButtons!.append(button)
} else {
button.removeFromSuperview()
}
}
self.accessoryItemButtons = updatedButtons
}
let (accessoryButtonsWidth, textFieldHeight) = self.calculateTextFieldMetrics(width: baseWidth, maxHeight: maxHeight, metrics: metrics)
let panelHeight = self.panelHeight(textFieldHeight: textFieldHeight, metrics: metrics)
self.actionButtons.micButton.updateMode(mode: interfaceState.interfaceState.mediaRecordingMode, animated: transition.isAnimated)
var hideMicButton = false
var audioRecordingItemsAlpha: CGFloat = 1
if let mediaRecordingState = interfaceState.inputTextPanelState.mediaRecordingState {
audioRecordingItemsAlpha = 0
let audioRecordingInfoContainerNode: ASDisplayNode
if let currentAudioRecordingInfoContainerNode = self.audioRecordingInfoContainerNode {
audioRecordingInfoContainerNode = currentAudioRecordingInfoContainerNode
} else {
audioRecordingInfoContainerNode = ASDisplayNode()
self.audioRecordingInfoContainerNode = audioRecordingInfoContainerNode
self.insertSubnode(audioRecordingInfoContainerNode, at: 0)
}
var animateTimeSlideIn = false
let audioRecordingTimeNode: ChatTextInputAudioRecordingTimeNode
if let currentAudioRecordingTimeNode = self.audioRecordingTimeNode {
audioRecordingTimeNode = currentAudioRecordingTimeNode
} else {
audioRecordingTimeNode = ChatTextInputAudioRecordingTimeNode(theme: interfaceState.theme)
self.audioRecordingTimeNode = audioRecordingTimeNode
audioRecordingInfoContainerNode.addSubnode(audioRecordingTimeNode)
if transition.isAnimated {
animateTimeSlideIn = true
}
}
var animateCancelSlideIn = false
let audioRecordingCancelIndicator: ChatTextInputAudioRecordingCancelIndicator
if let currentAudioRecordingCancelIndicator = self.audioRecordingCancelIndicator {
audioRecordingCancelIndicator = currentAudioRecordingCancelIndicator
} else {
animateCancelSlideIn = transition.isAnimated
audioRecordingCancelIndicator = ChatTextInputAudioRecordingCancelIndicator(theme: interfaceState.theme, strings: interfaceState.strings, cancel: { [weak self] in
self?.interfaceInteraction?.finishMediaRecording(.dismiss)
})
self.audioRecordingCancelIndicator = audioRecordingCancelIndicator
self.insertSubnode(audioRecordingCancelIndicator, at: 0)
}
let isLocked = mediaRecordingState.isLocked
var hideInfo = false
switch mediaRecordingState {
case let .audio(recorder, _):
self.actionButtons.micButton.audioRecorder = recorder
audioRecordingTimeNode.audioRecorder = recorder
case let .video(status, _):
switch status {
case let .recording(recordingStatus):
audioRecordingTimeNode.videoRecordingStatus = recordingStatus
self.actionButtons.micButton.videoRecordingStatus = recordingStatus
if isLocked {
audioRecordingCancelIndicator.layer.animateAlpha(from: audioRecordingCancelIndicator.alpha, to: 0, duration: 0.15, delay: 0, removeOnCompletion: false)
}
case .editing:
audioRecordingTimeNode.videoRecordingStatus = nil
self.actionButtons.micButton.videoRecordingStatus = nil
hideMicButton = true
hideInfo = true
}
}
transition.updateAlpha(layer: self.textInputBackgroundNode.layer, alpha: 0.0)
if let textInputNode = self.textInputNode {
transition.updateAlpha(node: textInputNode, alpha: 0.0)
}
for (_, button) in self.accessoryItemButtons {
transition.updateAlpha(layer: button.layer, alpha: 0.0)
}
let cancelTransformThreshold: CGFloat = 8.0
let indicatorTranslation = max(0.0, self.actionButtons.micButton.cancelTranslation - cancelTransformThreshold)
audioRecordingCancelIndicator.frame = CGRect(
origin: CGPoint(
x: leftInset + floor((baseWidth - audioRecordingCancelIndicator.bounds.size.width - indicatorTranslation) / 2.0),
y: panelHeight - minimalHeight + floor((minimalHeight - audioRecordingCancelIndicator.bounds.size.height) / 2.0)),
size: audioRecordingCancelIndicator.bounds.size)
if self.actionButtons.micButton.cancelTranslation > cancelTransformThreshold {
let progress = 1 - (self.actionButtons.micButton.cancelTranslation - cancelTransformThreshold) / 80
audioRecordingCancelIndicator.alpha = progress
} else {
audioRecordingCancelIndicator.alpha = 1
}
if animateCancelSlideIn {
let position = audioRecordingCancelIndicator.layer.position
audioRecordingCancelIndicator.layer.animatePosition(from: CGPoint(x: width + audioRecordingCancelIndicator.bounds.size.width, y: position.y), to: position, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
}
audioRecordingCancelIndicator.updateIsDisplayingCancel(isLocked, animated: !animateCancelSlideIn)
if isLocked || self.actionButtons.micButton.cancelTranslation > cancelTransformThreshold {
var deltaOffset: CGFloat = 0.0
if audioRecordingCancelIndicator.layer.animation(forKey: "slide_juggle") != nil, let presentationLayer = audioRecordingCancelIndicator.layer.presentation() {
let translation = CGPoint(x: presentationLayer.transform.m41, y: presentationLayer.transform.m42)
deltaOffset = translation.x
}
audioRecordingCancelIndicator.layer.removeAnimation(forKey: "slide_juggle")
if !deltaOffset.isZero {
audioRecordingCancelIndicator.layer.animatePosition(from: CGPoint(x: deltaOffset, y: 0.0), to: CGPoint(), duration: 0.3, additive: true)
}
} else if audioRecordingCancelIndicator.layer.animation(forKey: "slide_juggle") == nil, baseWidth > 320 {
let slideJuggleAnimation = CABasicAnimation(keyPath: "transform")
slideJuggleAnimation.toValue = CATransform3DMakeTranslation(6, 0, 0)
slideJuggleAnimation.duration = 1
slideJuggleAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
slideJuggleAnimation.autoreverses = true
slideJuggleAnimation.repeatCount = Float.infinity
audioRecordingCancelIndicator.layer.add(slideJuggleAnimation, forKey: "slide_juggle")
}
let audioRecordingTimeSize = audioRecordingTimeNode.measure(CGSize(width: 200.0, height: 100.0))
audioRecordingInfoContainerNode.frame = CGRect(
origin: CGPoint(
x: min(leftInset, width - audioRecordingTimeSize.width - 8.0 - 28.0),
y: 0.0
),
size: CGSize(width: baseWidth, height: panelHeight)
)
audioRecordingTimeNode.frame = CGRect(origin: CGPoint(x: 40.0, y: panelHeight - minimalHeight + floor((minimalHeight - audioRecordingTimeSize.height) / 2.0)), size: audioRecordingTimeSize)
if animateTimeSlideIn {
let position = audioRecordingTimeNode.layer.position
audioRecordingTimeNode.layer.animatePosition(from: CGPoint(x: position.x - 10.0, y: position.y), to: position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
audioRecordingTimeNode.layer.animateAlpha(from: 0, to: 1, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
}
var animateDotAppearing = false
let audioRecordingDotNode: AnimationNode
if let currentAudioRecordingDotNode = self.audioRecordingDotNode, !currentAudioRecordingDotNode.played {
audioRecordingDotNode = currentAudioRecordingDotNode
} else {
self.audioRecordingDotNode?.removeFromSupernode()
audioRecordingDotNode = AnimationNode(animation: "Bin")
self.audioRecordingDotNode = audioRecordingDotNode
self.addSubnode(audioRecordingDotNode)
}
animateDotAppearing = transition.isAnimated && !isLocked && !hideInfo
audioRecordingDotNode.frame = CGRect(origin: CGPoint(x: leftInset + 2.0 - UIScreenPixel, y: panelHeight - 44 + 1), size: CGSize(width: 40.0, height: 40))
if animateDotAppearing {
audioRecordingDotNode.layer.animateScale(from: 0.3, to: 1, duration: 0.15, delay: 0, removeOnCompletion: false)
audioRecordingDotNode.layer.animateAlpha(from: 0, to: 1, duration: 0.15, delay: 0, completion: { [weak audioRecordingDotNode] finished in
if finished {
let animation = CAKeyframeAnimation(keyPath: "opacity")
animation.values = [1.0 as NSNumber, 1.0 as NSNumber, 0.0 as NSNumber]
animation.keyTimes = [0.0 as NSNumber, 0.4546 as NSNumber, 0.9091 as NSNumber, 1 as NSNumber]
animation.duration = 0.5
animation.autoreverses = true
animation.repeatCount = Float.infinity
audioRecordingDotNode?.layer.add(animation, forKey: "recording")
}
})
self.attachmentButton.layer.animateAlpha(from: 1, to: 0, duration: 0.15, delay: 0, removeOnCompletion: false)
self.attachmentButton.layer.animateScale(from: 1, to: 0.3, duration: 0.15, delay: 0, removeOnCompletion: false)
}
if hideInfo {
audioRecordingDotNode.layer.removeAllAnimations()
audioRecordingDotNode.layer.animateAlpha(from: CGFloat(audioRecordingDotNode.layer.presentation()?.opacity ?? 1), to: 0, duration: 0.15, delay: 0, removeOnCompletion: false)
audioRecordingTimeNode.layer.animateAlpha(from: CGFloat(audioRecordingTimeNode.layer.presentation()?.opacity ?? 1), to: 0, duration: 0.15, delay: 0, removeOnCompletion: false)
audioRecordingCancelIndicator.layer.animateAlpha(from: CGFloat(audioRecordingCancelIndicator.layer.presentation()?.opacity ?? 1), to: 0, duration: 0.15, delay: 0, removeOnCompletion: false)
}
} else {
self.actionButtons.micButton.audioRecorder = nil
self.actionButtons.micButton.videoRecordingStatus = nil
transition.updateAlpha(layer: self.textInputBackgroundNode.layer, alpha: 1.0)
if let textInputNode = self.textInputNode {
transition.updateAlpha(node: textInputNode, alpha: 1.0)
}
for (_, button) in self.accessoryItemButtons {
transition.updateAlpha(layer: button.layer, alpha: 1.0)
}
if let audioRecordingInfoContainerNode = self.audioRecordingInfoContainerNode {
self.audioRecordingInfoContainerNode = nil
transition.updateAlpha(node: audioRecordingInfoContainerNode, alpha: 0) { [weak audioRecordingInfoContainerNode] _ in
audioRecordingInfoContainerNode?.removeFromSupernode()
}
}
if let audioRecordingDotNode = self.audioRecordingDotNode {
let dismissDotNode = { [weak audioRecordingDotNode, weak attachmentButton, weak self] in
guard let audioRecordingDotNode = audioRecordingDotNode, audioRecordingDotNode === self?.audioRecordingDotNode else { return }
self?.audioRecordingDotNode = nil
audioRecordingDotNode.layer.animateScale(from: CGFloat(audioRecordingDotNode.layer.presentation()?.opacity ?? 1), to: 0.3, duration: 0.15, delay: 0, removeOnCompletion: false)
audioRecordingDotNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, delay: 0, removeOnCompletion: false) { [weak audioRecordingDotNode] _ in
audioRecordingDotNode?.removeFromSupernode()
}
attachmentButton?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: 0, removeOnCompletion: false)
attachmentButton?.layer.animateScale(from: 0.3, to: 1.0, duration: 0.15, delay: 0, removeOnCompletion: false)
}
audioRecordingDotNode.layer.removeAllAnimations()
if self.isMediaDeleted {
audioRecordingDotNode.completion = dismissDotNode
audioRecordingDotNode.play()
} else {
dismissDotNode()
}
}
if let audioRecordingTimeNode = self.audioRecordingTimeNode {
self.audioRecordingTimeNode = nil
let timePosition = audioRecordingTimeNode.position
transition.updatePosition(node: audioRecordingTimeNode, position: CGPoint(x: timePosition.x - audioRecordingTimeNode.bounds.width / 2.0, y: timePosition.y))
transition.updateTransformScale(node: audioRecordingTimeNode, scale: 0.1)
}
if let audioRecordingCancelIndicator = self.audioRecordingCancelIndicator {
self.audioRecordingCancelIndicator = nil
if transition.isAnimated {
audioRecordingCancelIndicator.layer.animateAlpha(from: audioRecordingCancelIndicator.alpha, to: 0.0, duration: 0.25, completion: { [weak audioRecordingCancelIndicator] _ in
audioRecordingCancelIndicator?.removeFromSupernode()
})
} else {
audioRecordingCancelIndicator.removeFromSupernode()
}
}
}
transition.updateFrame(layer: self.attachmentButton.layer, frame: CGRect(origin: CGPoint(x: leftInset + 2.0 - UIScreenPixel, y: panelHeight - minimalHeight), size: CGSize(width: 40.0, height: minimalHeight)))
transition.updateFrame(node: self.attachmentButtonDisabledNode, frame: self.attachmentButton.frame)
var composeButtonsOffset: CGFloat = 0.0
var textInputBackgroundWidthOffset: CGFloat = 0.0
if self.extendedSearchLayout {
composeButtonsOffset = 44.0
textInputBackgroundWidthOffset = 36.0
}
let actionButtonsFrame = CGRect(origin: CGPoint(x: width - rightInset - 43.0 - UIScreenPixel + composeButtonsOffset, y: panelHeight - minimalHeight), size: CGSize(width: 44.0, height: minimalHeight))
transition.updateFrame(node: self.actionButtons, frame: actionButtonsFrame)
if let presentationInterfaceState = self.presentationInterfaceState {
self.actionButtons.updateLayout(size: CGSize(width: 44.0, height: minimalHeight), transition: transition, interfaceState: presentationInterfaceState)
}
if let _ = interfaceState.inputTextPanelState.mediaRecordingState {
let text: String = "Send"
let mediaRecordingAccessibilityArea: AccessibilityAreaNode
var added = false
if let current = self.mediaRecordingAccessibilityArea {
mediaRecordingAccessibilityArea = current
} else {
added = true
mediaRecordingAccessibilityArea = AccessibilityAreaNode()
mediaRecordingAccessibilityArea.accessibilityLabel = text
mediaRecordingAccessibilityArea.accessibilityTraits = [.button, .startsMediaSession]
self.mediaRecordingAccessibilityArea = mediaRecordingAccessibilityArea
mediaRecordingAccessibilityArea.activate = { [weak self] in
self?.interfaceInteraction?.finishMediaRecording(.send)
return true
}
self.insertSubnode(mediaRecordingAccessibilityArea, aboveSubnode: self.actionButtons)
}
self.actionButtons.isAccessibilityElement = false
let size: CGFloat = 120.0
mediaRecordingAccessibilityArea.frame = CGRect(origin: CGPoint(x: actionButtonsFrame.midX - size / 2.0, y: actionButtonsFrame.midY - size / 2.0), size: CGSize(width: size, height: size))
if added {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.4, execute: {
[weak mediaRecordingAccessibilityArea] in
UIAccessibility.post(notification: UIAccessibility.Notification.layoutChanged, argument: mediaRecordingAccessibilityArea?.view)
})
}
} else {
self.actionButtons.isAccessibilityElement = true
if let mediaRecordingAccessibilityArea = self.mediaRecordingAccessibilityArea {
self.mediaRecordingAccessibilityArea = nil
mediaRecordingAccessibilityArea.removeFromSupernode()
}
}
let searchLayoutClearButtonSize = CGSize(width: 44.0, height: minimalHeight)
let textFieldInsets = self.textFieldInsets(metrics: metrics)
transition.updateFrame(layer: self.searchLayoutClearButton.layer, frame: CGRect(origin: CGPoint(x: width - rightInset - textFieldInsets.left - textFieldInsets.right + textInputBackgroundWidthOffset + 3.0, y: panelHeight - minimalHeight), size: searchLayoutClearButtonSize))
if let image = self.searchLayoutClearImageNode.image {
self.searchLayoutClearImageNode.frame = CGRect(origin: CGPoint(x: floor((searchLayoutClearButtonSize.width - image.size.width) / 2.0), y: floor((searchLayoutClearButtonSize.height - image.size.height) / 2.0)), size: image.size)
}
let textInputFrame = CGRect(x: leftInset + textFieldInsets.left, y: textFieldInsets.top, width: baseWidth - textFieldInsets.left - textFieldInsets.right + textInputBackgroundWidthOffset, height: panelHeight - textFieldInsets.top - textFieldInsets.bottom)
transition.updateFrame(node: self.textInputContainer, frame: textInputFrame)
transition.updateAlpha(node: self.textInputContainer, alpha: audioRecordingItemsAlpha)
if let textInputNode = self.textInputNode {
let textFieldFrame = CGRect(origin: CGPoint(x: self.textInputViewInternalInsets.left, y: self.textInputViewInternalInsets.top), size: CGSize(width: textInputFrame.size.width - (self.textInputViewInternalInsets.left + self.textInputViewInternalInsets.right + accessoryButtonsWidth), height: textInputFrame.size.height - self.textInputViewInternalInsets.top - textInputViewInternalInsets.bottom))
let shouldUpdateLayout = textFieldFrame.size != textInputNode.frame.size
transition.updateFrame(node: textInputNode, frame: textFieldFrame)
if shouldUpdateLayout {
textInputNode.layout()
}
}
if interfaceState.slowmodeState == nil || interfaceState.isScheduledMessages, let contextPlaceholder = interfaceState.inputTextPanelState.contextPlaceholder {
let placeholderLayout = TextNode.asyncLayout(self.contextPlaceholderNode)
let (placeholderSize, placeholderApply) = placeholderLayout(TextNodeLayoutArguments(attributedString: contextPlaceholder, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - leftInset - rightInset - textFieldInsets.left - textFieldInsets.right - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right - accessoryButtonsWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let contextPlaceholderNode = placeholderApply()
if let currentContextPlaceholderNode = self.contextPlaceholderNode, currentContextPlaceholderNode !== contextPlaceholderNode {
self.contextPlaceholderNode = nil
currentContextPlaceholderNode.removeFromSupernode()
}
if self.contextPlaceholderNode !== contextPlaceholderNode {
contextPlaceholderNode.displaysAsynchronously = false
contextPlaceholderNode.isUserInteractionEnabled = false
self.contextPlaceholderNode = contextPlaceholderNode
self.insertSubnode(contextPlaceholderNode, aboveSubnode: self.textPlaceholderNode)
}
let _ = placeholderApply()
contextPlaceholderNode.frame = CGRect(origin: CGPoint(x: leftInset + textFieldInsets.left + self.textInputViewInternalInsets.left, y: textFieldInsets.top + self.textInputViewInternalInsets.top + self.textInputViewRealInsets.top + UIScreenPixel), size: placeholderSize.size)
contextPlaceholderNode.alpha = audioRecordingItemsAlpha
} else if let contextPlaceholderNode = self.contextPlaceholderNode {
self.contextPlaceholderNode = nil
contextPlaceholderNode.removeFromSupernode()
self.textPlaceholderNode.alpha = 1.0
}
if let slowmodeState = interfaceState.slowmodeState, !interfaceState.isScheduledMessages {
let slowmodePlaceholderNode: ChatTextInputSlowmodePlaceholderNode
if let current = self.slowmodePlaceholderNode {
slowmodePlaceholderNode = current
} else {
slowmodePlaceholderNode = ChatTextInputSlowmodePlaceholderNode(theme: interfaceState.theme)
self.slowmodePlaceholderNode = slowmodePlaceholderNode
self.insertSubnode(slowmodePlaceholderNode, aboveSubnode: self.textPlaceholderNode)
}
let placeholderFrame = CGRect(origin: CGPoint(x: leftInset + textFieldInsets.left + self.textInputViewInternalInsets.left, y: textFieldInsets.top + self.textInputViewInternalInsets.top + self.textInputViewRealInsets.top + UIScreenPixel), size: CGSize(width: width - leftInset - rightInset - textFieldInsets.left - textFieldInsets.right - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right - accessoryButtonsWidth, height: 30.0))
slowmodePlaceholderNode.updateState(slowmodeState)
slowmodePlaceholderNode.frame = placeholderFrame
slowmodePlaceholderNode.alpha = audioRecordingItemsAlpha
slowmodePlaceholderNode.updateLayout(size: placeholderFrame.size)
} else if let slowmodePlaceholderNode = self.slowmodePlaceholderNode {
self.slowmodePlaceholderNode = nil
slowmodePlaceholderNode.removeFromSupernode()
}
var inputHasText = false
if let textInputNode = self.textInputNode, let attributedText = textInputNode.attributedText, attributedText.length != 0 {
inputHasText = true
}
if (interfaceState.slowmodeState != nil && !interfaceState.isScheduledMessages && interfaceState.editMessageState == nil) || interfaceState.inputTextPanelState.contextPlaceholder != nil {
self.textPlaceholderNode.isHidden = true
self.slowmodePlaceholderNode?.isHidden = inputHasText
} else {
self.textPlaceholderNode.isHidden = inputHasText
self.slowmodePlaceholderNode?.isHidden = true
}
transition.updateFrame(node: self.textPlaceholderNode, frame: CGRect(origin: CGPoint(x: leftInset + textFieldInsets.left + self.textInputViewInternalInsets.left, y: textFieldInsets.top + self.textInputViewInternalInsets.top + self.textInputViewRealInsets.top + UIScreenPixel), size: self.textPlaceholderNode.frame.size))
transition.updateAlpha(node: self.textPlaceholderNode, alpha: audioRecordingItemsAlpha)
transition.updateFrame(layer: self.textInputBackgroundNode.layer, frame: CGRect(x: leftInset + textFieldInsets.left, y: textFieldInsets.top, width: baseWidth - textFieldInsets.left - textFieldInsets.right + textInputBackgroundWidthOffset, height: panelHeight - textFieldInsets.top - textFieldInsets.bottom))
transition.updateAlpha(node: self.textInputBackgroundNode, alpha: audioRecordingItemsAlpha)
var nextButtonTopRight = CGPoint(x: width - rightInset - textFieldInsets.right - accessoryButtonInset, y: panelHeight - textFieldInsets.bottom - minimalInputHeight)
for (_, button) in self.accessoryItemButtons.reversed() {
let buttonSize = CGSize(width: button.buttonWidth, height: minimalInputHeight)
button.updateLayout(size: buttonSize)
let buttonFrame = CGRect(origin: CGPoint(x: nextButtonTopRight.x - buttonSize.width, y: nextButtonTopRight.y + floor((minimalInputHeight - buttonSize.height) / 2.0)), size: buttonSize)
if button.superview == nil {
self.view.addSubview(button)
button.frame = buttonFrame
transition.updateFrame(layer: button.layer, frame: buttonFrame)
if animatedTransition {
button.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
button.layer.animateScale(from: 0.2, to: 1.0, duration: 0.25)
}
} else {
transition.updateFrame(layer: button.layer, frame: buttonFrame)
}
nextButtonTopRight.x -= buttonSize.width
nextButtonTopRight.x -= accessoryButtonSpacing
}
if let removeAccessoryButtons = removeAccessoryButtons {
for button in removeAccessoryButtons {
let buttonFrame = CGRect(origin: CGPoint(x: button.frame.origin.x, y: panelHeight - textFieldInsets.bottom - minimalInputHeight), size: button.frame.size)
transition.updateFrame(layer: button.layer, frame: buttonFrame)
button.layer.animateScale(from: 1.0, to: 0.2, duration: 0.25, removeOnCompletion: false)
button.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak button] _ in
button?.removeFromSuperview()
})
}
}
var hasText = false
if let textInputNode = self.textInputNode, let attributedText = textInputNode.attributedText, attributedText.length != 0 {
hasText = true
hideMicButton = true
}
if self.extendedSearchLayout {
hideMicButton = true
}
let mediaInputDisabled: Bool
if let channel = interfaceState.renderedPeer?.peer as? TelegramChannel, channel.hasBannedPermission(.banSendMedia) != nil {
mediaInputDisabled = true
} else if let group = interfaceState.renderedPeer?.peer as? TelegramGroup, group.hasBannedPermission(.banSendMedia) {
mediaInputDisabled = true
} else {
mediaInputDisabled = false
}
self.actionButtons.micButton.fadeDisabled = mediaInputDisabled
self.updateActionButtons(hasText: hasText, hideMicButton: hideMicButton, animated: transition.isAnimated)
return panelHeight
}
@objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) {
if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState {
let baseFontSize = max(17.0, presentationInterfaceState.fontSize.baseDisplaySize)
refreshChatTextInputAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize)
refreshChatTextInputTypingAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize)
let inputTextState = self.inputTextState
self.interfaceInteraction?.updateTextInputStateAndMode({ _, inputMode in return (inputTextState, inputMode) })
self.interfaceInteraction?.updateInputLanguage({ _ in return textInputNode.textInputMode.primaryLanguage })
self.updateTextNodeText(animated: true)
}
}
private func updateTextNodeText(animated: Bool) {
var inputHasText = false
var hideMicButton = false
if let textInputNode = self.textInputNode, let attributedText = textInputNode.attributedText, attributedText.length != 0 {
inputHasText = true
hideMicButton = true
}
if let interfaceState = self.presentationInterfaceState {
if (interfaceState.slowmodeState != nil && !interfaceState.isScheduledMessages && interfaceState.editMessageState == nil) || interfaceState.inputTextPanelState.contextPlaceholder != nil {
self.textPlaceholderNode.isHidden = true
self.slowmodePlaceholderNode?.isHidden = inputHasText
} else {
self.textPlaceholderNode.isHidden = inputHasText
self.slowmodePlaceholderNode?.isHidden = true
}
}
self.updateActionButtons(hasText: inputHasText, hideMicButton: hideMicButton, animated: animated)
self.updateTextHeight(animated: animated)
}
private func updateActionButtons(hasText: Bool, hideMicButton: Bool, animated: Bool) {
var hideMicButton = hideMicButton
var mediaInputIsActive = false
if let presentationInterfaceState = self.presentationInterfaceState {
if let mediaRecordingState = presentationInterfaceState.inputTextPanelState.mediaRecordingState {
if case .video(.editing, false) = mediaRecordingState {
hideMicButton = true
}
}
if case .media = presentationInterfaceState.inputMode {
mediaInputIsActive = true
}
}
var animateWithBounce = false
if self.extendedSearchLayout {
hideMicButton = true
if !self.actionButtons.sendButton.alpha.isZero {
self.actionButtons.sendButton.alpha = 0.0
self.actionButtons.sendButtonRadialStatusNode?.alpha = 0.0
self.actionButtons.updateAccessibility()
if animated {
self.actionButtons.animatingSendButton = true
self.actionButtons.sendButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in
if let strongSelf = self {
strongSelf.actionButtons.animatingSendButton = false
strongSelf.applyUpdateSendButtonIcon()
}
})
self.actionButtons.sendButton.layer.animateScale(from: 1.0, to: 0.2, duration: 0.2)
self.actionButtons.sendButtonRadialStatusNode?.layer.animateScale(from: 1.0, to: 0.2, duration: 0.2)
self.actionButtons.sendButtonRadialStatusNode?.alpha = 0.0
self.actionButtons.sendButtonRadialStatusNode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
}
}
if self.searchLayoutClearButton.alpha.isZero {
self.searchLayoutClearButton.alpha = 1.0
if animated {
self.searchLayoutClearButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
self.searchLayoutClearButton.layer.animateScale(from: 0.8, to: 1.0, duration: 0.2)
}
}
} else {
animateWithBounce = true
if !self.searchLayoutClearButton.alpha.isZero {
animateWithBounce = false
self.searchLayoutClearButton.alpha = 0.0
if animated {
self.searchLayoutClearButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
self.searchLayoutClearButton.layer.animateScale(from: 1.0, to: 0.8, duration: 0.2)
}
}
if (hasText || self.keepSendButtonEnabled && !mediaInputIsActive) {
hideMicButton = true
if self.actionButtons.sendButton.alpha.isZero {
self.actionButtons.sendButton.alpha = 1.0
self.actionButtons.sendButtonRadialStatusNode?.alpha = 1.0
self.actionButtons.updateAccessibility()
if animated {
self.actionButtons.sendButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
self.actionButtons.sendButtonRadialStatusNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
if animateWithBounce {
self.actionButtons.sendButton.layer.animateSpring(from: NSNumber(value: Float(0.1)), to: NSNumber(value: Float(1.0)), keyPath: "transform.scale", duration: 0.6)
self.actionButtons.sendButtonRadialStatusNode?.layer.animateSpring(from: NSNumber(value: Float(0.1)), to: NSNumber(value: Float(1.0)), keyPath: "transform.scale", duration: 0.6)
} else {
self.actionButtons.sendButton.layer.animateScale(from: 0.2, to: 1.0, duration: 0.25)
self.actionButtons.sendButtonRadialStatusNode?.layer.animateScale(from: 0.2, to: 1.0, duration: 0.25)
}
}
}
} else {
if !self.actionButtons.sendButton.alpha.isZero {
self.actionButtons.sendButton.alpha = 0.0
self.actionButtons.sendButtonRadialStatusNode?.alpha = 0.0
self.actionButtons.updateAccessibility()
if animated {
self.actionButtons.animatingSendButton = true
self.actionButtons.sendButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in
if let strongSelf = self {
strongSelf.actionButtons.animatingSendButton = false
strongSelf.applyUpdateSendButtonIcon()
}
})
self.actionButtons.sendButtonRadialStatusNode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
}
}
}
}
if mediaInputIsActive {
hideMicButton = true
}
if hideMicButton {
if !self.actionButtons.micButton.alpha.isZero {
self.actionButtons.micButton.alpha = 0.0
if animated {
self.actionButtons.micButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
}
}
} else {
let micAlpha: CGFloat = self.actionButtons.micButton.fadeDisabled ? 0.5 : 1.0
if !self.actionButtons.micButton.alpha.isEqual(to: micAlpha) {
self.actionButtons.micButton.alpha = micAlpha
if animated {
self.actionButtons.micButton.layer.animateAlpha(from: 0.0, to: micAlpha, duration: 0.1)
if animateWithBounce {
self.actionButtons.micButton.layer.animateSpring(from: NSNumber(value: Float(0.1)), to: NSNumber(value: Float(1.0)), keyPath: "transform.scale", duration: 0.6)
} else {
self.actionButtons.micButton.layer.animateScale(from: 0.2, to: 1.0, duration: 0.25)
}
}
}
}
if mediaInputIsActive {
if self.actionButtons.expandMediaInputButton.alpha.isZero {
self.actionButtons.expandMediaInputButton.alpha = 1.0
if animated {
self.actionButtons.expandMediaInputButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
if animateWithBounce {
self.actionButtons.expandMediaInputButton.layer.animateSpring(from: NSNumber(value: Float(0.1)), to: NSNumber(value: Float(1.0)), keyPath: "transform.scale", duration: 0.6)
} else {
self.actionButtons.expandMediaInputButton.layer.animateScale(from: 0.2, to: 1.0, duration: 0.25)
}
}
}
} else {
if !self.actionButtons.expandMediaInputButton.alpha.isZero {
self.actionButtons.expandMediaInputButton.alpha = 0.0
if animated {
self.actionButtons.expandMediaInputButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
}
}
}
self.actionButtons.updateAccessibility()
}
private func updateTextHeight(animated: Bool) {
if let (width, leftInset, rightInset, maxHeight, metrics, _) = self.validLayout {
let (_, textFieldHeight) = self.calculateTextFieldMetrics(width: width - leftInset - rightInset, maxHeight: maxHeight, metrics: metrics)
let panelHeight = self.panelHeight(textFieldHeight: textFieldHeight, metrics: metrics)
if !self.bounds.size.height.isEqual(to: panelHeight) {
self.updateHeight(animated)
}
}
}
func updateIsProcessingInlineRequest(_ value: Bool) {
if value {
if self.searchActivityIndicator == nil, let currentState = self.presentationInterfaceState {
let searchActivityIndicator = ActivityIndicator(type: .custom(currentState.theme.list.itemAccentColor, 20.0, 1.5, true))
searchActivityIndicator.isUserInteractionEnabled = false
self.searchActivityIndicator = searchActivityIndicator
let indicatorSize = searchActivityIndicator.measure(CGSize(width: 100.0, height: 100.0))
let size = self.searchLayoutClearButton.bounds.size
searchActivityIndicator.frame = CGRect(origin: CGPoint(x: floor((size.width - indicatorSize.width) / 2.0) + 0.0, y: floor((size.height - indicatorSize.height) / 2.0) - 0.0), size: indicatorSize)
//self.searchLayoutClearImageNode.isHidden = true
self.searchLayoutClearButton.addSubnode(searchActivityIndicator)
}
} else if let searchActivityIndicator = self.searchActivityIndicator {
self.searchActivityIndicator = nil
//self.searchLayoutClearImageNode.isHidden = false
searchActivityIndicator.removeFromSupernode()
}
}
@objc func editableTextNodeShouldReturn(_ editableTextNode: ASEditableTextNode) -> Bool {
if self.actionButtons.sendButton.supernode != nil && !self.actionButtons.sendButton.isHidden && !self.actionButtons.sendButton.alpha.isZero {
self.sendButtonPressed()
}
return false
}
private func applyUpdateSendButtonIcon() {
if let interfaceState = self.presentationInterfaceState {
let sendButtonHasApplyIcon = interfaceState.interfaceState.editMessage != nil
if sendButtonHasApplyIcon != self.actionButtons.sendButtonHasApplyIcon {
self.actionButtons.sendButtonHasApplyIcon = sendButtonHasApplyIcon
if self.actionButtons.sendButtonHasApplyIcon {
self.actionButtons.sendButton.setImage(PresentationResourcesChat.chatInputPanelApplyButtonImage(interfaceState.theme), for: [])
} else {
if interfaceState.isScheduledMessages {
self.actionButtons.sendButton.setImage(PresentationResourcesChat.chatInputPanelScheduleButtonImage(interfaceState.theme), for: [])
} else {
self.actionButtons.sendButton.setImage(PresentationResourcesChat.chatInputPanelSendButtonImage(interfaceState.theme), for: [])
}
}
}
}
}
@objc func editableTextNodeDidChangeSelection(_ editableTextNode: ASEditableTextNode, fromSelectedRange: NSRange, toSelectedRange: NSRange, dueToEditing: Bool) {
if !dueToEditing && !self.updatingInputState {
let inputTextState = self.inputTextState
self.interfaceInteraction?.updateTextInputStateAndMode({ _, inputMode in return (inputTextState, inputMode) })
}
if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState {
if case .format = self.inputMenu.state {
self.inputMenu.deactivate()
UIMenuController.shared.update()
}
let baseFontSize = max(17.0, presentationInterfaceState.fontSize.baseDisplaySize)
refreshChatTextInputTypingAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize)
}
}
@objc func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) {
self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in
return (.text, state.keyboardButtonsMessage?.id)
})
self.inputMenu.activate()
}
func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) {
self.storedInputLanguage = editableTextNode.textInputMode.primaryLanguage
self.inputMenu.deactivate()
}
func editableTextNodeTarget(forAction action: Selector) -> ASEditableTextNodeTargetForAction? {
if action == Selector(("_showTextStyleOptions:")) {
if case .general = self.inputMenu.state {
if let textInputNode = self.textInputNode, textInputNode.attributedText == nil || textInputNode.attributedText!.length == 0 || textInputNode.selectedRange.length == 0 {
return ASEditableTextNodeTargetForAction(target: nil)
}
return ASEditableTextNodeTargetForAction(target: self)
} else {
return ASEditableTextNodeTargetForAction(target: nil)
}
} else if action == #selector(self.formatAttributesBold(_:)) || action == #selector(self.formatAttributesItalic(_:)) || action == #selector(self.formatAttributesMonospace(_:)) || action == #selector(self.formatAttributesLink(_:)) || action == #selector(self.formatAttributesStrikethrough(_:)) || action == #selector(self.formatAttributesUnderline(_:)) {
if case .format = self.inputMenu.state {
return ASEditableTextNodeTargetForAction(target: self)
} else {
return ASEditableTextNodeTargetForAction(target: nil)
}
}
if case .format = self.inputMenu.state {
return ASEditableTextNodeTargetForAction(target: nil)
}
return nil
}
@objc func _showTextStyleOptions(_ sender: Any) {
if let textInputNode = self.textInputNode {
self.inputMenu.format(view: textInputNode.view, rect: textInputNode.selectionRect.offsetBy(dx: 0.0, dy: -textInputNode.textView.contentOffset.y).insetBy(dx: 0.0, dy: -1.0))
}
}
@objc func formatAttributesBold(_ sender: Any) {
self.inputMenu.back()
self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.bold), inputMode)
}
}
@objc func formatAttributesItalic(_ sender: Any) {
self.inputMenu.back()
self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.italic), inputMode)
}
}
@objc func formatAttributesMonospace(_ sender: Any) {
self.inputMenu.back()
self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.monospace), inputMode)
}
}
@objc func formatAttributesLink(_ sender: Any) {
self.inputMenu.back()
self.interfaceInteraction?.openLinkEditing()
}
@objc func formatAttributesStrikethrough(_ sender: Any) {
self.inputMenu.back()
self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.strikethrough), inputMode)
}
}
@objc func formatAttributesUnderline(_ sender: Any) {
self.inputMenu.back()
self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.underline), inputMode)
}
}
@objc func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
self.updateActivity()
var cleanText = text
let removeSequences: [String] = ["\u{202d}", "\u{202c}"]
for sequence in removeSequences {
inner: while true {
if let range = cleanText.range(of: sequence) {
cleanText.removeSubrange(range)
} else {
break inner
}
}
}
let newText = (editableTextNode.textView.text as NSString).replacingCharacters(in: range, with: cleanText)
if let interfaceState = self.presentationInterfaceState, let editMessage = interfaceState.interfaceState.editMessage, let inputTextMaxLength = editMessage.inputTextMaxLength {
if newText.count > inputTextMaxLength {
return false
}
}
if cleanText != text {
let string = NSMutableAttributedString(attributedString: editableTextNode.attributedText ?? NSAttributedString())
var textColor: UIColor = .black
var accentTextColor: UIColor = .blue
var baseFontSize: CGFloat = 17.0
if let presentationInterfaceState = self.presentationInterfaceState {
textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor
accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor
baseFontSize = max(17.0, presentationInterfaceState.fontSize.baseDisplaySize)
}
let cleanReplacementString = textAttributedStringForStateText(NSAttributedString(string: cleanText), fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil)
string.replaceCharacters(in: range, with: cleanReplacementString)
self.textInputNode?.attributedText = string
self.textInputNode?.selectedRange = NSMakeRange(range.lowerBound + cleanReplacementString.length, 0)
self.updateTextNodeText(animated: true)
return false
}
return true
}
@objc func editableTextNodeShouldCopy(_ editableTextNode: ASEditableTextNode) -> Bool {
self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
storeInputTextInPasteboard(current.inputText.attributedSubstring(from: NSMakeRange(current.selectionRange.lowerBound, current.selectionRange.count)))
return (current, inputMode)
}
return false
}
@objc func editableTextNodeShouldPaste(_ editableTextNode: ASEditableTextNode) -> Bool {
let pasteboard = UIPasteboard.general
var attributedString: NSAttributedString?
if let data = pasteboard.data(forPasteboardType: kUTTypeRTF as String) {
attributedString = chatInputStateStringFromRTF(data, type: NSAttributedString.DocumentType.rtf)
} else if let data = pasteboard.data(forPasteboardType: "com.apple.flat-rtfd") {
attributedString = chatInputStateStringFromRTF(data, type: NSAttributedString.DocumentType.rtfd)
}
if let attributedString = attributedString {
self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
if let inputText = current.inputText.mutableCopy() as? NSMutableAttributedString {
inputText.replaceCharacters(in: NSMakeRange(current.selectionRange.lowerBound, current.selectionRange.count), with: attributedString)
let updatedRange = current.selectionRange.lowerBound + attributedString.length
return (ChatTextInputState(inputText: inputText, selectionRange: updatedRange ..< updatedRange), inputMode)
} else {
return (ChatTextInputState(inputText: attributedString), inputMode)
}
}
return false
}
var images: [UIImage] = []
if let data = pasteboard.data(forPasteboardType: "com.compuserve.gif") {
self.paste(.gif(data))
return false
} else {
var isPNG = false
var isMemoji = false
for item in pasteboard.items {
if let image = item["com.apple.png-sticker"] as? UIImage {
images.append(image)
isPNG = true
isMemoji = true
} else if let image = item[kUTTypePNG as String] as? UIImage {
images.append(image)
isPNG = true
} else if let image = item["com.apple.uikit.image"] as? UIImage {
images.append(image)
isPNG = true
} else if let image = item[kUTTypeJPEG as String] as? UIImage {
images.append(image)
} else if let image = item[kUTTypeGIF as String] as? UIImage {
images.append(image)
}
}
if isPNG && images.count == 1, let image = images.first, let cgImage = image.cgImage {
let maxSide = max(image.size.width, image.size.height)
if maxSide.isZero {
return false
}
let aspectRatio = min(image.size.width, image.size.height) / maxSide
if isMemoji || (imageHasTransparency(cgImage) && aspectRatio > 0.85) {
self.paste(.sticker(image, isMemoji))
return true
}
}
if !images.isEmpty {
self.paste(.images(images))
return false
}
}
return true
}
@objc func sendButtonPressed() {
self.sendMessage()
}
@objc func attachmentButtonPressed() {
self.displayAttachmentMenu()
}
@objc func searchLayoutClearButtonPressed() {
if let interfaceInteraction = self.interfaceInteraction {
interfaceInteraction.updateTextInputStateAndMode { textInputState, inputMode in
var mentionQueryRange: NSRange?
inner: for (_, type, queryRange) in textInputStateContextQueryRangeAndType(textInputState) {
if type == [.contextRequest] {
mentionQueryRange = queryRange
break inner
}
}
if let mentionQueryRange = mentionQueryRange, mentionQueryRange.length > 0 {
let inputText = NSMutableAttributedString(attributedString: textInputState.inputText)
let rangeLower = mentionQueryRange.lowerBound
let rangeUpper = mentionQueryRange.upperBound
inputText.replaceCharacters(in: NSRange(location: rangeLower, length: rangeUpper - rangeLower), with: "")
return (ChatTextInputState(inputText: inputText), inputMode)
} else {
return (ChatTextInputState(inputText: NSAttributedString(string: "")), inputMode)
}
}
}
}
@objc func textInputBackgroundViewTap(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.ensureFocused()
}
}
var isFocused: Bool {
return self.textInputNode?.isFirstResponder() ?? false
}
func ensureUnfocused() {
self.textInputNode?.resignFirstResponder()
}
func ensureFocused() {
if self.textInputNode == nil {
self.loadTextInputNode()
}
self.textInputNode?.becomeFirstResponder()
}
@objc func expandButtonPressed() {
self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in
if case let .media(mode, expanded) = state.inputMode {
if let _ = expanded {
return (.media(mode: mode, expanded: nil), state.interfaceState.messageActionsState.closedButtonKeyboardMessageId)
} else {
return (.media(mode: mode, expanded: .content), state.interfaceState.messageActionsState.closedButtonKeyboardMessageId)
}
} else {
return (state.inputMode, state.interfaceState.messageActionsState.closedButtonKeyboardMessageId)
}
})
}
@objc func accessoryItemButtonPressed(_ button: UIView) {
for (item, currentButton) in self.accessoryItemButtons {
if currentButton === button {
switch item {
case let .stickers(enabled):
if enabled {
self.interfaceInteraction?.openStickers()
} else {
self.interfaceInteraction?.displayRestrictedInfo(.stickers, .tooltip)
}
case .keyboard:
self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in
return (.text, state.keyboardButtonsMessage?.id)
})
case .inputButtons:
self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in
return (.inputButtons, nil)
})
case .commands:
self.interfaceInteraction?.updateTextInputStateAndMode { _, inputMode in
return (ChatTextInputState(inputText: NSAttributedString(string: "/")), .text)
}
case .silentPost:
self.interfaceInteraction?.toggleSilentPost()
case .messageAutoremoveTimeout:
self.interfaceInteraction?.setupMessageAutoremoveTimeout()
case .scheduledMessages:
self.interfaceInteraction?.openScheduledMessages()
}
break
}
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let audioRecordingCancelIndicator = self.audioRecordingCancelIndicator {
if let result = audioRecordingCancelIndicator.hitTest(point.offsetBy(dx: -audioRecordingCancelIndicator.frame.minX, dy: -audioRecordingCancelIndicator.frame.minY), with: event) {
return result
}
}
let result = super.hitTest(point, with: event)
return result
}
func frameForAccessoryButton(_ item: ChatTextInputAccessoryItem) -> CGRect? {
for (buttonItem, buttonNode) in self.accessoryItemButtons {
if buttonItem == item {
return buttonNode.frame
}
}
return nil
}
func frameForAttachmentButton() -> CGRect? {
if !self.attachmentButton.alpha.isZero {
return self.attachmentButton.frame.insetBy(dx: 0.0, dy: 6.0).offsetBy(dx: 2.0, dy: 0.0)
}
return nil
}
func frameForInputActionButton() -> CGRect? {
if !self.actionButtons.alpha.isZero {
if self.actionButtons.micButton.alpha.isZero {
return self.actionButtons.frame.insetBy(dx: 0.0, dy: 6.0).offsetBy(dx: 4.0, dy: 0.0)
} else {
return self.actionButtons.frame.insetBy(dx: 0.0, dy: 6.0).offsetBy(dx: 2.0, dy: 0.0)
}
}
return nil
}
func frameForStickersButton() -> CGRect? {
for (item, button) in self.accessoryItemButtons {
if case .stickers = item {
return button.frame.insetBy(dx: 0.0, dy: 6.0)
}
}
return nil
}
}