Swiftgram/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift
Ilya Laktyushin e5762bd9c8 Various fixes
2025-05-18 04:35:56 +04:00

5039 lines
264 KiB
Swift

import Foundation
import UniformTypeIdentifiers
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
import MobileCoreServices
import TelegramPresentationData
import TextFormat
import AccountContext
import TouchDownGesture
import ImageTransparency
import ActivityIndicator
import AnimationUI
import Speak
import ObjCRuntimeUtils
import AvatarNode
import ContextUI
import InvisibleInkDustNode
import TextInputMenu
import Pasteboard
import ChatPresentationInterfaceState
import ManagedAnimationNode
import AttachmentUI
import EditableChatTextNode
import EmojiTextAttachmentView
import LottieAnimationComponent
import ComponentFlow
import EmojiSuggestionsComponent
import AudioToolbox
import ChatControllerInteraction
import UndoUI
import PremiumUI
import StickerPeekUI
import LottieComponent
import SolidRoundedButtonNode
import TooltipUI
import ChatTextInputMediaRecordingButton
import ChatContextQuery
import ChatInputTextNode
import ChatInputPanelNode
import TelegramNotices
import AnimatedCountLabelNode
import TelegramStringFormatting
import TextNodeWithEntities
import DeviceModel
private let accessoryButtonFont = Font.medium(14.0)
private let counterFont = Font.with(size: 14.0, design: .regular, traits: [.monospacedNumbers])
private final class AccessoryItemIconButtonNode: HighlightTrackingButtonNode {
private var item: ChatTextInputAccessoryItem
private var theme: PresentationTheme
private var strings: PresentationStrings
private var width: CGFloat
private let iconImageNode: ASImageNode
private var animationView: ComponentView<Empty>?
private var imageEdgeInsets = UIEdgeInsets()
init(item: ChatTextInputAccessoryItem, theme: PresentationTheme, strings: PresentationStrings) {
self.item = item
self.theme = theme
self.strings = strings
self.iconImageNode = ASImageNode()
let (image, text, accessibilityLabel, alpha, insets) = AccessoryItemIconButtonNode.imageAndInsets(item: item, theme: theme, strings: strings)
self.width = AccessoryItemIconButtonNode.calculateWidth(item: item, image: image, text: text, strings: strings)
super.init(pointerStyle: .circle(30.0))
self.isAccessibilityElement = true
self.accessibilityTraits = [.button]
self.iconImageNode.isUserInteractionEnabled = false
self.addSubnode(self.iconImageNode)
switch item {
case .input, .botInput, .silentPost:
self.iconImageNode.isHidden = true
self.animationView = ComponentView<Empty>()
default:
break
}
if let text = text {
self.setAttributedTitle(NSAttributedString(string: text, font: accessoryButtonFont, textColor: theme.chat.inputPanel.inputControlColor), for: .normal)
} else {
self.setAttributedTitle(NSAttributedString(), for: .normal)
}
self.iconImageNode.image = image
self.iconImageNode.alpha = alpha
self.imageEdgeInsets = insets
self.accessibilityLabel = accessibilityLabel
self.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.layer.removeAnimation(forKey: "opacity")
strongSelf.alpha = 0.4
strongSelf.layer.allowsGroupOpacity = true
} else {
strongSelf.alpha = 1.0
strongSelf.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
strongSelf.layer.allowsGroupOpacity = false
}
}
}
}
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
self.theme = theme
self.strings = strings
let (image, text, accessibilityLabel, alpha, insets) = AccessoryItemIconButtonNode.imageAndInsets(item: item, theme: theme, strings: strings)
self.width = AccessoryItemIconButtonNode.calculateWidth(item: item, image: image, text: text, strings: strings)
if let text = text {
self.setAttributedTitle(NSAttributedString(string: text, font: accessoryButtonFont, textColor: theme.chat.inputPanel.inputControlColor), for: .normal)
} else {
self.setAttributedTitle(NSAttributedString(), for: .normal)
}
self.iconImageNode.image = image
self.imageEdgeInsets = insets
self.iconImageNode.alpha = alpha
self.accessibilityLabel = accessibilityLabel
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private static func imageAndInsets(item: ChatTextInputAccessoryItem, theme: PresentationTheme, strings: PresentationStrings) -> (UIImage?, String?, String, CGFloat, UIEdgeInsets) {
switch item {
case let .input(isEnabled, inputMode), let .botInput(isEnabled, inputMode):
switch inputMode {
case .keyboard:
return (PresentationResourcesChat.chatInputTextFieldKeyboardImage(theme), nil, strings.VoiceOver_Keyboard, 1.0, UIEdgeInsets())
case .stickers, .emoji:
return (PresentationResourcesChat.chatInputTextFieldStickersImage(theme), nil, strings.VoiceOver_Stickers, isEnabled ? 1.0 : 0.4, UIEdgeInsets())
case .bot:
return (PresentationResourcesChat.chatInputTextFieldInputButtonsImage(theme), nil, strings.VoiceOver_BotKeyboard, 1.0, UIEdgeInsets())
}
case .commands:
return (PresentationResourcesChat.chatInputTextFieldCommandsImage(theme), nil, strings.VoiceOver_BotCommands, 1.0, UIEdgeInsets())
case let .silentPost(value):
if value {
return (PresentationResourcesChat.chatInputTextFieldSilentPostOnImage(theme), nil, strings.VoiceOver_SilentPostOn, 1.0, UIEdgeInsets())
} else {
return (PresentationResourcesChat.chatInputTextFieldSilentPostOffImage(theme), nil, strings.VoiceOver_SilentPostOff, 1.0, UIEdgeInsets())
}
case let .messageAutoremoveTimeout(timeout):
if let timeout = timeout {
return (nil, shortTimeIntervalString(strings: strings, value: timeout), strings.VoiceOver_SelfDestructTimerOn(timeIntervalString(strings: strings, value: timeout)).string, 1.0, UIEdgeInsets())
} else {
return (PresentationResourcesChat.chatInputTextFieldTimerImage(theme), nil, strings.VoiceOver_SelfDestructTimerOff, 1.0, UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0))
}
case .scheduledMessages:
return (PresentationResourcesChat.chatInputTextFieldScheduleImage(theme), nil, strings.VoiceOver_ScheduledMessages, 1.0, UIEdgeInsets())
case .gift:
return (PresentationResourcesChat.chatInputTextFieldGiftImage(theme), nil, strings.VoiceOver_GiftPremium, 1.0, UIEdgeInsets())
}
}
private static func calculateWidth(item: ChatTextInputAccessoryItem, image: UIImage?, text: String?, strings: PresentationStrings) -> CGFloat {
switch item {
case .input, .botInput, .silentPost, .commands, .scheduledMessages, .gift:
return 32.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(item: ChatTextInputAccessoryItem, size: CGSize) {
let previousItem = self.item
self.item = item
let (updatedImage, text, _, _, _) = AccessoryItemIconButtonNode.imageAndInsets(item: item, theme: self.theme, strings: self.strings)
if let image = self.iconImageNode.image {
self.iconImageNode.image = updatedImage
let bottomInset: CGFloat = 0.0
let imageFrame = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0) - bottomInset), size: image.size)
self.iconImageNode.frame = imageFrame
if let animationView = self.animationView {
let width = AccessoryItemIconButtonNode.calculateWidth(item: item, image: image, text: "", strings: self.strings)
//let iconSize = CGSize(width: width, height: width)
let animationFrame = CGRect(origin: CGPoint(x: floor((size.width - width) / 2.0), y: floor((size.height - width) / 2.0) - bottomInset), size: CGSize(width: width, height: width))
//let colorKeys: [String] = ["__allcolors__"]
let animationName: String
var animationMode: LottieAnimationComponent.AnimationItem.Mode = .still(position: .end)
if case let .silentPost(muted) = item {
if case let .silentPost(previousMuted) = previousItem {
if muted {
animationName = "input_anim_channelMute"
} else {
animationName = "input_anim_channelUnmute"
}
if muted != previousMuted {
animationMode = .animating(loop: false)
}
} else {
animationName = "input_anim_channelMute"
}
} else {
var previousInputMode: ChatTextInputAccessoryItem.InputMode?
var inputMode: ChatTextInputAccessoryItem.InputMode?
switch previousItem {
case let .input(_, itemInputMode), let .botInput(_, itemInputMode):
previousInputMode = itemInputMode
default:
break
}
switch item {
case let .input(_, itemInputMode), let .botInput(_, itemInputMode):
inputMode = itemInputMode
default:
break
}
if let inputMode = inputMode {
switch inputMode {
case .keyboard:
if let previousInputMode = previousInputMode {
if case .stickers = previousInputMode {
animationName = "input_anim_stickerToKey"
animationMode = .animating(loop: false)
} else if case .emoji = previousInputMode {
animationName = "input_anim_smileToKey"
animationMode = .animating(loop: false)
} else if case .bot = previousInputMode {
animationName = "input_anim_botToKey"
animationMode = .animating(loop: false)
} else {
animationName = "input_anim_stickerToKey"
}
} else {
animationName = "input_anim_stickerToKey"
}
case .stickers:
if let previousInputMode = previousInputMode {
if case .keyboard = previousInputMode {
animationName = "input_anim_keyToSticker"
animationMode = .animating(loop: false)
} else if case .emoji = previousInputMode {
animationName = "input_anim_smileToSticker"
animationMode = .animating(loop: false)
} else {
animationName = "input_anim_keyToSticker"
}
} else {
animationName = "input_anim_keyToSticker"
}
case .emoji:
if let previousInputMode = previousInputMode {
if case .keyboard = previousInputMode {
animationName = "input_anim_keyToSmile"
animationMode = .animating(loop: false)
} else if case .stickers = previousInputMode {
animationName = "input_anim_stickerToSmile"
animationMode = .animating(loop: false)
} else {
animationName = "input_anim_keyToSmile"
}
} else {
animationName = "input_anim_keyToSmile"
}
case .bot:
if let previousInputMode = previousInputMode {
if case .keyboard = previousInputMode {
animationName = "input_anim_keyToBot"
animationMode = .animating(loop: false)
} else {
animationName = "input_anim_keyToBot"
}
} else {
animationName = "input_anim_keyToBot"
}
}
} else {
animationName = ""
}
}
/*var colors: [String: UIColor] = [:]
for colorKey in colorKeys {
colors[colorKey] = self.theme.chat.inputPanel.inputControlColor.blitOver(self.theme.chat.inputPanel.inputBackgroundColor, alpha: 1.0)
}*/
let animationSize = animationView.update(
transition: .immediate,
component: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(name: animationName),
color: self.theme.chat.inputPanel.inputControlColor.blitOver(self.theme.chat.inputPanel.inputBackgroundColor, alpha: 1.0)
)),
environment: {},
containerSize: animationFrame.size
)
if let view = animationView.view as? LottieComponent.View {
view.isUserInteractionEnabled = false
if view.superview == nil {
self.view.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: animationFrame.minX + floor((animationFrame.width - animationSize.width) / 2.0), y: animationFrame.minY + floor((animationFrame.height - animationSize.height) / 2.0)), size: animationSize)
if case .animating = animationMode {
view.playOnce()
}
}
}
}
if let text = text {
self.setAttributedTitle(NSAttributedString(string: text, font: accessoryButtonFont, textColor: self.theme.chat.inputPanel.inputControlColor), for: .normal)
} else {
self.setAttributedTitle(NSAttributedString(), for: .normal)
}
}
var buttonWidth: CGFloat {
return self.width
}
}
let chatTextInputMinFontSize: CGFloat = 5.0
private let minInputFontSize = chatTextInputMinFontSize
private func calclulateTextFieldMinHeight(_ presentationInterfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat {
let baseFontSize = max(minInputFontSize, 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 func calculateTextFieldRealInsets(presentationInterfaceState: ChatPresentationInterfaceState, accessoryButtonsWidth: CGFloat) -> UIEdgeInsets {
let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize)
let top: CGFloat
let bottom: CGFloat
if baseFontSize.isEqual(to: 14.0) {
top = 2.0
bottom = 1.0
} else if baseFontSize.isEqual(to: 15.0) {
top = 1.0
bottom = 1.0
} else if baseFontSize.isEqual(to: 16.0) {
top = 0.5
bottom = 0.0
} else {
top = 0.0
bottom = 0.0
}
var right: CGFloat = 0.0
right += max(0.0, accessoryButtonsWidth - 14.0)
return UIEdgeInsets(top: 4.5 + top, left: 0.0, bottom: 5.5 + bottom, right: right)
}
private var currentTextInputBackgroundImage: (UIColor, UIColor, CGFloat, CGFloat, UIImage)?
private func textInputBackgroundImage(backgroundColor: UIColor?, inputBackgroundColor: UIColor?, strokeColor: UIColor, diameter: CGFloat, strokeWidth: CGFloat) -> UIImage? {
if let backgroundColor = backgroundColor, let current = currentTextInputBackgroundImage {
if current.0.isEqual(backgroundColor) && current.1.isEqual(strokeColor) && current.2.isEqual(to: diameter) && current.3.isEqual(to: strokeWidth) {
return current.4
}
}
let image = generateImage(CGSize(width: diameter, height: diameter), rotatedContext: { size, context in
context.clear(CGRect(x: 0.0, y: 0.0, width: diameter, height: diameter))
if let inputBackgroundColor = inputBackgroundColor {
context.setBlendMode(.normal)
context.setFillColor(inputBackgroundColor.cgColor)
} else {
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)
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 {
if let backgroundColor = backgroundColor {
currentTextInputBackgroundImage = (backgroundColor, strokeColor, diameter, strokeWidth, image)
}
return image
} else {
return nil
}
}
enum ChatTextInputPanelPasteData {
case images([UIImage])
case video(Data)
case gif(Data)
case sticker(UIImage, Bool)
case animatedSticker(Data)
}
final class ChatTextViewForOverlayContent: UIView, ChatInputPanelViewForOverlayContent {
let ignoreHit: (UIView, CGPoint) -> Bool
let dismissSuggestions: () -> Void
init(ignoreHit: @escaping (UIView, CGPoint) -> Bool, dismissSuggestions: @escaping () -> Void) {
self.ignoreHit = ignoreHit
self.dismissSuggestions = dismissSuggestions
super.init(frame: CGRect())
}
required init(coder: NSCoder) {
preconditionFailure()
}
func maybeDismissContent(point: CGPoint) {
for subview in self.subviews.reversed() {
if let _ = subview.hitTest(self.convert(point, to: subview), with: nil) {
return
}
}
self.dismissSuggestions()
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
for subview in self.subviews.reversed() {
if let result = subview.hitTest(self.convert(point, to: subview), with: event) {
return result
}
}
if event == nil || self.ignoreHit(self, point) {
return nil
}
self.dismissSuggestions()
return nil
}
}
private func makeTextInputTheme(context: AccountContext, interfaceState: ChatPresentationInterfaceState) -> ChatInputTextView.Theme {
let lineStyle: ChatInputTextView.Theme.Quote.LineStyle
let authorNameColor: UIColor
if let peer = interfaceState.renderedPeer?.peer as? TelegramChannel, case .broadcast = peer.info, let nameColor = peer.nameColor {
let colors = context.peerNameColors.get(nameColor)
authorNameColor = colors.main
if let secondary = colors.secondary, let tertiary = colors.tertiary {
lineStyle = .tripleDashed(mainColor: colors.main, secondaryColor: secondary, tertiaryColor: tertiary)
} else if let secondary = colors.secondary {
lineStyle = .doubleDashed(mainColor: colors.main, secondaryColor: secondary)
} else {
lineStyle = .solid(color: colors.main)
}
} else if let accountPeerColor = interfaceState.accountPeerColor {
authorNameColor = interfaceState.theme.list.itemAccentColor
switch accountPeerColor.style {
case .solid:
lineStyle = .solid(color: authorNameColor)
case .doubleDashed:
lineStyle = .doubleDashed(mainColor: authorNameColor, secondaryColor: .clear)
case .tripleDashed:
lineStyle = .tripleDashed(mainColor: authorNameColor, secondaryColor: .clear, tertiaryColor: .clear)
}
} else {
lineStyle = .solid(color: interfaceState.theme.list.itemAccentColor)
authorNameColor = interfaceState.theme.list.itemAccentColor
}
let codeBackgroundColor: UIColor
if interfaceState.theme.overallDarkAppearance {
codeBackgroundColor = UIColor(white: 1.0, alpha: 0.05)
} else {
codeBackgroundColor = UIColor(white: 0.0, alpha: 0.05)
}
return ChatInputTextView.Theme(
quote: ChatInputTextView.Theme.Quote(
background: authorNameColor.withMultipliedAlpha(interfaceState.theme.overallDarkAppearance ? 0.2 : 0.1),
foreground: authorNameColor,
lineStyle: lineStyle,
codeBackground: codeBackgroundColor,
codeForeground: authorNameColor
)
)
}
class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, ChatInputTextNodeDelegate {
let clippingNode: ASDisplayNode
var textPlaceholderNode: ImmediateTextNodeWithEntities
var textLockIconNode: ASImageNode?
var contextPlaceholderNode: TextNode?
var slowmodePlaceholderNode: ChatTextInputSlowmodePlaceholderNode?
let textInputContainerBackgroundNode: ASImageNode
let textInputContainer: ASDisplayNode
var textInputNode: ChatInputTextNode?
var dustNode: InvisibleInkDustNode?
var customEmojiContainerView: CustomEmojiContainerView?
let textInputBackgroundNode: ASImageNode
var textInputBackgroundTapRecognizer: TouchDownGestureRecognizer?
private var transparentTextInputBackgroundImage: UIImage?
let actionButtons: ChatTextInputActionButtonsNode
private let slowModeButton: BoostSlowModeButton
var mediaRecordingAccessibilityArea: AccessibilityAreaNode?
private let counterTextNode: ImmediateTextNode
let menuButton: HighlightTrackingButtonNode
private let menuButtonBackgroundNode: ASDisplayNode
private let menuButtonClippingNode: ASDisplayNode
private let menuButtonIconNode: MenuIconNode
private let menuButtonTextNode: ImmediateTextNode
private let startButton: SolidRoundedButtonNode
let sendAsAvatarButtonNode: HighlightableButtonNode
let sendAsAvatarReferenceNode: ContextReferenceContentNode
let sendAsAvatarContainerNode: ContextControllerSourceNode
private let sendAsAvatarNode: AvatarNode
let attachmentButton: HighlightableButtonNode
let attachmentButtonDisabledNode: HighlightableButtonNode
let searchLayoutClearButton: HighlightableButton
private let searchLayoutClearImageNode: ASImageNode
private var searchActivityIndicator: ActivityIndicator?
var audioRecordingInfoContainerNode: ASDisplayNode?
var audioRecordingDotNode: AnimationNode?
var audioRecordingDotNodeDismissed = false
var audioRecordingTimeNode: ChatTextInputAudioRecordingTimeNode?
var audioRecordingCancelIndicator: ChatTextInputAudioRecordingCancelIndicator?
var animatingBinNode: AnimationNode?
var viewOnce = false
let viewOnceButton: ChatRecordingViewOnceButtonNode
private var accessoryItemButtons: [(ChatTextInputAccessoryItem, AccessoryItemIconButtonNode)] = []
private var validLayout: (CGFloat, CGFloat, CGFloat, CGFloat, UIEdgeInsets, CGFloat, LayoutMetrics, Bool, Bool)?
private var leftMenuInset: CGFloat = 0.0
private var rightSlowModeInset: CGFloat = 0.0
private var currentTextInputBackgroundWidthOffset: CGFloat = 0.0
var displayAttachmentMenu: () -> Void = { }
var sendMessage: () -> Void = { }
var paste: (ChatTextInputPanelPasteData) -> Void = { _ in }
var updateHeight: (Bool) -> Void = { _ in }
var toggleExpandMediaInput: (() -> Void)?
var switchToTextInputIfNeeded: (() -> Void)?
var updateActivity: () -> Void = { }
private var updatingInputState = false
private var currentPlaceholder: String?
private var sendingTextDisabled: Bool = false
private var presentationInterfaceState: ChatPresentationInterfaceState?
private var initializedPlaceholder = false
private var keepSendButtonEnabled = false
private var extendedSearchLayout = false
var isMediaDeleted: Bool = false
private var recordingPaused = false
private let inputMenu: TextInputMenu
private var theme: PresentationTheme?
private var strings: PresentationStrings?
private let hapticFeedback = HapticFeedback()
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.statusBarHost = self.context?.sharedContext.mainWindow?.statusBarHost
}
}
var micButton: ChatTextInputMediaRecordingButton? {
return self.actionButtons.micButton
}
private let statusDisposable = MetaDisposable()
override var interfaceInteraction: ChatPanelInterfaceInteraction? {
didSet {
if let statuses = self.interfaceInteraction?.statuses {
self.statusDisposable.set((statuses.inlineSearch
|> distinctUntilChanged
|> deliverOnMainQueue).startStrict(next: { [weak self] value in
self?.updateIsProcessingInlineRequest(value)
}).strict())
}
}
}
func updateInputTextState(_ state: ChatTextInputState, keepSendButtonEnabled: Bool, extendedSearchLayout: Bool, accessoryItems: [ChatTextInputAccessoryItem], animated: Bool) {
if let currentState = self.presentationInterfaceState {
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, AccessoryItemIconButtonNode)] = []
for item in accessoryItems {
var itemAndButton: (ChatTextInputAccessoryItem, AccessoryItemIconButtonNode)?
for i in 0 ..< self.accessoryItemButtons.count {
if self.accessoryItemButtons[i].0.key == item.key {
itemAndButton = self.accessoryItemButtons[i]
itemAndButton?.0 = item
self.accessoryItemButtons.remove(at: i)
break
}
}
if itemAndButton == nil {
let button = AccessoryItemIconButtonNode(item: item, theme: currentState.theme, strings: currentState.strings)
button.addTarget(self, action: #selector(self.accessoryItemButtonPressed(_:)), forControlEvents: .touchUpInside)
itemAndButton = (item, button)
}
updatedButtons.append(itemAndButton!)
}
for (_, button) in self.accessoryItemButtons {
button.removeFromSupernode()
}
self.accessoryItemButtons = updatedButtons
}
}
if state.inputText.length != 0 && self.textInputNode == nil {
self.loadTextInputNode()
}
if let textInputNode = self.textInputNode, let _ = self.presentationInterfaceState, let context = self.context {
self.updatingInputState = true
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(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize)
}
textInputNode.attributedText = textAttributedStringForStateText(context: context, stateText: state.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed, availableEmojis: (self.context?.animatedEmojiStickersValue.keys).flatMap(Set.init) ?? Set(), emojiViewProvider: self.emojiViewProvider, makeCollapsedQuoteAttachment: { text, attributes in
return ChatInputTextCollapsedQuoteAttachmentImpl(text: text, attributes: attributes)
})
textInputNode.selectedRange = NSMakeRange(state.selectionRange.lowerBound, state.selectionRange.count)
if let presentationInterfaceState = self.presentationInterfaceState {
refreshChatTextInputAttributes(context: context, textView: textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: (self.context?.animatedEmojiStickersValue.keys).flatMap(Set.init) ?? Set(), emojiViewProvider: self.emojiViewProvider, makeCollapsedQuoteAttachment: { text, attributes in
return ChatInputTextCollapsedQuoteAttachmentImpl(text: text, attributes: attributes)
})
}
self.updatingInputState = false
self.keepSendButtonEnabled = keepSendButtonEnabled
self.extendedSearchLayout = extendedSearchLayout
self.updateTextNodeText(animated: animated)
self.updateSpoiler()
}
}
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(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize)
}
textInputNode.attributedText = NSAttributedString(string: value, font: Font.regular(baseFontSize), textColor: textColor)
self.chatInputTextNodeDidUpdateText()
}
}
}
private let textInputViewInternalInsets: UIEdgeInsets
private let accessoryButtonSpacing: CGFloat = 0.0
private let accessoryButtonInset: CGFloat = 2.0
private var spoilersRevealed = false
private var animatingTransition = false
var finishedTransitionToPreview: Bool?
private var touchDownGestureRecognizer: TouchDownGestureRecognizer?
var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?
private let presentationContext: ChatPresentationContext?
private var tooltipController: TooltipScreen?
init(context: AccountContext, presentationInterfaceState: ChatPresentationInterfaceState, presentationContext: ChatPresentationContext?, presentController: @escaping (ViewController) -> Void) {
self.presentationInterfaceState = presentationInterfaceState
self.presentationContext = presentationContext
self.textInputViewInternalInsets = UIEdgeInsets(top: 1.0, left: 13.0, bottom: 1.0, right: 13.0)
var hasSpoilers = true
var hasQuotes = true
if presentationInterfaceState.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat {
hasSpoilers = false
hasQuotes = false
}
self.inputMenu = TextInputMenu(hasSpoilers: hasSpoilers, hasQuotes: hasQuotes)
self.clippingNode = ASDisplayNode()
self.clippingNode.clipsToBounds = true
self.textInputContainerBackgroundNode = ASImageNode()
self.textInputContainerBackgroundNode.isUserInteractionEnabled = false
self.textInputContainerBackgroundNode.displaysAsynchronously = false
self.textInputContainer = ASDisplayNode()
self.textInputContainer.addSubnode(self.textInputContainerBackgroundNode)
self.textInputContainer.clipsToBounds = true
self.textInputBackgroundNode = ASImageNode()
self.textInputBackgroundNode.displaysAsynchronously = false
self.textInputBackgroundNode.displayWithoutProcessing = true
self.textPlaceholderNode = ImmediateTextNodeWithEntities()
self.textPlaceholderNode.arguments = TextNodeWithEntities.Arguments(
context: context,
cache: context.animationCache,
renderer: context.animationRenderer,
placeholderColor: .clear,
attemptSynchronous: true
)
self.textPlaceholderNode.contentMode = .topLeft
self.textPlaceholderNode.contentsScale = UIScreenScale
self.textPlaceholderNode.maximumNumberOfLines = 1
self.textPlaceholderNode.isUserInteractionEnabled = false
self.menuButton = HighlightTrackingButtonNode()
self.menuButton.clipsToBounds = true
self.menuButton.cornerRadius = 16.0
self.menuButton.accessibilityLabel = presentationInterfaceState.strings.Conversation_InputMenu
self.menuButtonBackgroundNode = ASDisplayNode()
self.menuButtonBackgroundNode.isUserInteractionEnabled = false
self.menuButtonClippingNode = ASDisplayNode()
self.menuButtonClippingNode.clipsToBounds = true
self.menuButtonClippingNode.isUserInteractionEnabled = false
self.menuButtonIconNode = MenuIconNode()
self.menuButtonIconNode.isUserInteractionEnabled = false
self.menuButtonIconNode.customColor = presentationInterfaceState.theme.chat.inputPanel.actionControlForegroundColor
self.menuButtonTextNode = ImmediateTextNode()
self.startButton = SolidRoundedButtonNode(title: presentationInterfaceState.strings.Bot_Start, theme: SolidRoundedButtonTheme(theme: presentationInterfaceState.theme), height: 50.0, cornerRadius: 11.0, gloss: true)
self.startButton.progressType = .embedded
self.startButton.isHidden = true
self.sendAsAvatarButtonNode = HighlightableButtonNode()
self.sendAsAvatarReferenceNode = ContextReferenceContentNode()
self.sendAsAvatarContainerNode = ContextControllerSourceNode()
self.sendAsAvatarContainerNode.animateScale = false
self.sendAsAvatarNode = AvatarNode(font: avatarPlaceholderFont(size: 16.0))
self.attachmentButton = HighlightableButtonNode(pointerStyle: .circle(36.0))
self.attachmentButton.accessibilityLabel = presentationInterfaceState.strings.VoiceOver_AttachMedia
self.attachmentButton.accessibilityTraits = [.button]
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(context: context, presentationInterfaceState: presentationInterfaceState, presentationContext: presentationContext, presentController: presentController)
self.counterTextNode = ImmediateTextNode()
self.counterTextNode.textAlignment = .center
self.slowModeButton = BoostSlowModeButton()
self.slowModeButton.alpha = 0.0
self.viewOnceButton = ChatRecordingViewOnceButtonNode(icon: .viewOnce)
super.init()
self.slowModeButton.requestUpdate = { [weak self] in
self?.requestLayout(transition: .animated(duration: 0.2, curve: .easeInOut))
}
self.slowModeButton.addTarget(self, action: #selector(self.slowModeButtonPressed), forControlEvents: .touchUpInside)
self.viewForOverlayContent = ChatTextViewForOverlayContent(
ignoreHit: { [weak self] view, point in
guard let strongSelf = self else {
return false
}
if strongSelf.view.hitTest(view.convert(point, to: strongSelf.view), with: nil) != nil {
return true
}
if view.convert(point, to: strongSelf.view).y > strongSelf.view.bounds.maxY {
return true
}
return false
},
dismissSuggestions: { [weak self] in
guard let strongSelf = self, let currentEmojiSuggestion = strongSelf.currentEmojiSuggestion, let textInputNode = strongSelf.textInputNode else {
return
}
strongSelf.dismissedEmojiSuggestionPosition = currentEmojiSuggestion.position
strongSelf.updateInputField(textInputFrame: textInputNode.frame, transition: .immediate)
}
)
self.context = context
self.addSubnode(self.clippingNode)
self.sendAsAvatarContainerNode.activated = { [weak self] gesture, _ in
guard let strongSelf = self else {
return
}
strongSelf.interfaceInteraction?.openSendAsPeer(strongSelf.sendAsAvatarReferenceNode, gesture)
}
self.sendAsAvatarButtonNode.addTarget(self, action: #selector(self.sendAsAvatarButtonPressed), forControlEvents: .touchUpInside)
self.sendAsAvatarButtonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .spring)
transition.updateTransformScale(node: strongSelf.sendAsAvatarButtonNode, scale: 0.85)
} else {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.5, curve: .spring)
transition.updateTransformScale(node: strongSelf.sendAsAvatarButtonNode, scale: 1.0)
}
}
}
self.menuButton.addTarget(self, action: #selector(self.menuButtonPressed), forControlEvents: .touchUpInside)
self.menuButton.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .spring)
transition.updateTransformScale(node: strongSelf.menuButton, scale: 0.85)
} else {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.5, curve: .spring)
transition.updateTransformScale(node: strongSelf.menuButton, scale: 1.0)
}
}
}
self.startButton.pressed = { [weak self] in
guard let self, let presentationInterfaceState = self.presentationInterfaceState else {
return
}
if presentationInterfaceState.peerIsBlocked {
self.interfaceInteraction?.unblockPeer()
} else {
self.interfaceInteraction?.sendBotStart(presentationInterfaceState.botStartPayload)
}
if let tooltipController = self.tooltipController {
self.tooltipController = nil
tooltipController.dismiss()
}
}
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
if let strongSelf = self {
if strongSelf.presentationInterfaceState?.voiceMessagesAvailable == false {
self?.interfaceInteraction?.displayRestrictedInfo(.premiumVoiceMessages, .tooltip)
} else {
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(viewOnce: strongSelf.viewOnce))
} else {
interfaceInteraction.finishMediaRecording(.dismiss)
}
} else {
// interfaceInteraction.finishMediaRecording(.dismiss)
}
strongSelf.viewOnce = false
strongSelf.tooltipController?.dismiss()
}
}
self.actionButtons.micButton.offsetRecordingControls = { [weak self] in
if let strongSelf = self, let presentationInterfaceState = strongSelf.presentationInterfaceState {
if let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, metrics, isSecondary, isMediaInputExpanded) = strongSelf.validLayout {
let _ = strongSelf.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: .immediate, interfaceState: presentationInterfaceState, metrics: metrics, isMediaInputExpanded: isMediaInputExpanded)
}
}
}
self.actionButtons.micButton.updateCancelTranslation = { [weak self] in
if let strongSelf = self, let presentationInterfaceState = strongSelf.presentationInterfaceState {
if let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, metrics, isSecondary, isMediaInputExpanded) = strongSelf.validLayout {
let _ = strongSelf.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: .immediate, interfaceState: presentationInterfaceState, metrics: metrics, isMediaInputExpanded: isMediaInputExpanded)
}
}
}
self.actionButtons.micButton.stopRecording = { [weak self] in
if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction {
interfaceInteraction.stopMediaRecording()
strongSelf.tooltipController?.dismiss()
}
}
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.sendContainerNode.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.clippingNode.addSubnode(self.textInputContainer)
self.clippingNode.addSubnode(self.textInputBackgroundNode)
self.clippingNode.addSubnode(self.textPlaceholderNode)
self.menuButton.addSubnode(self.menuButtonBackgroundNode)
self.menuButton.addSubnode(self.menuButtonClippingNode)
self.menuButtonClippingNode.addSubnode(self.menuButtonTextNode)
self.menuButton.addSubnode(self.menuButtonIconNode)
self.sendAsAvatarContainerNode.addSubnode(self.sendAsAvatarReferenceNode)
self.sendAsAvatarReferenceNode.addSubnode(self.sendAsAvatarNode)
self.sendAsAvatarButtonNode.addSubnode(self.sendAsAvatarContainerNode)
self.clippingNode.addSubnode(self.sendAsAvatarButtonNode)
self.clippingNode.addSubnode(self.menuButton)
self.clippingNode.addSubnode(self.attachmentButton)
self.clippingNode.addSubnode(self.attachmentButtonDisabledNode)
self.clippingNode.addSubnode(self.startButton)
self.clippingNode.addSubnode(self.actionButtons)
self.clippingNode.addSubnode(self.counterTextNode)
self.clippingNode.addSubnode(self.slowModeButton)
self.clippingNode.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 {
if strongSelf.sendingTextDisabled {
guard let controller = strongSelf.interfaceInteraction?.chatController() as? ChatControllerImpl else {
return
}
if let boostsToUnrestrict = strongSelf.presentationInterfaceState?.boostsToUnrestrict, boostsToUnrestrict > 0 {
strongSelf.interfaceInteraction?.openBoostToUnrestrict()
return
}
controller.controllerInteraction?.displayUndo(.universal(animation: "premium_unlock", scale: 1.0, colors: ["__allcolors__": UIColor(white: 1.0, alpha: 1.0)], title: nil, text: controller.restrictedSendingContentsText(), customUndoText: nil, timeout: nil))
} else {
strongSelf.ensureFocused()
}
}
}
recognizer.waitForTouchUp = { [weak self] in
guard let strongSelf = self, let textInputNode = strongSelf.textInputNode else {
return true
}
if textInputNode.textView.isFirstResponder {
return true
} else {
return false
}
}
self.textInputBackgroundTapRecognizer = recognizer
self.textInputBackgroundNode.isUserInteractionEnabled = true
self.textInputBackgroundNode.view.addGestureRecognizer(recognizer)
if let presentationContext = presentationContext {
self.emojiViewProvider = { [weak self, weak presentationContext] emoji in
guard let strongSelf = self, let presentationContext = presentationContext, let presentationInterfaceState = strongSelf.presentationInterfaceState, let context = strongSelf.context else {
return UIView()
}
let pointSize = floor(24.0 * 1.3)
return EmojiTextAttachmentView(context: context, userLocation: .other, emoji: emoji, file: emoji.file, cache: presentationContext.animationCache, renderer: presentationContext.animationRenderer, placeholderColor: presentationInterfaceState.theme.chat.inputPanel.inputTextColor.withAlphaComponent(0.12), pointSize: CGSize(width: pointSize, height: pointSize))
}
}
self.viewOnceButton.addTarget(self, action: #selector(self.viewOncePressed), forControlEvents: [.touchUpInside])
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.statusDisposable.dispose()
self.tooltipController?.dismiss()
self.currentEmojiSuggestion?.disposable.dispose()
}
override func didLoad() {
super.didLoad()
if let viewForOverlayContent = self.viewForOverlayContent {
viewForOverlayContent.addSubnode(self.viewOnceButton)
}
}
func loadTextInputNodeIfNeeded() {
if self.textInputNode == nil {
self.loadTextInputNode()
}
}
private func loadTextInputNode() {
let textInputNode = ChatInputTextNode()
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 context = self.context, let presentationInterfaceState = self.presentationInterfaceState {
textInputNode.textView.theme = makeTextInputTheme(context: context, interfaceState: presentationInterfaceState)
textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor
tintColor = presentationInterfaceState.theme.list.itemAccentColor
baseFontSize = max(minInputFontSize, 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.textView.typingAttributes = [NSAttributedString.Key.font: Font.regular(max(minInputFontSize, baseFontSize)), NSAttributedString.Key.foregroundColor: textColor, NSAttributedString.Key.paragraphStyle: 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.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
textInputNode.isUserInteractionEnabled = !self.sendingTextDisabled
self.textInputNode = textInputNode
if let textInputBackgroundTapRecognizer = self.textInputBackgroundTapRecognizer {
self.textInputBackgroundTapRecognizer = nil
self.textInputBackgroundNode.view.removeGestureRecognizer(textInputBackgroundTapRecognizer)
}
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
}
if let presentationInterfaceState = self.presentationInterfaceState {
refreshChatTextInputTypingAttributes(textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize)
textInputNode.textContainerInset = calculateTextFieldRealInsets(presentationInterfaceState: presentationInterfaceState, accessoryButtonsWidth: accessoryButtonsWidth)
}
if !self.textInputContainer.bounds.size.width.isZero {
let textInputFrame = self.textInputContainer.frame
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), height: textInputFrame.size.height - self.textInputViewInternalInsets.top - self.textInputViewInternalInsets.bottom))
textInputNode.updateLayout(size: textInputNode.bounds.size)
textInputNode.view.layoutIfNeeded()
self.updateSpoiler()
}
self.textInputBackgroundNode.isUserInteractionEnabled = !textInputNode.isUserInteractionEnabled
//self.textInputBackgroundNode.view.removeGestureRecognizer(self.textInputBackgroundNode.view.gestureRecognizers![0])
textInputNode.textView.onUpdateLayout = { [weak self] in
guard let self else {
return
}
self.updateSpoiler()
}
textInputNode.textView.toggleQuoteCollapse = { [weak self] range in
guard let self else {
return
}
self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
let result = NSMutableAttributedString(attributedString: current.inputText)
var selectionRange = current.selectionRange
if let _ = result.attribute(ChatTextInputAttributes.block, at: range.lowerBound, effectiveRange: nil) as? ChatTextInputTextQuoteAttribute {
let blockString = NSMutableAttributedString(attributedString: result.attributedSubstring(from: range))
blockString.removeAttribute(ChatTextInputAttributes.block, range: NSRange(location: 0, length: blockString.length))
result.replaceCharacters(in: range, with: "")
result.insert(NSAttributedString(string: " ", attributes: [
ChatTextInputAttributes.collapsedBlock: blockString
]), at: range.lowerBound)
if selectionRange.lowerBound >= range.lowerBound && selectionRange.upperBound < range.upperBound {
selectionRange = range.lowerBound ..< range.lowerBound
} else if selectionRange.lowerBound >= range.upperBound {
let deltaLength = 1 - range.length
selectionRange = (selectionRange.lowerBound + deltaLength) ..< (selectionRange.lowerBound + deltaLength)
}
} else if let current = result.attribute(ChatTextInputAttributes.collapsedBlock, at: range.lowerBound, effectiveRange: nil) as? NSAttributedString {
result.replaceCharacters(in: range, with: "")
let updatedBlockString = NSMutableAttributedString(attributedString: current)
updatedBlockString.addAttribute(ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .quote, isCollapsed: false), range: NSRange(location: 0, length: updatedBlockString.length))
result.insert(updatedBlockString, at: range.lowerBound)
if selectionRange.lowerBound >= range.upperBound {
let deltaLength = updatedBlockString.length - 1
selectionRange = (selectionRange.lowerBound + deltaLength) ..< (selectionRange.lowerBound + deltaLength)
}
}
let stateResult = stateAttributedStringForText(result)
if selectionRange.lowerBound < 0 {
selectionRange = 0 ..< selectionRange.upperBound
}
if selectionRange.upperBound > stateResult.length {
selectionRange = selectionRange.lowerBound ..< stateResult.length
}
return (ChatTextInputState(
inputText: stateResult,
selectionRange: selectionRange
), inputMode)
}
}
let recognizer = TouchDownGestureRecognizer(target: self, action: #selector(self.textInputBackgroundViewTap(_:)))
recognizer.touchDown = { [weak self] in
if let strongSelf = self {
if strongSelf.textInputNode?.isFirstResponder() == true {
Queue.mainQueue().after(0.05) {
strongSelf.ensureFocusedOnTap()
}
} else {
strongSelf.ensureFocusedOnTap()
}
}
}
recognizer.waitForTouchUp = {
return true
}
textInputNode.view.addGestureRecognizer(recognizer)
self.touchDownGestureRecognizer = 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
var textFieldInsets = self.textFieldInsets(metrics: metrics)
if self.actionButtons.frame.width > 44.0 {
textFieldInsets.right = self.actionButtons.frame.width - 2.0
}
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
var textInputViewRealInsets = UIEdgeInsets()
if let presentationInterfaceState = self.presentationInterfaceState {
textFieldMinHeight = calclulateTextFieldMinHeight(presentationInterfaceState, metrics: metrics)
}
if let presentationInterfaceState = self.presentationInterfaceState {
textInputViewRealInsets = calculateTextFieldRealInsets(presentationInterfaceState: presentationInterfaceState, accessoryButtonsWidth: accessoryButtonsWidth)
}
let textFieldHeight: CGFloat
if let textInputNode = self.textInputNode {
let maxTextWidth = width - textFieldInsets.left - textFieldInsets.right - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right
let measuredHeight = textInputNode.textHeightForWidth(maxTextWidth, rightInset: textInputViewRealInsets.right)
let unboundTextFieldHeight = max(textFieldMinHeight, ceil(measuredHeight))
let maxNumberOfLines = min(12, (Int(fieldMaxHeight - 11.0) - 33) / 22)
let updatedMaxHeight = (CGFloat(maxNumberOfLines) * (22.0 + 2.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
}
private func animateBotButtonInFromMenu(transition: ContainedViewLayoutTransition) {
guard !self.animatingTransition else {
return
}
guard let menuIconSnapshotView = self.menuButtonIconNode.view.snapshotView(afterScreenUpdates: false), let menuTextSnapshotView = self.menuButtonTextNode.view.snapshotView(afterScreenUpdates: false) else {
self.startButton.highlightEnabled = true
self.menuButton.isHidden = true
return
}
if transition.isAnimated {
self.animatingTransition = true
self.startButton.highlightEnabled = false
}
self.menuButton.isHidden = true
transition.animateFrame(layer: self.startButton.layer, from: self.menuButton.frame)
transition.animateFrame(layer: self.startButton.buttonBackgroundNode.layer, from: CGRect(origin: .zero, size: self.menuButton.frame.size))
transition.animatePosition(node: self.startButton.titleNode, from: CGPoint(x: self.menuButton.frame.width / 2.0, y: self.menuButton.frame.height / 2.0))
let targetButtonCornerRadius = self.startButton.buttonCornerRadius
self.startButton.buttonBackgroundNode.cornerRadius = self.menuButton.cornerRadius
transition.updateCornerRadius(node: self.startButton.buttonBackgroundNode, cornerRadius: targetButtonCornerRadius)
transition.animateTransformScale(node: self.startButton.titleNode, from: 0.4)
self.startButton.titleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
let menuContentDelta = (self.startButton.frame.width - self.menuButton.frame.width) / 2.0
menuIconSnapshotView.frame = self.menuButtonIconNode.frame.offsetBy(dx: self.menuButton.frame.minX, dy: self.menuButton.frame.minY)
self.view.addSubview(menuIconSnapshotView)
menuIconSnapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak menuIconSnapshotView] _ in
menuIconSnapshotView?.removeFromSuperview()
})
transition.updatePosition(layer: menuIconSnapshotView.layer, position: CGPoint(x: menuIconSnapshotView.center.x + menuContentDelta, y: self.startButton.position.y))
menuTextSnapshotView.frame = self.menuButtonTextNode.frame.offsetBy(dx: self.menuButton.frame.minX + 19.0, dy: self.menuButton.frame.minY)
self.view.addSubview(menuTextSnapshotView)
menuTextSnapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak menuTextSnapshotView, weak self] _ in
menuTextSnapshotView?.removeFromSuperview()
self?.animatingTransition = false
self?.startButton.highlightEnabled = true
})
transition.updatePosition(layer: menuTextSnapshotView.layer, position: CGPoint(x: menuTextSnapshotView.center.x + menuContentDelta, y: self.startButton.position.y))
}
func animateBotButtonOutToMenu(transition: ContainedViewLayoutTransition) {
guard !self.animatingTransition else {
return
}
guard let menuIconSnapshotView = self.menuButtonIconNode.view.snapshotView(afterScreenUpdates: false), let menuTextSnapshotView = self.menuButtonTextNode.view.snapshotView(afterScreenUpdates: false) else {
self.startButton.highlightEnabled = true
self.menuButton.isHidden = false
return
}
if transition.isAnimated {
self.animatingTransition = true
self.startButton.highlightEnabled = false
}
let sourceButtonFrame = self.startButton.frame
transition.updateFrame(node: self.startButton, frame: self.menuButton.frame)
transition.updateFrame(node: self.startButton.buttonBackgroundNode, frame: CGRect(origin: .zero, size: self.menuButton.frame.size))
let sourceButtonTextPosition = self.startButton.titleNode.position
transition.updatePosition(node: self.startButton.titleNode, position: CGPoint(x: self.menuButton.frame.width / 2.0, y: self.menuButton.frame.height / 2.0))
let sourceButtonCornerRadius = self.startButton.buttonCornerRadius
transition.updateCornerRadius(node: self.startButton.buttonBackgroundNode, cornerRadius: self.menuButton.cornerRadius)
transition.animateTransformScale(layer: self.startButton.titleNode.layer, from: CGPoint(x: 1.0, y: 1.0), to: CGPoint(x: 0.4, y: 0.4))
Queue.mainQueue().justDispatch {
self.startButton.titleNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
}
let menuContentDelta = (sourceButtonFrame.width - self.menuButton.frame.width) / 2.0
var menuIconSnapshotViewFrame = self.menuButtonIconNode.frame.offsetBy(dx: self.menuButton.frame.minX + menuContentDelta, dy: self.menuButton.frame.minY)
menuIconSnapshotViewFrame.origin.y = self.startButton.position.y - menuIconSnapshotViewFrame.height / 2.0
menuIconSnapshotView.frame = menuIconSnapshotViewFrame
self.view.addSubview(menuIconSnapshotView)
menuIconSnapshotView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
transition.updatePosition(layer: menuIconSnapshotView.layer, position: CGPoint(x: menuIconSnapshotView.center.x - menuContentDelta, y: self.menuButton.position.y))
var menuTextSnapshotViewFrame = self.menuButtonTextNode.frame.offsetBy(dx: self.menuButton.frame.minX + 19.0 + menuContentDelta, dy: self.menuButton.frame.minY)
menuTextSnapshotViewFrame.origin.y = self.startButton.position.y - menuTextSnapshotViewFrame.height / 2.0
menuTextSnapshotView.frame = menuTextSnapshotViewFrame
self.view.addSubview(menuTextSnapshotView)
menuTextSnapshotView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
transition.updatePosition(layer: menuTextSnapshotView.layer, position: CGPoint(x: menuTextSnapshotView.center.x - menuContentDelta, y: self.menuButton.position.y), completion: { [weak self, weak menuIconSnapshotView, weak menuTextSnapshotView] _ in
self?.animatingTransition = false
menuIconSnapshotView?.removeFromSuperview()
menuTextSnapshotView?.removeFromSuperview()
self?.menuButton.isHidden = false
self?.startButton.isHidden = true
self?.startButton.frame = sourceButtonFrame
self?.startButton.buttonBackgroundNode.frame = CGRect(origin: .zero, size: sourceButtonFrame.size)
self?.startButton.titleNode.position = sourceButtonTextPosition
self?.startButton.titleNode.layer.removeAllAnimations()
self?.startButton.buttonBackgroundNode.cornerRadius = sourceButtonCornerRadius
self?.startButton.highlightEnabled = true
})
}
private var absoluteRect: (CGRect, CGSize)?
override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition) {
self.absoluteRect = (rect, containerSize)
if !self.actionButtons.frame.width.isZero {
self.actionButtons.updateAbsoluteRect(CGRect(origin: rect.origin.offsetBy(dx: self.actionButtons.frame.minX, dy: self.actionButtons.frame.minY), size: self.actionButtons.frame.size), within: containerSize, transition: transition)
}
let absoluteFrame = self.startButton.view.convert(self.startButton.bounds, to: nil)
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY - 1.0), size: CGSize())
if let tooltipController = self.tooltipController, self.view.window != nil {
tooltipController.location = .point(location, .bottom)
}
}
func requestLayout(transition: ContainedViewLayoutTransition = .immediate) {
guard let presentationInterfaceState = self.presentationInterfaceState, let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, metrics, isSecondary, isMediaInputExpanded) = self.validLayout else {
return
}
let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: transition, interfaceState: presentationInterfaceState, metrics: metrics, isMediaInputExpanded: isMediaInputExpanded)
}
override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat {
let previousAdditionalSideInsets = self.validLayout?.4
self.validLayout = (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, metrics, isSecondary, isMediaInputExpanded)
var transition = transition
var additionalOffset: CGFloat = 0.0
if let previousAdditionalSideInsets = previousAdditionalSideInsets, previousAdditionalSideInsets.right != additionalSideInsets.right {
additionalOffset = (previousAdditionalSideInsets.right - additionalSideInsets.right) / 3.0
if case .animated = transition {
transition = .animated(duration: 0.2, curve: .easeInOut)
}
}
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 = true
}
}
var isRecording = false
if let _ = interfaceState.inputTextPanelState.mediaRecordingState {
isRecording = true
}
var isScheduledMessages = false
if case .scheduledMessages = interfaceState.subject {
isScheduledMessages = true
}
var isSlowmodeActive = false
if interfaceState.slowmodeState != nil && !isScheduledMessages {
isSlowmodeActive = true
if !isEditingMedia {
isMediaEnabled = false
}
}
var displayMediaButton = true
if case let .customChatContents(customChatContents) = interfaceState.subject {
switch customChatContents.kind {
case .hashTagSearch:
break
case .quickReplyMessageInput:
break
case .businessLinkSetup:
displayMediaButton = false
case .postSuggestions:
break
}
}
let attachmentButtonAlpha: CGFloat
if displayMediaButton {
attachmentButtonAlpha = isMediaEnabled ? 1.0 : 0.4
} else {
attachmentButtonAlpha = 0.0
}
transition.updateAlpha(layer: self.attachmentButton.layer, alpha: attachmentButtonAlpha)
self.attachmentButton.isEnabled = isMediaEnabled && !isRecording
self.attachmentButton.accessibilityTraits = (!isSlowmodeActive || isMediaEnabled) ? [.button] : [.button, .notEnabled]
self.attachmentButtonDisabledNode.isHidden = !isSlowmodeActive || isMediaEnabled
let canBypassRestrictions = canBypassRestrictions(chatPresentationInterfaceState: interfaceState)
var sendingTextDisabled = false
if interfaceState.interfaceState.editMessage == nil {
if let peer = interfaceState.renderedPeer?.peer {
if let channel = peer as? TelegramChannel, channel.hasBannedPermission(.banSendText, ignoreDefault: canBypassRestrictions) != nil {
sendingTextDisabled = true
} else if let group = peer as? TelegramGroup, group.hasBannedPermission(.banSendText) {
sendingTextDisabled = true
}
}
}
self.sendingTextDisabled = sendingTextDisabled
self.textInputNode?.isUserInteractionEnabled = !sendingTextDisabled
var displayBotStartButton = false
if case .scheduledMessages = interfaceState.subject {
} else {
if let user = interfaceState.renderedPeer?.peer as? TelegramUser, user.botInfo != nil {
if let chatHistoryState = interfaceState.chatHistoryState, case .loaded(true, _) = chatHistoryState {
displayBotStartButton = true
} else if interfaceState.peerIsBlocked {
displayBotStartButton = true
}
}
}
var inputHasText = false
if let textInputNode = self.textInputNode, let attributedText = textInputNode.attributedText, attributedText.length != 0 {
inputHasText = true
}
var hasMenuButton = false
var menuButtonExpanded = false
var isSendAsButton = false
var shouldDisplayMenuButton = false
if interfaceState.hasBotCommands {
shouldDisplayMenuButton = true
} else if case .webView = interfaceState.botMenuButton {
shouldDisplayMenuButton = true
}
let mediaRecordingState = interfaceState.inputTextPanelState.mediaRecordingState
if let sendAsPeers = interfaceState.sendAsPeers, !sendAsPeers.isEmpty && interfaceState.editMessageState == nil {
hasMenuButton = true
menuButtonExpanded = false
isSendAsButton = true
self.sendAsAvatarNode.isHidden = false
var currentPeer = sendAsPeers.first(where: { $0.peer.id == interfaceState.currentSendAsPeerId})?.peer
if currentPeer == nil {
currentPeer = sendAsPeers.first?.peer
}
if let context = self.context, let peer = currentPeer {
self.sendAsAvatarNode.setPeer(context: context, theme: interfaceState.theme, peer: EnginePeer(peer), emptyColor: interfaceState.theme.list.mediaPlaceholderColor)
}
} else if let peer = interfaceState.renderedPeer?.peer as? TelegramUser, let _ = peer.botInfo, shouldDisplayMenuButton && interfaceState.editMessageState == nil {
hasMenuButton = true
if !inputHasText {
switch interfaceState.inputMode {
case .none, .inputButtons:
menuButtonExpanded = true
default:
break
}
}
self.sendAsAvatarNode.isHidden = true
} else {
self.sendAsAvatarNode.isHidden = true
}
if mediaRecordingState != nil {
hasMenuButton = false
}
let buttonInset: CGFloat = max(leftInset, 16.0)
let maximumButtonWidth: CGFloat = min(430.0, width)
let buttonHeight = self.startButton.updateLayout(width: maximumButtonWidth - buttonInset * 2.0, transition: transition)
let buttonSize = CGSize(width: maximumButtonWidth - buttonInset * 2.0, height: buttonHeight)
self.startButton.frame = CGRect(origin: CGPoint(x: leftInset + floor((width - leftInset - rightInset - buttonSize.width) / 2.0), y: 6.0), size: buttonSize)
var hideOffset: CGPoint = .zero
if displayBotStartButton {
if hasMenuButton {
hideOffset = CGPoint(x: width, y: 0.0)
} else {
hideOffset = CGPoint(x: 0.0, y: 80.0)
}
if self.startButton.isHidden {
self.startButton.isHidden = false
if hasMenuButton {
self.animateBotButtonInFromMenu(transition: transition)
} else {
transition.animatePosition(layer: self.startButton.layer, from: CGPoint(x: 0.0, y: 80.0), to: CGPoint(), additive: true)
}
if let context = self.context {
let parentFrame = self.view.convert(self.bounds, to: nil)
let absoluteFrame = self.startButton.view.convert(self.startButton.bounds, to: nil).offsetBy(dx: -parentFrame.minX, dy: 0.0)
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY - 1.0), size: CGSize())
if let tooltipController = self.tooltipController {
if self.view.window != nil {
tooltipController.location = .point(location, .bottom)
}
} else {
let controller = TooltipScreen(account: context.account, sharedContext: context.sharedContext, text: .plain(text: interfaceState.strings.Bot_TapToUse), icon: .downArrows, location: .point(location, .bottom), displayDuration: .infinite, shouldDismissOnTouch: { _, _ in
return .ignore
})
controller.alwaysVisible = true
self.tooltipController = controller
let delay: Double
if case .regular = metrics.widthClass {
delay = 0.1
} else {
delay = 0.35
}
Queue.mainQueue().after(delay, {
let parentFrame = self.view.convert(self.bounds, to: nil)
let absoluteFrame = self.startButton.view.convert(self.startButton.bounds, to: nil).offsetBy(dx: -parentFrame.minX, dy: 0.0)
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY - 1.0), size: CGSize())
controller.location = .point(location, .bottom)
self.interfaceInteraction?.presentControllerInCurrent(controller, nil)
})
}
}
} else {
if hasMenuButton && !self.animatingTransition {
self.menuButton.isHidden = true
}
}
} else if !self.startButton.isHidden {
if hasMenuButton {
self.animateBotButtonOutToMenu(transition: transition)
} else {
transition.animatePosition(node: self.startButton, to: CGPoint(x: 0.0, y: 80.0), removeOnCompletion: false, additive: true, completion: { _ in
self.startButton.isHidden = true
self.startButton.layer.removeAllAnimations()
})
}
}
var updatedPlaceholder: String?
var placeholderHasStar = false
let themeUpdated = self.presentationInterfaceState?.theme !== interfaceState.theme
var buttonTitleUpdated = false
var menuTextSize = self.menuButtonTextNode.frame.size
if self.presentationInterfaceState != interfaceState {
let previousState = self.presentationInterfaceState
self.presentationInterfaceState = interfaceState
if case .webView = interfaceState.botMenuButton, self.menuButtonIconNode.iconState == .menu {
self.menuButtonIconNode.enqueueState(.app, animated: false)
} else if case .commands = interfaceState.botMenuButton, self.menuButtonIconNode.iconState == .app {
self.menuButtonIconNode.enqueueState(.menu, animated: false)
}
if themeUpdated {
self.menuButtonIconNode.customColor = interfaceState.theme.chat.inputPanel.actionControlForegroundColor
self.startButton.updateTheme(SolidRoundedButtonTheme(theme: interfaceState.theme))
}
if let sendAsPeers = interfaceState.sendAsPeers, !sendAsPeers.isEmpty {
self.menuButtonIconNode.enqueueState(.close, animated: false)
} else if case .webView = interfaceState.botMenuButton, let previousShowWebView = previousState?.showWebView, previousShowWebView != interfaceState.showWebView {
if interfaceState.showWebView {
// self.menuButtonIconNode.enqueueState(.close, animated: true)
} else {
self.menuButtonIconNode.enqueueState(.app, animated: true)
}
} else if let previousShowCommands = previousState?.showCommands, previousShowCommands != interfaceState.showCommands {
if interfaceState.showCommands {
self.menuButtonIconNode.enqueueState(.close, animated: true)
} else {
self.menuButtonIconNode.enqueueState(.menu, animated: true)
}
}
let buttonTitle: String
if case let .webView(title, _) = interfaceState.botMenuButton {
buttonTitle = title
} else {
buttonTitle = interfaceState.strings.Conversation_InputMenu
}
buttonTitleUpdated = self.menuButtonTextNode.attributedText != nil && self.menuButtonTextNode.attributedText?.string != buttonTitle
self.menuButtonTextNode.attributedText = NSAttributedString(string: buttonTitle, font: Font.with(size: 16.0, design: .round, weight: .medium, traits: []), textColor: interfaceState.theme.chat.inputPanel.actionControlForegroundColor)
self.menuButton.accessibilityLabel = self.menuButtonTextNode.attributedText?.string
if buttonTitleUpdated, let buttonTextSnapshotView = self.menuButtonTextNode.view.snapshotView(afterScreenUpdates: false) {
buttonTextSnapshotView.frame = self.menuButtonTextNode.view.frame
self.menuButtonTextNode.view.superview?.addSubview(buttonTextSnapshotView)
buttonTextSnapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak buttonTextSnapshotView] _ in
buttonTextSnapshotView?.removeFromSuperview()
})
self.menuButtonTextNode.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
menuTextSize = self.menuButtonTextNode.updateLayout(CGSize(width: width / 2.0 - 60.0, height: 44.0))
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(minInputFontSize, interfaceState.fontSize.baseDisplaySize)
if let textInputNode = self.textInputNode {
if let text = textInputNode.attributedText {
let range = textInputNode.selectedRange
let updatedText = NSMutableAttributedString(attributedString: text)
updatedText.addAttribute(NSAttributedString.Key.foregroundColor, value: textColor, range: NSRange(location: 0, length: updatedText.length))
textInputNode.attributedText = updatedText
textInputNode.selectedRange = range
}
textInputNode.textView.typingAttributes = [NSAttributedString.Key.font: Font.regular(baseFontSize), NSAttributedString.Key.foregroundColor: textColor]
self.updateSpoiler()
}
}
let tintColor = interfaceState.theme.list.itemAccentColor
if let textInputNode = self.textInputNode, tintColor != textInputNode.tintColor {
textInputNode.tintColor = tintColor
textInputNode.tintColorDidChange()
}
if let textInputNode = self.textInputNode, let context = self.context {
textInputNode.textView.theme = makeTextInputTheme(context: context, interfaceState: interfaceState)
}
let keyboardAppearance = interfaceState.theme.rootController.keyboardColor.keyboardAppearance
if let textInputNode = self.textInputNode, textInputNode.keyboardAppearance != keyboardAppearance {
if textInputNode.isFirstResponder() && textInputNode.isCurrentlyEmoji() {
textInputNode.initialPrimaryLanguage = "emoji"
textInputNode.resetInitialPrimaryLanguage()
}
textInputNode.keyboardAppearance = keyboardAppearance
}
self.theme = interfaceState.theme
self.menuButtonBackgroundNode.backgroundColor = interfaceState.theme.chat.inputPanel.actionControlFillColor
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, wallpaper: interfaceState.chatWallpaper)
let textFieldMinHeight = calclulateTextFieldMinHeight(interfaceState, metrics: metrics)
let minimalInputHeight: CGFloat = 2.0 + textFieldMinHeight
let strokeWidth: CGFloat
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
strokeWidth = 1.0 - UIScreenPixel
} else {
backgroundColor = interfaceState.theme.chat.inputPanel.panelBackgroundColor
strokeWidth = UIScreenPixel
}
self.textInputBackgroundNode.image = textInputBackgroundImage(backgroundColor: backgroundColor, inputBackgroundColor: nil, strokeColor: interfaceState.theme.chat.inputPanel.inputStrokeColor, diameter: minimalInputHeight, strokeWidth: strokeWidth)
self.transparentTextInputBackgroundImage = textInputBackgroundImage(backgroundColor: nil, inputBackgroundColor: interfaceState.theme.chat.inputPanel.inputBackgroundColor, strokeColor: interfaceState.theme.chat.inputPanel.inputStrokeColor, diameter: minimalInputHeight, strokeWidth: strokeWidth)
self.textInputContainerBackgroundNode.image = generateStretchableFilledCircleImage(diameter: minimalInputHeight, color: interfaceState.theme.chat.inputPanel.inputBackgroundColor)
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: [])
}
}
}
let dismissedButtonMessageUpdated = interfaceState.interfaceState.messageActionsState.dismissedButtonKeyboardMessageId != previousState?.interfaceState.messageActionsState.dismissedButtonKeyboardMessageId
let replyMessageUpdated = interfaceState.interfaceState.replyMessageSubject != previousState?.interfaceState.replyMessageSubject
var peerUpdated = false
if let peer = interfaceState.renderedPeer?.peer, previousState?.renderedPeer?.peer == nil || !peer.isEqual(previousState!.renderedPeer!.peer!) {
peerUpdated = true
}
if peerUpdated || previousState?.interfaceState.silentPosting != interfaceState.interfaceState.silentPosting || themeUpdated || !self.initializedPlaceholder || previousState?.keyboardButtonsMessage?.id != interfaceState.keyboardButtonsMessage?.id || previousState?.keyboardButtonsMessage?.visibleReplyMarkupPlaceholder != interfaceState.keyboardButtonsMessage?.visibleReplyMarkupPlaceholder || dismissedButtonMessageUpdated || replyMessageUpdated || (previousState?.interfaceState.editMessage == nil) != (interfaceState.interfaceState.editMessage == nil) || previousState?.forumTopicData != interfaceState.forumTopicData || previousState?.replyMessage?.id != interfaceState.replyMessage?.id || previousState?.sendPaidMessageStars != interfaceState.sendPaidMessageStars {
self.initializedPlaceholder = true
var placeholder: String = ""
if let peer = interfaceState.renderedPeer?.peer {
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 {
if sendingTextDisabled {
placeholder = interfaceState.strings.Chat_PlaceholderTextNotAllowed
} else {
if let channel = peer as? TelegramChannel, case .group = channel.info, channel.hasPermission(.canBeAnonymous) {
placeholder = interfaceState.strings.Conversation_InputTextAnonymousPlaceholder
} else if case let .replyThread(replyThreadMessage) = interfaceState.chatLocation, !replyThreadMessage.isForumPost, replyThreadMessage.peerId != self.context?.account.peerId {
if replyThreadMessage.isChannelPost {
if let sendPaidMessageStars = interfaceState.sendPaidMessageStars, interfaceState.interfaceState.editMessage == nil {
placeholder = interfaceState.strings.Chat_InputTextPaidCommentPlaceholder(" # \(presentationStringsFormattedNumber(Int32(sendPaidMessageStars.value), interfaceState.dateTimeFormat.groupingSeparator))").string
placeholderHasStar = true
} else {
placeholder = interfaceState.strings.Conversation_InputTextPlaceholderComment
}
} else {
placeholder = interfaceState.strings.Conversation_InputTextPlaceholderReply
}
} else if let channel = peer as? TelegramChannel, channel.isForum, let forumTopicData = interfaceState.forumTopicData {
if let replyMessage = interfaceState.replyMessage, let threadInfo = replyMessage.associatedThreadInfo {
placeholder = interfaceState.strings.Chat_InputPlaceholderReplyInTopic(threadInfo.title).string
} else {
placeholder = interfaceState.strings.Chat_InputPlaceholderMessageInTopic(forumTopicData.title).string
}
} else {
if let sendPaidMessageStars = interfaceState.sendPaidMessageStars, interfaceState.interfaceState.editMessage == nil {
placeholder = interfaceState.strings.Chat_InputTextPaidMessagePlaceholder(" # \(presentationStringsFormattedNumber(Int32(sendPaidMessageStars.value), interfaceState.dateTimeFormat.groupingSeparator))").string
placeholderHasStar = true
} else {
placeholder = interfaceState.strings.Conversation_InputTextPlaceholder
}
}
}
}
}
if case let .customChatContents(customChatContents) = interfaceState.subject {
switch customChatContents.kind {
case .hashTagSearch:
placeholder = ""
case let .quickReplyMessageInput(_, shortcutType):
switch shortcutType {
case .generic:
placeholder = interfaceState.strings.Chat_Placeholder_QuickReply
case .greeting:
placeholder = interfaceState.strings.Chat_Placeholder_GreetingMessage
case .away:
placeholder = interfaceState.strings.Chat_Placeholder_AwayMessage
}
case .businessLinkSetup:
placeholder = interfaceState.strings.Chat_Placeholder_BusinessLinkPreset
case let .postSuggestions(postSuggestions):
//TODO:localize
placeholder = "Suggest for # \(postSuggestions)"
placeholderHasStar = true
}
}
if let keyboardButtonsMessage = interfaceState.keyboardButtonsMessage, interfaceState.interfaceState.messageActionsState.dismissedButtonKeyboardMessageId != keyboardButtonsMessage.id {
if keyboardButtonsMessage.requestsSetupReply && keyboardButtonsMessage.id != interfaceState.interfaceState.replyMessageSubject?.messageId {
} else {
if let placeholderValue = interfaceState.keyboardButtonsMessage?.visibleReplyMarkupPlaceholder, !placeholderValue.isEmpty {
placeholder = placeholderValue
}
}
}
updatedPlaceholder = placeholder
self.actionButtons.sendButtonLongPressEnabled = !isScheduledMessages
}
var sendButtonHasApplyIcon = interfaceState.interfaceState.editMessage != nil
if let interfaceState = self.presentationInterfaceState {
if case let .customChatContents(customChatContents) = interfaceState.subject {
switch customChatContents.kind {
case .hashTagSearch:
break
case .quickReplyMessageInput:
break
case .businessLinkSetup:
sendButtonHasApplyIcon = true
case .postSuggestions:
break
}
}
}
if updateSendButtonIcon {
if !self.actionButtons.animatingSendButton {
let imageNode = self.actionButtons.sendButton.imageNode
if transition.isAnimated && !self.actionButtons.sendContainerNode.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.chatInputPanelApplyIconImage(interfaceState.theme), for: [])
} else {
if isScheduledMessages {
self.actionButtons.sendButton.setImage(PresentationResourcesChat.chatInputPanelScheduleButtonImage(interfaceState.theme), for: [])
} else {
self.actionButtons.sendButton.setImage(PresentationResourcesChat.chatInputPanelSendIconImage(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: [AccessoryItemIconButtonNode]?
if updateAccessoryButtons {
var updatedButtons: [(ChatTextInputAccessoryItem, AccessoryItemIconButtonNode)] = []
for item in interfaceState.inputTextPanelState.accessoryItems {
var itemAndButton: (ChatTextInputAccessoryItem, AccessoryItemIconButtonNode)?
for i in 0 ..< self.accessoryItemButtons.count {
if self.accessoryItemButtons[i].0.key == item.key {
itemAndButton = self.accessoryItemButtons[i]
itemAndButton?.0 = item
self.accessoryItemButtons.remove(at: i)
break
}
}
if itemAndButton == nil {
let button = AccessoryItemIconButtonNode(item: item, theme: interfaceState.theme, strings: interfaceState.strings)
button.addTarget(self, action: #selector(self.accessoryItemButtonPressed(_:)), forControlEvents: .touchUpInside)
itemAndButton = (item, button)
}
updatedButtons.append(itemAndButton!)
}
for (_, button) in self.accessoryItemButtons {
if animatedTransition {
if removeAccessoryButtons == nil {
removeAccessoryButtons = []
}
removeAccessoryButtons!.append(button)
} else {
button.removeFromSupernode()
}
}
self.accessoryItemButtons = updatedButtons
}
let leftMenuInset: CGFloat
let menuButtonHeight: CGFloat = 33.0
let menuCollapsedButtonWidth: CGFloat = isSendAsButton ? menuButtonHeight : 38.0
let menuButtonWidth = menuTextSize.width + 47.0
if hasMenuButton {
let menuButtonSpacing: CGFloat = 10.0
if menuButtonExpanded {
leftMenuInset = menuButtonWidth + menuButtonSpacing
} else {
leftMenuInset = menuCollapsedButtonWidth + menuButtonSpacing
}
} else {
leftMenuInset = 0.0
}
self.leftMenuInset = leftMenuInset
var rightSlowModeInset: CGFloat = 0.0
var slowModeButtonSize: CGSize = .zero
if let presentationInterfaceState = self.presentationInterfaceState, (presentationInterfaceState.boostsToUnrestrict ?? 0) > 0 {
slowModeButtonSize = self.slowModeButton.update(size: CGSize(width: width, height: 44.0), interfaceState: presentationInterfaceState)
rightSlowModeInset = max(0.0, slowModeButtonSize.width - 33.0)
}
self.rightSlowModeInset = rightSlowModeInset
if buttonTitleUpdated && !transition.isAnimated {
transition = .animated(duration: 0.3, curve: .easeInOut)
}
var leftInset = leftInset
var textInputBackgroundWidthOffset: CGFloat = 0.0
var attachmentButtonX: CGFloat = hideOffset.x + leftInset + leftMenuInset + 2.0 - UIScreenPixel
if !displayMediaButton {
attachmentButtonX = -40.0
let inputFieldAdditionalWidth = 40.0 - 4.0
leftInset -= inputFieldAdditionalWidth
textInputBackgroundWidthOffset += inputFieldAdditionalWidth
}
let baseWidth = width - leftInset - leftMenuInset - rightInset - rightSlowModeInset
let (accessoryButtonsWidth, textFieldHeight) = self.calculateTextFieldMetrics(width: baseWidth, maxHeight: maxHeight, metrics: metrics)
var panelHeight = self.panelHeight(textFieldHeight: textFieldHeight, metrics: metrics)
if displayBotStartButton {
panelHeight += 27.0
}
let menuButtonOriginY: CGFloat
if displayBotStartButton {
menuButtonOriginY = floorToScreenPixels((minimalHeight - menuButtonHeight) / 2.0)
} else {
menuButtonOriginY = panelHeight - minimalHeight + floorToScreenPixels((minimalHeight - menuButtonHeight) / 2.0)
}
let menuButtonFrame = CGRect(x: leftInset + 10.0, y: menuButtonOriginY, width: menuButtonExpanded ? menuButtonWidth : menuCollapsedButtonWidth, height: menuButtonHeight)
transition.updateFrameAsPositionAndBounds(node: self.menuButton, frame: menuButtonFrame)
transition.updateFrame(node: self.menuButtonBackgroundNode, frame: CGRect(origin: CGPoint(), size: menuButtonFrame.size))
transition.updateFrame(node: self.menuButtonClippingNode, frame: CGRect(origin: CGPoint(x: 19.0, y: 0.0), size: CGSize(width: menuButtonWidth - 19.0, height: menuButtonFrame.height)))
var menuButtonTitleTransition = transition
if buttonTitleUpdated {
menuButtonTitleTransition = .immediate
}
menuButtonTitleTransition.updateFrame(node: self.menuButtonTextNode, frame: CGRect(origin: CGPoint(x: 16.0, y: 7.0 - UIScreenPixel), size: menuTextSize))
transition.updateAlpha(node: self.menuButtonTextNode, alpha: menuButtonExpanded ? 1.0 : 0.0)
transition.updateFrame(node: self.menuButtonIconNode, frame: CGRect(x: isSendAsButton ? 1.0 + UIScreenPixel : (4.0 + UIScreenPixel), y: 1.0 + UIScreenPixel, width: 30.0, height: 30.0))
transition.updateFrame(node: self.sendAsAvatarButtonNode, frame: menuButtonFrame)
transition.updateFrame(node: self.sendAsAvatarContainerNode, frame: CGRect(origin: CGPoint(), size: menuButtonFrame.size))
transition.updateFrame(node: self.sendAsAvatarReferenceNode, frame: CGRect(origin: CGPoint(), size: menuButtonFrame.size))
transition.updateFrame(node: self.sendAsAvatarNode, frame: CGRect(origin: CGPoint(), size: menuButtonFrame.size))
let showMenuButton = hasMenuButton && interfaceState.interfaceState.mediaDraftState == nil
if isSendAsButton {
if interfaceState.showSendAsPeers {
transition.updateTransformScale(node: self.menuButton, scale: 1.0)
transition.updateAlpha(node: self.menuButton, alpha: 1.0)
transition.updateTransformScale(node: self.sendAsAvatarButtonNode, scale: 0.001)
transition.updateAlpha(node: self.sendAsAvatarButtonNode, alpha: 0.0)
} else {
transition.updateTransformScale(node: self.menuButton, scale: 0.001)
transition.updateAlpha(node: self.menuButton, alpha: 0.0)
transition.updateTransformScale(node: self.sendAsAvatarButtonNode, scale: showMenuButton ? 1.0 : 0.001)
transition.updateAlpha(node: self.sendAsAvatarButtonNode, alpha: showMenuButton ? 1.0 : 0.0)
}
} else {
transition.updateTransformScale(node: self.menuButton, scale: showMenuButton ? 1.0 : 0.001)
transition.updateAlpha(node: self.menuButton, alpha: showMenuButton ? 1.0 : 0.0)
transition.updateTransformScale(node: self.sendAsAvatarButtonNode, scale: 0.001)
transition.updateAlpha(node: self.sendAsAvatarButtonNode, alpha: 0.0)
}
self.menuButton.isUserInteractionEnabled = hasMenuButton
self.sendAsAvatarButtonNode.isUserInteractionEnabled = hasMenuButton && isSendAsButton
self.actionButtons.micButton.updateMode(mode: interfaceState.interfaceState.mediaRecordingMode, animated: transition.isAnimated)
var hideMicButton = false
var audioRecordingItemsAlpha: CGFloat = 1
if mediaRecordingState != nil || (interfaceState.interfaceState.mediaDraftState != nil && self.finishedTransitionToPreview != true) {
if interfaceState.interfaceState.mediaDraftState != nil {
self.finishedTransitionToPreview = false
}
audioRecordingItemsAlpha = 0
let audioRecordingInfoContainerNode: ASDisplayNode
if let currentAudioRecordingInfoContainerNode = self.audioRecordingInfoContainerNode {
audioRecordingInfoContainerNode = currentAudioRecordingInfoContainerNode
} else {
audioRecordingInfoContainerNode = ASDisplayNode()
self.audioRecordingInfoContainerNode = audioRecordingInfoContainerNode
self.clippingNode.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 && mediaRecordingState != nil {
animateTimeSlideIn = true
}
}
var animateCancelSlideIn = false
let audioRecordingCancelIndicator: ChatTextInputAudioRecordingCancelIndicator
if let currentAudioRecordingCancelIndicator = self.audioRecordingCancelIndicator {
audioRecordingCancelIndicator = currentAudioRecordingCancelIndicator
} else {
animateCancelSlideIn = transition.isAnimated && mediaRecordingState != nil
audioRecordingCancelIndicator = ChatTextInputAudioRecordingCancelIndicator(theme: interfaceState.theme, strings: interfaceState.strings, cancel: { [weak self] in
self?.viewOnce = false
self?.interfaceInteraction?.finishMediaRecording(.dismiss)
self?.tooltipController?.dismiss()
})
self.audioRecordingCancelIndicator = audioRecordingCancelIndicator
self.clippingNode.insertSubnode(audioRecordingCancelIndicator, at: 0)
}
let isLocked = mediaRecordingState?.isLocked ?? (interfaceState.interfaceState.mediaDraftState != nil)
var hideInfo = false
if let mediaRecordingState = mediaRecordingState {
switch mediaRecordingState {
case let .audio(recorder, isLocked):
let hadAudioRecorder = self.actionButtons.micButton.audioRecorder != nil
if !hadAudioRecorder, isLocked {
self.actionButtons.micButton.lock()
}
self.actionButtons.micButton.audioRecorder = recorder
audioRecordingTimeNode.audioRecorder = recorder
case let .video(status, _):
let hadVideoRecorder = self.actionButtons.micButton.videoRecordingStatus != nil
if !hadVideoRecorder, isLocked {
self.actionButtons.micButton.lock()
}
switch status {
case let .recording(recordingStatus):
audioRecordingTimeNode.videoRecordingStatus = recordingStatus
self.actionButtons.micButton.videoRecordingStatus = recordingStatus
case .editing:
audioRecordingTimeNode.videoRecordingStatus = nil
self.actionButtons.micButton.videoRecordingStatus = nil
hideMicButton = true
hideInfo = true
}
case .waitingForPreview:
Queue.mainQueue().after(0.3, {
self.actionButtons.micButton.audioRecorder = nil
})
}
}
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)
let audioRecordingCancelIndicatorFrame = 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)
audioRecordingCancelIndicator.frame = audioRecordingCancelIndicatorFrame
if self.actionButtons.micButton.cancelTranslation > cancelTransformThreshold {
let progress: CGFloat = max(0.0, min(1.0, (audioRecordingCancelIndicatorFrame.minX - 100.0) / 10.0))
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 && mediaRecordingState != nil)
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)
}
let dotFrame = CGRect(origin: CGPoint(x: leftInset + 2.0 - UIScreenPixel, y: audioRecordingTimeNode.frame.midY - 20), size: CGSize(width: 40.0, height: 40))
var animateDotAppearing = false
let audioRecordingDotNode: AnimationNode
if let currentAudioRecordingDotNode = self.audioRecordingDotNode, !currentAudioRecordingDotNode.didPlay {
audioRecordingDotNode = currentAudioRecordingDotNode
} else {
self.audioRecordingDotNode?.removeFromSupernode()
audioRecordingDotNode = AnimationNode(animation: "BinRed")
self.audioRecordingDotNode = audioRecordingDotNode
self.audioRecordingDotNodeDismissed = false
self.clippingNode.insertSubnode(audioRecordingDotNode, belowSubnode: self.menuButton)
audioRecordingDotNode.frame = dotFrame
self.animatingBinNode?.removeFromSupernode()
self.animatingBinNode = nil
}
var resumingRecording = false
animateDotAppearing = transition.isAnimated && !hideInfo
if let mediaRecordingState = mediaRecordingState {
if case .waitingForPreview = mediaRecordingState {
self.recordingPaused = true
animateDotAppearing = false
} else {
if self.recordingPaused {
self.recordingPaused = false
resumingRecording = true
if (audioRecordingDotNode.layer.animationKeys() ?? []).isEmpty {
animateDotAppearing = true
}
}
}
}
audioRecordingDotNode.bounds = CGRect(origin: .zero, size: dotFrame.size)
audioRecordingDotNode.position = dotFrame.center
if animateDotAppearing {
Queue.mainQueue().justDispatch {
audioRecordingDotNode.layer.animateScale(from: 0.3, to: 1, duration: 0.15, delay: 0, removeOnCompletion: false)
let animateDot = { [weak audioRecordingDotNode] in
if let audioRecordingDotNode, audioRecordingDotNode.layer.animation(forKey: "recording") == nil {
audioRecordingDotNode.layer.animateAlpha(from: CGFloat(audioRecordingDotNode.layer.presentation()?.opacity ?? 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")
}
})
}
}
if resumingRecording {
animateDot()
} else {
audioRecordingTimeNode.started = {
animateDot()
}
}
}
self.attachmentButton.layer.animateAlpha(from: CGFloat(self.attachmentButton.layer.presentation()?.opacity ?? 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.finishedTransitionToPreview = nil
var update = self.actionButtons.micButton.audioRecorder != nil || self.actionButtons.micButton.videoRecordingStatus != nil
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 self] in
guard let audioRecordingDotNode = audioRecordingDotNode, audioRecordingDotNode === self?.audioRecordingDotNode else { return }
self?.audioRecordingDotNode = nil
audioRecordingDotNode.layer.animateScale(from: 1.0, to: 0.3, duration: 0.15, delay: 0.0, removeOnCompletion: false)
audioRecordingDotNode.layer.animateAlpha(from: CGFloat(audioRecordingDotNode.layer.presentation()?.opacity ?? 1), to: 0.0, duration: 0.15, delay: 0.0, removeOnCompletion: false) { [weak audioRecordingDotNode] _ in
audioRecordingDotNode?.removeFromSupernode()
}
self?.attachmentButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: 0.0, removeOnCompletion: false)
self?.attachmentButton.layer.animateScale(from: 0.3, to: 1.0, duration: 0.15, delay: 0.0, removeOnCompletion: false)
}
if update && !self.audioRecordingDotNodeDismissed {
audioRecordingDotNode.layer.removeAllAnimations()
}
if self.isMediaDeleted {
if self.prevInputPanelNode is ChatRecordingPreviewInputPanelNode {
self.audioRecordingDotNode?.removeFromSupernode()
self.audioRecordingDotNode = nil
} else {
if !self.audioRecordingDotNodeDismissed {
audioRecordingDotNode.layer.removeAllAnimations()
}
audioRecordingDotNode.completion = dismissDotNode
audioRecordingDotNode.play()
update = true
}
} else {
dismissDotNode()
}
if update && !self.audioRecordingDotNodeDismissed {
self.audioRecordingDotNode?.layer.animatePosition(from: CGPoint(), to: CGPoint(x: leftMenuInset, y: 0.0), duration: 0.15, removeOnCompletion: false, additive: true)
self.audioRecordingDotNodeDismissed = true
}
}
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()
}
}
}
leftInset += leftMenuInset
transition.updateFrame(layer: self.attachmentButton.layer, frame: CGRect(origin: CGPoint(x: attachmentButtonX, y: hideOffset.y + panelHeight - minimalHeight), size: CGSize(width: 40.0, height: minimalHeight)))
transition.updateFrame(node: self.attachmentButtonDisabledNode, frame: self.attachmentButton.frame)
var composeButtonsOffset: CGFloat = 0.0
if self.extendedSearchLayout {
composeButtonsOffset = 44.0
textInputBackgroundWidthOffset = 36.0
}
self.updateCounterTextNode(transition: transition)
if inputHasText || self.extendedSearchLayout {
hideMicButton = true
}
self.updateActionButtons(hasText: inputHasText, hideMicButton: hideMicButton, animated: transition.isAnimated)
var actionButtonsSize = CGSize(width: 44.0, height: minimalHeight)
if let presentationInterfaceState = self.presentationInterfaceState {
var showTitle = false
if !self.actionButtons.sendContainerNode.alpha.isZero {
if let _ = presentationInterfaceState.sendPaidMessageStars {
showTitle = true
} else if case let .customChatContents(customChatContents) = interfaceState.subject {
switch customChatContents.kind {
case .postSuggestions:
showTitle = true
default:
break
}
}
}
actionButtonsSize = self.actionButtons.updateLayout(size: CGSize(width: 44.0, height: minimalHeight), isMediaInputExpanded: isMediaInputExpanded, showTitle: showTitle, currentMessageEffectId: presentationInterfaceState.interfaceState.sendMessageEffect, transition: transition, interfaceState: presentationInterfaceState)
}
let actionButtonsFrame = CGRect(origin: CGPoint(x: hideOffset.x + width - rightInset - actionButtonsSize.width + 1 - UIScreenPixel + composeButtonsOffset, y: hideOffset.y + panelHeight - minimalHeight), size: actionButtonsSize)
transition.updateFrame(node: self.actionButtons, frame: actionButtonsFrame)
if let (rect, containerSize) = self.absoluteRect {
self.actionButtons.updateAbsoluteRect(CGRect(x: rect.origin.x + actionButtonsFrame.origin.x, y: rect.origin.y + actionButtonsFrame.origin.y, width: actionButtonsFrame.width, height: actionButtonsFrame.height), within: containerSize, transition: transition)
}
let slowModeButtonFrame = CGRect(origin: CGPoint(x: hideOffset.x + width - rightInset - 5.0 - slowModeButtonSize.width + composeButtonsOffset, y: hideOffset.y + panelHeight - minimalHeight + 6.0), size: slowModeButtonSize)
transition.updateFrame(node: self.slowModeButton, frame: slowModeButtonFrame)
if let _ = interfaceState.inputTextPanelState.mediaRecordingState {
let text: String = interfaceState.strings.VoiceOver_MessageContextSend
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
if let self {
self.interfaceInteraction?.finishMediaRecording(.send(viewOnce: self.viewOnce))
}
return true
}
self.clippingNode.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)
var textFieldInsets = self.textFieldInsets(metrics: metrics)
if actionButtonsSize.width > 44.0 {
textFieldInsets.right = actionButtonsSize.width - 2.0
}
if additionalSideInsets.right > 0.0 {
textFieldInsets.right += additionalSideInsets.right / 3.0
}
self.actionButtons.micButton.isHidden = additionalSideInsets.right > 0.0
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)
}
var textInputViewRealInsets = UIEdgeInsets()
if let presentationInterfaceState = self.presentationInterfaceState {
textInputViewRealInsets = calculateTextFieldRealInsets(presentationInterfaceState: presentationInterfaceState, accessoryButtonsWidth: accessoryButtonsWidth)
}
let textInputFrame = CGRect(x: hideOffset.x + leftInset + textFieldInsets.left, y: hideOffset.y + textFieldInsets.top, width: baseWidth - textFieldInsets.left - textFieldInsets.right, height: panelHeight - textFieldInsets.top - textFieldInsets.bottom)
transition.updateFrame(node: self.textInputContainer, frame: textInputFrame)
transition.updateFrame(node: self.textInputContainerBackgroundNode, frame: CGRect(origin: CGPoint(), size: textInputFrame.size))
transition.updateAlpha(node: self.textInputContainer, alpha: audioRecordingItemsAlpha)
if let textInputNode = self.textInputNode {
textInputNode.textContainerInset = textInputViewRealInsets
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), height: textInputFrame.size.height - self.textInputViewInternalInsets.top - textInputViewInternalInsets.bottom))
let shouldUpdateLayout = textFieldFrame.size != textInputNode.frame.size
//transition.updateFrame(node: textInputNode, frame: textFieldFrame)
textInputNode.frame = textFieldFrame
textInputNode.updateLayout(size: textFieldFrame.size)
self.updateInputField(textInputFrame: textFieldFrame, transition: ComponentTransition(transition))
if shouldUpdateLayout {
textInputNode.layout()
}
}
if interfaceState.slowmodeState == nil || 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.clippingNode.insertSubnode(contextPlaceholderNode, aboveSubnode: self.textPlaceholderNode)
}
let _ = placeholderApply()
let placeholderTransition: ContainedViewLayoutTransition
if placeholderSize.size.width == contextPlaceholderNode.frame.width {
placeholderTransition = transition
} else {
placeholderTransition = .immediate
}
placeholderTransition.updateFrame(node: contextPlaceholderNode, frame: CGRect(origin: CGPoint(x: hideOffset.x + leftInset + textFieldInsets.left + self.textInputViewInternalInsets.left, y: hideOffset.y + textFieldInsets.top + self.textInputViewInternalInsets.top + 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, !isScheduledMessages && rightSlowModeInset.isZero {
let slowmodePlaceholderNode: ChatTextInputSlowmodePlaceholderNode
if let current = self.slowmodePlaceholderNode {
slowmodePlaceholderNode = current
} else {
slowmodePlaceholderNode = ChatTextInputSlowmodePlaceholderNode(theme: interfaceState.theme)
self.slowmodePlaceholderNode = slowmodePlaceholderNode
self.clippingNode.insertSubnode(slowmodePlaceholderNode, aboveSubnode: self.textPlaceholderNode)
}
let placeholderFrame = CGRect(origin: CGPoint(x: leftInset + textFieldInsets.left + self.textInputViewInternalInsets.left, y: textFieldInsets.top + self.textInputViewInternalInsets.top + 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()
}
if (interfaceState.slowmodeState != nil && rightSlowModeInset.isZero && !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
}
var nextButtonTopRight = CGPoint(x: hideOffset.x + width - rightInset - textFieldInsets.right - accessoryButtonInset - rightSlowModeInset, y: hideOffset.y + panelHeight - textFieldInsets.bottom - minimalInputHeight)
for (item, button) in self.accessoryItemButtons.reversed() {
let buttonSize = CGSize(width: button.buttonWidth, height: minimalInputHeight)
button.updateLayout(item: item, 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.supernode == nil {
self.clippingNode.addSubnode(button)
button.frame = buttonFrame.offsetBy(dx: -additionalOffset, dy: 0.0)
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
}
let textInputBackgroundFrame = CGRect(x: hideOffset.x + leftInset + textFieldInsets.left, y: hideOffset.y + textFieldInsets.top, width: baseWidth - textFieldInsets.left - textFieldInsets.right, height: panelHeight - textFieldInsets.top - textFieldInsets.bottom)
self.currentTextInputBackgroundWidthOffset = textInputBackgroundWidthOffset
transition.updateFrame(layer: self.textInputBackgroundNode.layer, frame: textInputBackgroundFrame)
transition.updateAlpha(node: self.textInputBackgroundNode, alpha: audioRecordingItemsAlpha)
let textPlaceholderSize: CGSize
let textPlaceholderMaxWidth: CGFloat = max(1.0, (nextButtonTopRight.x - textInputBackgroundFrame.minX) - 12.0)
if (updatedPlaceholder != nil && self.currentPlaceholder != updatedPlaceholder) || themeUpdated {
let currentPlaceholder = updatedPlaceholder ?? self.currentPlaceholder ?? ""
self.currentPlaceholder = currentPlaceholder
let baseFontSize = max(minInputFontSize, interfaceState.fontSize.baseDisplaySize)
let attributedPlaceholder = NSMutableAttributedString(string: currentPlaceholder, font:Font.regular(baseFontSize), textColor: interfaceState.theme.chat.inputPanel.inputPlaceholderColor)
if placeholderHasStar, let range = attributedPlaceholder.string.range(of: "#") {
attributedPlaceholder.addAttribute(.attachment, value: PresentationResourcesChat.chatPlaceholderStarIcon(interfaceState.theme)!, range: NSRange(range, in: attributedPlaceholder.string))
attributedPlaceholder.addAttribute(.foregroundColor, value: interfaceState.theme.chat.inputPanel.inputPlaceholderColor, range: NSRange(range, in: attributedPlaceholder.string))
attributedPlaceholder.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: attributedPlaceholder.string))
}
self.textPlaceholderNode.attributedText = attributedPlaceholder
self.textInputNode?.textView.accessibilityHint = currentPlaceholder
let placeholderSize = self.textPlaceholderNode.updateLayout(CGSize(width: textPlaceholderMaxWidth, 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)
}
textPlaceholderSize = placeholderSize
} else {
textPlaceholderSize = self.textPlaceholderNode.bounds.size
}
let textPlaceholderFrame: CGRect
if sendingTextDisabled {
textPlaceholderFrame = CGRect(origin: CGPoint(x: textInputBackgroundFrame.minX + floor((textInputBackgroundFrame.width - textPlaceholderSize.width) / 2.0), y: textFieldInsets.top + self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel), size: textPlaceholderSize)
let textLockIconNode: ASImageNode
var textLockIconTransition = transition
if let current = self.textLockIconNode {
textLockIconNode = current
} else {
textLockIconTransition = .immediate
textLockIconNode = ASImageNode()
self.textLockIconNode = textLockIconNode
self.textPlaceholderNode.addSubnode(textLockIconNode)
textLockIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/TextLockIcon"), color: interfaceState.theme.chat.inputPanel.inputPlaceholderColor)
}
if let image = textLockIconNode.image {
textLockIconTransition.updateFrame(node: textLockIconNode, frame: CGRect(origin: CGPoint(x: -image.size.width - 4.0, y: floor((textPlaceholderFrame.height - image.size.height) / 2.0)), size: image.size))
}
} else {
textPlaceholderFrame = CGRect(origin: CGPoint(x: hideOffset.x + leftInset + textFieldInsets.left + self.textInputViewInternalInsets.left, y: hideOffset.y + textFieldInsets.top + self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel), size: textPlaceholderSize)
if let textLockIconNode = self.textLockIconNode {
self.textLockIconNode = nil
textLockIconNode.removeFromSupernode()
}
}
transition.updateFrame(node: self.textPlaceholderNode, frame: textPlaceholderFrame)
let textPlaceholderAlpha: CGFloat = audioRecordingItemsAlpha
transition.updateAlpha(node: self.textPlaceholderNode, alpha: textPlaceholderAlpha)
if let removeAccessoryButtons = removeAccessoryButtons {
for button in removeAccessoryButtons {
let buttonFrame = CGRect(origin: CGPoint(x: button.frame.origin.x + additionalOffset, 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?.removeFromSupernode()
})
}
}
let mediaInputDisabled: Bool
if !interfaceState.voiceMessagesAvailable {
mediaInputDisabled = true
} else if interfaceState.hasActiveGroupCall {
mediaInputDisabled = true
} else if let channel = interfaceState.renderedPeer?.peer as? TelegramChannel, channel.hasBannedPermission(.banSendVoice, ignoreDefault: canBypassRestrictions) != nil, channel.hasBannedPermission(.banSendInstantVideos, ignoreDefault: canBypassRestrictions) != nil {
mediaInputDisabled = true
} else if let group = interfaceState.renderedPeer?.peer as? TelegramGroup, group.hasBannedPermission(.banSendVoice), group.hasBannedPermission(.banSendInstantVideos) {
mediaInputDisabled = true
} else {
mediaInputDisabled = false
}
self.actionButtons.micButton.fadeDisabled = mediaInputDisabled
var viewOnceIsVisible = false
if let recordingState = interfaceState.inputTextPanelState.mediaRecordingState {
if case let .audio(_, isLocked) = recordingState {
viewOnceIsVisible = isLocked
} else if case let .video(_, isLocked) = recordingState {
viewOnceIsVisible = isLocked
}
}
if let prevInputPanelNode = self.prevInputPanelNode {
prevInputPanelNode.frame = CGRect(origin: .zero, size: prevInputPanelNode.frame.size)
}
if let prevPreviewInputPanelNode = self.prevInputPanelNode as? ChatRecordingPreviewInputPanelNode {
self.prevInputPanelNode = nil
if !prevPreviewInputPanelNode.viewOnceButton.isHidden {
self.viewOnce = prevPreviewInputPanelNode.viewOnce
self.viewOnceButton.update(isSelected: prevPreviewInputPanelNode.viewOnce, animated: false)
self.viewOnceButton.layer.animatePosition(from: prevPreviewInputPanelNode.viewOnceButton.position, to: self.viewOnceButton.position, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in
})
}
let animateOutPreviewButton: (ASDisplayNode) -> Void = { button in
if button.alpha > 0.0 {
if let snapshotView = button.view.snapshotContentTree() {
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
snapshotView.removeFromSuperview()
})
snapshotView.layer.animateScale(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
self.viewForOverlayContent?.addSubview(snapshotView)
}
}
}
animateOutPreviewButton(prevPreviewInputPanelNode.viewOnceButton)
animateOutPreviewButton(prevPreviewInputPanelNode.recordMoreButton)
prevPreviewInputPanelNode.gestureRecognizer?.isEnabled = false
prevPreviewInputPanelNode.isUserInteractionEnabled = false
if self.isMediaDeleted {
func animatePosition(for previewLayer: CALayer) {
previewLayer.animatePosition(
from: previewLayer.position,
to: CGPoint(x: leftMenuInset.isZero ? previewLayer.position.x - 20 : leftMenuInset + previewLayer.frame.width / 2.0, y: previewLayer.position.y),
duration: 0.15
)
}
animatePosition(for: prevPreviewInputPanelNode.waveformBackgroundNode.layer)
animatePosition(for: prevPreviewInputPanelNode.waveformScrubberNode.layer)
animatePosition(for: prevPreviewInputPanelNode.durationLabel.layer)
animatePosition(for: prevPreviewInputPanelNode.playButton.layer)
if let view = prevPreviewInputPanelNode.scrubber.view {
animatePosition(for: view.layer)
}
}
func animateAlpha(for previewLayer: CALayer) {
previewLayer.animateAlpha(
from: 1.0,
to: 0.0,
duration: 0.15,
removeOnCompletion: false
)
}
animateAlpha(for: prevPreviewInputPanelNode.waveformBackgroundNode.layer)
animateAlpha(for: prevPreviewInputPanelNode.waveformScrubberNode.layer)
animateAlpha(for: prevPreviewInputPanelNode.durationLabel.layer)
animateAlpha(for: prevPreviewInputPanelNode.playButton.layer)
if let view = prevPreviewInputPanelNode.scrubber.view {
animateAlpha(for: view.layer)
}
let binNode = prevPreviewInputPanelNode.binNode
self.animatingBinNode = binNode
let dismissBin = { [weak self, weak prevPreviewInputPanelNode, weak binNode] in
if binNode?.supernode != nil {
prevPreviewInputPanelNode?.deleteButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, delay: 0.0, removeOnCompletion: false) { [weak prevPreviewInputPanelNode] _ in
if prevPreviewInputPanelNode?.supernode === self {
prevPreviewInputPanelNode?.removeFromSupernode()
}
}
prevPreviewInputPanelNode?.deleteButton.layer.animateScale(from: 1.0, to: 0.3, duration: 0.15, delay: 0.0, removeOnCompletion: false)
if isRecording {
self?.attachmentButton.layer.animateAlpha(from: 0.0, to: 0, duration: 0.01, delay: 0.0, removeOnCompletion: false)
self?.attachmentButton.layer.animateScale(from: 1, to: 0.3, duration: 0.01, delay: 0.0, removeOnCompletion: false)
} else {
self?.attachmentButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: 0.0, removeOnCompletion: false)
self?.attachmentButton.layer.animateScale(from: 0.3, to: 1.0, duration: 0.15, delay: 0.0, removeOnCompletion: false)
}
} else if prevPreviewInputPanelNode?.supernode === self {
prevPreviewInputPanelNode?.removeFromSupernode()
}
}
if self.isMediaDeleted {
Queue.mainQueue().after(0.5, {
self.isMediaDeleted = false
})
}
if self.isMediaDeleted && !isRecording {
self.attachmentButton.layer.animateAlpha(from: 0.0, to: 0, duration: 0.01, delay: 0.0, removeOnCompletion: false)
binNode.completion = dismissBin
binNode.play()
} else {
dismissBin()
}
prevPreviewInputPanelNode.deleteButton.layer.animatePosition(from: CGPoint(), to: CGPoint(x: leftMenuInset, y: 0.0), duration: 0.15, removeOnCompletion: false, additive: true)
prevPreviewInputPanelNode.sendButton.layer.animateScale(from: 1.0, to: 0.3, duration: 0.15, removeOnCompletion: false)
prevPreviewInputPanelNode.sendButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
self.actionButtons.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: 0, removeOnCompletion: false)
self.actionButtons.layer.animateScale(from: 0.3, to: 1.0, duration: 0.15, delay: 0, removeOnCompletion: false)
if hasMenuButton {
if isSendAsButton {
} else {
self.menuButton.alpha = 1.0
self.menuButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: 0, removeOnCompletion: false)
self.menuButton.transform = CATransform3DIdentity
self.menuButton.layer.animateScale(from: 0.3, to: 1.0, duration: 0.15, delay: 0, removeOnCompletion: false)
}
}
}
var clippingDelta: CGFloat = 0.0
if case let .media(_, _, focused) = interfaceState.inputMode, focused {
clippingDelta = -panelHeight
}
transition.updateFrame(node: self.clippingNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: panelHeight)))
transition.updateSublayerTransformOffset(layer: self.clippingNode.layer, offset: CGPoint(x: 0.0, y: clippingDelta))
let viewOnceSize = self.viewOnceButton.update(theme: interfaceState.theme)
let viewOnceButtonFrame = CGRect(origin: CGPoint(x: width - rightInset - 44.0 - UIScreenPixel, y: -152.0), size: viewOnceSize)
self.viewOnceButton.bounds = CGRect(origin: .zero, size: viewOnceButtonFrame.size)
transition.updatePosition(node: self.viewOnceButton, position: viewOnceButtonFrame.center)
if self.viewOnceButton.alpha.isZero && viewOnceIsVisible {
self.viewOnceButton.update(isSelected: self.viewOnce, animated: false)
}
transition.updateAlpha(node: self.viewOnceButton, alpha: viewOnceIsVisible ? 1.0 : 0.0)
transition.updateTransformScale(node: self.viewOnceButton, scale: viewOnceIsVisible ? 1.0 : 0.01)
if let user = interfaceState.renderedPeer?.peer as? TelegramUser, user.id != interfaceState.accountPeerId && user.botInfo == nil && interfaceState.sendPaidMessageStars == nil {
self.viewOnceButton.isHidden = false
} else {
self.viewOnceButton.isHidden = true
}
return panelHeight
}
@objc private func slowModeButtonPressed() {
self.interfaceInteraction?.openBoostToUnrestrict()
}
@objc private func viewOncePressed() {
guard let context = self.context, let interfaceState = self.presentationInterfaceState else {
return
}
self.viewOnce = !self.viewOnce
self.viewOnceButton.update(isSelected: self.viewOnce, animated: true)
self.tooltipController?.dismiss()
if self.viewOnce {
self.displayViewOnceTooltip(text: interfaceState.strings.Chat_PlayVoiceMessageOnceTooltip)
let _ = ApplicationSpecificNotice.incrementVoiceMessagesPlayOnceSuggestion(accountManager: context.sharedContext.accountManager, count: 3).startStandalone()
}
}
private func displayViewOnceTooltip(text: String) {
guard let context = self.context, let parentController = self.interfaceInteraction?.chatController() else {
return
}
let absoluteFrame = self.viewOnceButton.view.convert(self.viewOnceButton.bounds, to: parentController.view)
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX - 20.0, y: absoluteFrame.midY), size: CGSize())
let tooltipController = TooltipScreen(
account: context.account,
sharedContext: context.sharedContext,
text: .plain(text: text),
balancedTextLayout: true,
constrainWidth: 240.0,
style: .customBlur(UIColor(rgb: 0x18181a), 0.0),
arrowStyle: .small,
icon: .animation(name: "anim_autoremove_on", delay: 0.1, tintColor: nil),
location: .point(location, .right),
displayDuration: .default,
inset: 8.0,
cornerRadius: 8.0,
shouldDismissOnTouch: { _, _ in
return .ignore
}
)
self.tooltipController = tooltipController
parentController.present(tooltipController, in: .current)
}
override func canHandleTransition(from prevInputPanelNode: ChatInputPanelNode?) -> Bool {
return prevInputPanelNode is ChatRecordingPreviewInputPanelNode
}
func chatInputTextNodeDidUpdateText() {
if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState, let context = self.context {
let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize)
refreshChatTextInputAttributes(context: context, textView: textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: (self.context?.animatedEmojiStickersValue.keys).flatMap(Set.init) ?? Set(), emojiViewProvider: self.emojiViewProvider, makeCollapsedQuoteAttachment: { text, attributes in
return ChatInputTextCollapsedQuoteAttachmentImpl(text: text, attributes: attributes)
})
refreshChatTextInputTypingAttributes(textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize)
self.updateSpoiler()
let inputTextState = self.inputTextState
self.interfaceInteraction?.updateTextInputStateAndMode({ _, inputMode in return (inputTextState, inputMode) })
self.interfaceInteraction?.updateInputLanguage({ _ in return textInputNode.textInputMode?.primaryLanguage })
self.updateTextNodeText(animated: true)
self.updateCounterTextNode(transition: .immediate)
}
}
@objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) {
self.chatInputTextNodeDidUpdateText()
}
private func updateSpoiler() {
guard let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState else {
return
}
let textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor
var rects: [CGRect] = []
var customEmojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute, CGFloat)] = []
let fontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize)
if let attributedText = textInputNode.attributedText {
let beginning = textInputNode.textView.beginningOfDocument
attributedText.enumerateAttributes(in: NSMakeRange(0, attributedText.length), options: [], using: { attributes, range, _ in
if let _ = attributes[ChatTextInputAttributes.spoiler] {
func addSpoiler(startIndex: Int, endIndex: Int) {
if let start = textInputNode.textView.position(from: beginning, offset: startIndex), let end = textInputNode.textView.position(from: start, offset: endIndex - startIndex), let textRange = textInputNode.textView.textRange(from: start, to: end) {
let textRects = textInputNode.textView.selectionRects(for: textRange)
for textRect in textRects {
if textRect.rect.width > 1.0 && textRect.rect.size.height > 1.0 {
rects.append(textRect.rect.insetBy(dx: 1.0, dy: 1.0).offsetBy(dx: 0.0, dy: 1.0))
}
}
}
}
var startIndex: Int?
var currentIndex: Int?
let nsString = (attributedText.string as NSString)
nsString.enumerateSubstrings(in: range, options: .byComposedCharacterSequences) { substring, range, _, _ in
if let substring = substring, substring.rangeOfCharacter(from: .whitespacesAndNewlines) != nil {
if let currentStartIndex = startIndex {
startIndex = nil
let endIndex = range.location
addSpoiler(startIndex: currentStartIndex, endIndex: endIndex)
}
} else if startIndex == nil {
startIndex = range.location
}
currentIndex = range.location + range.length
}
if let currentStartIndex = startIndex, let currentIndex = currentIndex {
startIndex = nil
let endIndex = currentIndex
addSpoiler(startIndex: currentStartIndex, endIndex: endIndex)
}
}
if let value = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute {
if let start = textInputNode.textView.position(from: beginning, offset: range.location), let end = textInputNode.textView.position(from: start, offset: range.length), let textRange = textInputNode.textView.textRange(from: start, to: end) {
let textRects = textInputNode.textView.selectionRects(for: textRange)
for textRect in textRects {
var emojiFontSize = fontSize
if let font = attributes[.font] as? UIFont {
emojiFontSize = font.pointSize
}
customEmojiRects.append((textRect.rect, value, emojiFontSize))
break
}
}
}
})
}
if !rects.isEmpty {
let dustNode: InvisibleInkDustNode
if let current = self.dustNode {
dustNode = current
} else {
dustNode = InvisibleInkDustNode(textNode: nil, enableAnimations: self.context?.sharedContext.energyUsageSettings.fullTranslucency ?? true)
dustNode.alpha = self.spoilersRevealed ? 0.0 : 1.0
dustNode.isUserInteractionEnabled = false
textInputNode.textView.addSubview(dustNode.view)
self.dustNode = dustNode
}
dustNode.frame = CGRect(origin: CGPoint(), size: textInputNode.textView.contentSize)
dustNode.update(size: textInputNode.textView.contentSize, color: textColor, textColor: textColor, rects: rects, wordRects: rects)
} else if let dustNode = self.dustNode {
dustNode.removeFromSupernode()
self.dustNode = nil
}
if !customEmojiRects.isEmpty {
let customEmojiContainerView: CustomEmojiContainerView
if let current = self.customEmojiContainerView {
customEmojiContainerView = current
} else {
customEmojiContainerView = CustomEmojiContainerView(emojiViewProvider: { [weak self] emoji in
guard let strongSelf = self, let emojiViewProvider = strongSelf.emojiViewProvider else {
return nil
}
return emojiViewProvider(emoji)
})
customEmojiContainerView.isUserInteractionEnabled = false
textInputNode.textView.addSubview(customEmojiContainerView)
self.customEmojiContainerView = customEmojiContainerView
}
customEmojiContainerView.update(fontSize: fontSize, textColor: textColor, emojiRects: customEmojiRects)
} else if let customEmojiContainerView = self.customEmojiContainerView {
customEmojiContainerView.removeFromSuperview()
self.customEmojiContainerView = nil
}
}
private func updateSpoilersRevealed(animated: Bool = true) {
guard let textInputNode = self.textInputNode else {
return
}
let selectionRange = textInputNode.textView.selectedRange
var revealed = false
if let attributedText = textInputNode.attributedText {
attributedText.enumerateAttributes(in: NSMakeRange(0, attributedText.length), options: [], using: { attributes, range, _ in
if let _ = attributes[ChatTextInputAttributes.spoiler] {
if let _ = selectionRange.intersection(range) {
revealed = true
}
}
})
}
guard self.spoilersRevealed != revealed else {
return
}
self.spoilersRevealed = revealed
if revealed {
self.updateInternalSpoilersRevealed(true, animated: animated)
} else {
Queue.mainQueue().after(1.5, {
self.updateInternalSpoilersRevealed(false, animated: true)
})
}
}
private func updateInternalSpoilersRevealed(_ revealed: Bool, animated: Bool) {
guard self.spoilersRevealed == revealed, let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState, let context = self.context else {
return
}
let textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor
let accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor
let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize)
textInputNode.textView.isScrollEnabled = false
refreshChatTextInputAttributes(context: context, textView: textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: (self.context?.animatedEmojiStickersValue.keys).flatMap(Set.init) ?? Set(), emojiViewProvider: self.emojiViewProvider, makeCollapsedQuoteAttachment: { text, attributes in
return ChatInputTextCollapsedQuoteAttachmentImpl(text: text, attributes: attributes)
})
textInputNode.attributedText = textAttributedStringForStateText(context: context, stateText: self.inputTextState.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed, availableEmojis: (self.context?.animatedEmojiStickersValue.keys).flatMap(Set.init) ?? Set(), emojiViewProvider: self.emojiViewProvider, makeCollapsedQuoteAttachment: { text, attributes in
return ChatInputTextCollapsedQuoteAttachmentImpl(text: text, attributes: attributes)
})
if textInputNode.textView.subviews.count > 1, animated {
let containerView = textInputNode.textView.subviews[1]
if let canvasView = containerView.subviews.first {
if let snapshotView = canvasView.snapshotView(afterScreenUpdates: false) {
snapshotView.frame = canvasView.frame.offsetBy(dx: 0.0, dy: -textInputNode.textView.contentOffset.y)
textInputNode.view.insertSubview(snapshotView, at: 0)
canvasView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView, weak textInputNode] _ in
textInputNode?.textView.isScrollEnabled = false
snapshotView?.removeFromSuperview()
Queue.mainQueue().after(0.1) {
textInputNode?.textView.isScrollEnabled = true
}
})
}
}
}
Queue.mainQueue().after(0.1) {
textInputNode.textView.isScrollEnabled = true
}
if animated {
if revealed {
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear)
if let dustNode = self.dustNode {
transition.updateAlpha(node: dustNode, alpha: 0.0)
}
} else {
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear)
if let dustNode = self.dustNode {
transition.updateAlpha(node: dustNode, alpha: 1.0)
}
}
} else if let dustNode = self.dustNode {
dustNode.alpha = revealed ? 0.0 : 1.0
}
}
private struct EmojiSuggestionPosition: Equatable {
var range: NSRange
var value: String
}
private final class CurrentEmojiSuggestion {
var localPosition: CGPoint
var position: EmojiSuggestionPosition
let disposable: MetaDisposable
var value: [TelegramMediaFile]?
init(localPosition: CGPoint, position: EmojiSuggestionPosition, disposable: MetaDisposable, value: [TelegramMediaFile]?) {
self.localPosition = localPosition
self.position = position
self.disposable = disposable
self.value = value
}
}
private var currentEmojiSuggestion: CurrentEmojiSuggestion?
private var currentEmojiSuggestionView: ComponentHostView<Empty>?
private var dismissedEmojiSuggestionPosition: EmojiSuggestionPosition?
private func updateInputField(textInputFrame: CGRect, transition: ComponentTransition) {
guard let textInputNode = self.textInputNode, let context = self.context else {
return
}
var hasTracking = false
var hasTrackingView = false
if textInputNode.selectedRange.length == 0, textInputNode.selectedRange.location > 0, let attributedText = textInputNode.textView.attributedText {
let selectedSubstring = attributedText.attributedSubstring(from: NSRange(location: 0, length: textInputNode.selectedRange.location))
if let lastCharacter = selectedSubstring.string.last, String(lastCharacter).isSingleEmoji {
let queryLength = (String(lastCharacter) as NSString).length
if selectedSubstring.attribute(ChatTextInputAttributes.customEmoji, at: selectedSubstring.length - queryLength, effectiveRange: nil) == nil {
let beginning = textInputNode.textView.beginningOfDocument
let characterRange = NSRange(location: selectedSubstring.length - queryLength, length: queryLength)
let start = textInputNode.textView.position(from: beginning, offset: selectedSubstring.length - queryLength)
let end = textInputNode.textView.position(from: beginning, offset: selectedSubstring.length)
if let start = start, let end = end, let textRange = textInputNode.textView.textRange(from: start, to: end) {
let selectionRects = textInputNode.textView.selectionRects(for: textRange)
let emojiSuggestionPosition = EmojiSuggestionPosition(range: characterRange, value: String(lastCharacter))
hasTracking = true
if let trackingRect = selectionRects.first?.rect {
let trackingPosition = CGPoint(x: trackingRect.midX, y: trackingRect.minY)
if self.dismissedEmojiSuggestionPosition == emojiSuggestionPosition {
} else {
hasTrackingView = true
var beginRequest = false
let suggestionContext: CurrentEmojiSuggestion
if let current = self.currentEmojiSuggestion, current.position.value == emojiSuggestionPosition.value {
suggestionContext = current
} else {
beginRequest = true
suggestionContext = CurrentEmojiSuggestion(localPosition: trackingPosition, position: emojiSuggestionPosition, disposable: MetaDisposable(), value: nil)
self.currentEmojiSuggestion?.disposable.dispose()
self.currentEmojiSuggestion = suggestionContext
}
suggestionContext.localPosition = trackingPosition
suggestionContext.position = emojiSuggestionPosition
self.dismissedEmojiSuggestionPosition = nil
if beginRequest {
suggestionContext.disposable.set((EmojiSuggestionsComponent.suggestionData(context: context, isSavedMessages: self.presentationInterfaceState?.chatLocation.peerId == self.context?.account.peerId, query: String(lastCharacter))
|> deliverOnMainQueue).startStrict(next: { [weak self, weak suggestionContext] result in
guard let strongSelf = self, let suggestionContext = suggestionContext, strongSelf.currentEmojiSuggestion === suggestionContext else {
return
}
suggestionContext.value = result
if let textInputNode = strongSelf.textInputNode {
strongSelf.updateInputField(textInputFrame: textInputNode.frame, transition: .immediate)
}
}).strict())
}
}
}
}
}
}
}
if !hasTracking {
self.dismissedEmojiSuggestionPosition = nil
}
if let currentEmojiSuggestion = self.currentEmojiSuggestion, let value = currentEmojiSuggestion.value, value.isEmpty {
hasTrackingView = false
}
if !textInputNode.textView.isFirstResponder {
hasTrackingView = false
}
if !hasTrackingView {
if let currentEmojiSuggestion = self.currentEmojiSuggestion {
self.currentEmojiSuggestion = nil
currentEmojiSuggestion.disposable.dispose()
}
if let currentEmojiSuggestionView = self.currentEmojiSuggestionView {
self.currentEmojiSuggestionView = nil
currentEmojiSuggestionView.alpha = 0.0
currentEmojiSuggestionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak currentEmojiSuggestionView] _ in
currentEmojiSuggestionView?.removeFromSuperview()
})
}
}
if let context = self.context, let theme = self.theme, let viewForOverlayContent = self.viewForOverlayContent, let presentationContext = self.presentationContext, let currentEmojiSuggestion = self.currentEmojiSuggestion, let value = currentEmojiSuggestion.value {
let currentEmojiSuggestionView: ComponentHostView<Empty>
if let current = self.currentEmojiSuggestionView {
currentEmojiSuggestionView = current
} else {
currentEmojiSuggestionView = ComponentHostView<Empty>()
self.currentEmojiSuggestionView = currentEmojiSuggestionView
viewForOverlayContent.addSubview(currentEmojiSuggestionView)
currentEmojiSuggestionView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
self.installEmojiSuggestionPreviewGesture(hostView: currentEmojiSuggestionView)
}
let globalPosition = textInputNode.textView.convert(currentEmojiSuggestion.localPosition, to: self.view)
let sideInset: CGFloat = 16.0
let viewSize = currentEmojiSuggestionView.update(
transition: .immediate,
component: AnyComponent(EmojiSuggestionsComponent(
context: context,
userLocation: .other,
theme: EmojiSuggestionsComponent.Theme(theme: theme, backgroundColor: theme.list.itemBlocksBackgroundColor),
animationCache: presentationContext.animationCache,
animationRenderer: presentationContext.animationRenderer,
files: value,
action: { [weak self] file in
guard let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction, let currentEmojiSuggestion = strongSelf.currentEmojiSuggestion else {
return
}
AudioServicesPlaySystemSound(0x450)
interfaceInteraction.updateTextInputStateAndMode { textInputState, inputMode in
let inputText = NSMutableAttributedString(attributedString: textInputState.inputText)
var text: String?
var emojiAttribute: ChatTextInputTextCustomEmojiAttribute?
loop: for attribute in file.attributes {
switch attribute {
case let .CustomEmoji(_, _, displayText, _):
text = displayText
emojiAttribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: file.fileId.id, file: file)
break loop
default:
break
}
}
if let emojiAttribute = emojiAttribute, let text = text {
let replacementText = NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: emojiAttribute])
let range = currentEmojiSuggestion.position.range
let previousText = inputText.attributedSubstring(from: range)
inputText.replaceCharacters(in: range, with: replacementText)
var replacedUpperBound = range.lowerBound
while true {
if inputText.attributedSubstring(from: NSRange(location: 0, length: replacedUpperBound)).string.hasSuffix(previousText.string) {
let replaceRange = NSRange(location: replacedUpperBound - previousText.length, length: previousText.length)
if replaceRange.location < 0 {
break
}
let adjacentString = inputText.attributedSubstring(from: replaceRange)
if adjacentString.string != previousText.string || adjacentString.attribute(ChatTextInputAttributes.customEmoji, at: 0, effectiveRange: nil) != nil {
break
}
inputText.replaceCharacters(in: replaceRange, with: NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: emojiAttribute.interactivelySelectedFromPackId, fileId: emojiAttribute.fileId, file: emojiAttribute.file)]))
replacedUpperBound = replaceRange.lowerBound
} else {
break
}
}
let selectionPosition = range.lowerBound + (replacementText.string as NSString).length
return (ChatTextInputState(inputText: inputText, selectionRange: selectionPosition ..< selectionPosition), inputMode)
}
return (textInputState, inputMode)
}
if let textInputNode = strongSelf.textInputNode {
strongSelf.dismissedEmojiSuggestionPosition = currentEmojiSuggestion.position
strongSelf.updateInputField(textInputFrame: textInputNode.frame, transition: .immediate)
}
}
)),
environment: {},
containerSize: CGSize(width: self.bounds.width - sideInset * 2.0, height: 100.0)
)
let viewFrame = CGRect(origin: CGPoint(x: min(self.bounds.width - sideInset - viewSize.width, max(sideInset, floor(globalPosition.x - viewSize.width / 2.0))), y: globalPosition.y - 2.0 - viewSize.height), size: viewSize)
currentEmojiSuggestionView.frame = viewFrame
if let componentView = currentEmojiSuggestionView.componentView as? EmojiSuggestionsComponent.View {
componentView.adjustBackground(relativePositionX: floor(globalPosition.x - viewFrame.minX))
}
}
}
private func updateCounterTextNode(transition: ContainedViewLayoutTransition) {
var inputTextMaxLength: Int32?
if let presentationInterfaceState = self.presentationInterfaceState {
if let editMessage = presentationInterfaceState.interfaceState.editMessage, let inputTextMaxLengthValue = editMessage.inputTextMaxLength {
inputTextMaxLength = inputTextMaxLengthValue
} else if case let .customChatContents(customChatContents) = presentationInterfaceState.subject, case .businessLinkSetup = customChatContents.kind {
inputTextMaxLength = 4096
}
}
if let presentationInterfaceState = self.presentationInterfaceState, let textInputNode = self.textInputNode, let inputTextMaxLength {
let textCount = Int32(textInputNode.textView.text.count)
let counterColor: UIColor = textCount > inputTextMaxLength ? presentationInterfaceState.theme.chat.inputPanel.panelControlDestructiveColor : presentationInterfaceState.theme.chat.inputPanel.panelControlColor
let remainingCount = max(-999, inputTextMaxLength - textCount)
let counterText = remainingCount >= 5 ? "" : "\(remainingCount)"
self.counterTextNode.attributedText = NSAttributedString(string: counterText, font: counterFont, textColor: counterColor)
} else {
self.counterTextNode.attributedText = NSAttributedString(string: "", font: counterFont, textColor: .black)
}
if let (width, leftInset, rightInset, _, _, maxHeight, metrics, _, _) = self.validLayout {
var composeButtonsOffset: CGFloat = 0.0
if self.extendedSearchLayout {
composeButtonsOffset = 44.0
}
let (_, textFieldHeight) = self.calculateTextFieldMetrics(width: width - leftInset - rightInset - self.leftMenuInset - self.rightSlowModeInset + self.currentTextInputBackgroundWidthOffset, maxHeight: maxHeight, metrics: metrics)
let panelHeight = self.panelHeight(textFieldHeight: textFieldHeight, metrics: metrics)
var textFieldMinHeight: CGFloat = 33.0
if let presentationInterfaceState = self.presentationInterfaceState {
textFieldMinHeight = calclulateTextFieldMinHeight(presentationInterfaceState, metrics: metrics)
}
let minimalHeight: CGFloat = 14.0 + textFieldMinHeight
let counterSize = self.counterTextNode.updateLayout(CGSize(width: 44.0, height: 44.0))
let actionButtonsOriginX = width - rightInset - 43.0 - UIScreenPixel + composeButtonsOffset
let counterFrame = CGRect(origin: CGPoint(x: actionButtonsOriginX, y: panelHeight - minimalHeight - counterSize.height + 3.0), size: CGSize(width: width - actionButtonsOriginX - rightInset, height: counterSize.height))
transition.updateFrame(node: self.counterTextNode, frame: counterFrame)
}
}
private func installEmojiSuggestionPreviewGesture(hostView: UIView) {
let peekRecognizer = PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point in
guard let self else {
return nil
}
return self.emojiSuggestionPeekContentAtPoint(point: point)
}, present: { [weak self] content, sourceView, sourceRect in
guard let strongSelf = self, let context = strongSelf.context else {
return nil
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = PeekController(presentationData: presentationData, content: content, sourceView: {
return (sourceView, sourceRect)
})
//strongSelf.peekController = controller
strongSelf.interfaceInteraction?.presentGlobalOverlayController(controller, nil)
return controller
}, updateContent: { [weak self] content in
guard let strongSelf = self else {
return
}
let _ = strongSelf
})
hostView.addGestureRecognizer(peekRecognizer)
}
private func emojiSuggestionPeekContentAtPoint(point: CGPoint) -> Signal<(UIView, CGRect, PeekControllerContent)?, NoError>? {
guard let presentationInterfaceState = self.presentationInterfaceState else {
return nil
}
guard let chatPeerId = presentationInterfaceState.renderedPeer?.peer?.id else {
return nil
}
guard let context = self.context else {
return nil
}
var maybeFile: TelegramMediaFile?
var maybeItemLayer: CALayer?
if let currentEmojiSuggestionView = self.currentEmojiSuggestionView?.componentView as? EmojiSuggestionsComponent.View {
if let (itemLayer, file) = currentEmojiSuggestionView.item(at: point) {
maybeFile = file
maybeItemLayer = itemLayer
}
}
guard let file = maybeFile else {
return nil
}
guard let itemLayer = maybeItemLayer else {
return nil
}
let _ = chatPeerId
let _ = file
let _ = itemLayer
var collectionId: ItemCollectionId?
for attribute in file.attributes {
if case let .CustomEmoji(_, _, _, packReference) = attribute {
switch packReference {
case let .id(id, _):
collectionId = ItemCollectionId(namespace: Namespaces.ItemCollection.CloudEmojiPacks, id: id)
default:
break
}
}
}
var bubbleUpEmojiOrStickersets: [ItemCollectionId] = []
if let collectionId {
bubbleUpEmojiOrStickersets.append(collectionId)
}
let accountPeerId = context.account.peerId
let _ = bubbleUpEmojiOrStickersets
let _ = context
let _ = accountPeerId
return context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: accountPeerId))
|> map { peer -> Bool in
var hasPremium = false
if case let .user(user) = peer, user.isPremium {
hasPremium = true
}
return hasPremium
}
|> deliverOnMainQueue
|> map { [weak self, weak itemLayer] hasPremium -> (UIView, CGRect, PeekControllerContent)? in
guard let strongSelf = self, let itemLayer = itemLayer else {
return nil
}
let _ = strongSelf
let _ = itemLayer
var menuItems: [ContextMenuItem] = []
menuItems.removeAll()
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let _ = presentationData
var isLocked = false
if !hasPremium {
isLocked = file.isPremiumEmoji
if isLocked && chatPeerId == context.account.peerId {
isLocked = false
}
}
if let interaction = strongSelf.interfaceInteraction {
let _ = interaction
let sendEmoji: (TelegramMediaFile) -> Void = { file in
guard let self else {
return
}
guard let controller = (self.interfaceInteraction?.chatController() as? ChatControllerImpl) else {
return
}
var text = "."
var emojiAttribute: ChatTextInputTextCustomEmojiAttribute?
loop: for attribute in file.attributes {
switch attribute {
case let .CustomEmoji(_, _, displayText, stickerPackReference):
text = displayText
var packId: ItemCollectionId?
if case let .id(id, _) = stickerPackReference {
packId = ItemCollectionId(namespace: Namespaces.ItemCollection.CloudEmojiPacks, id: id)
}
emojiAttribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: packId, fileId: file.fileId.id, file: file)
break loop
default:
break
}
}
if let emojiAttribute {
controller.controllerInteraction?.sendEmoji(text, emojiAttribute, true)
}
}
let setStatus: (TelegramMediaFile) -> Void = { file in
guard let self, let context = self.context else {
return
}
guard let controller = (self.interfaceInteraction?.chatController() as? ChatControllerImpl) else {
return
}
let _ = context.engine.accountData.setEmojiStatus(file: file, expirationDate: nil).startStandalone()
var animateInAsReplacement = false
animateInAsReplacement = false
/*if let currentUndoOverlayController = strongSelf.currentUndoOverlayController {
currentUndoOverlayController.dismissWithCommitActionAndReplacementAnimation()
strongSelf.currentUndoOverlayController = nil
animateInAsReplacement = true
}*/
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let undoController = UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, loop: true, title: nil, text: presentationData.strings.EmojiStatus_AppliedText, undoText: nil, customAction: nil), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { _ in return false })
//strongSelf.currentUndoOverlayController = controller
controller.controllerInteraction?.presentController(undoController, nil)
}
let copyEmoji: (TelegramMediaFile) -> Void = { file in
var text = "."
var emojiAttribute: ChatTextInputTextCustomEmojiAttribute?
loop: for attribute in file.attributes {
switch attribute {
case let .CustomEmoji(_, _, displayText, _):
text = displayText
emojiAttribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: file.fileId.id, file: file)
break loop
default:
break
}
}
if let _ = emojiAttribute {
storeMessageTextInPasteboard(text, entities: [MessageTextEntity(range: 0 ..< (text as NSString).length, type: .CustomEmoji(stickerPack: nil, fileId: file.fileId.id))])
}
}
menuItems.append(.action(ContextMenuActionItem(text: presentationData.strings.EmojiPreview_SendEmoji, icon: { theme in
if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Download"), color: theme.actionSheet.primaryTextColor) {
return generateImage(image.size, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
if let cgImage = image.cgImage {
context.draw(cgImage, in: CGRect(origin: CGPoint(), size: size))
}
})
} else {
return nil
}
}, action: { _, f in
sendEmoji(file)
f(.default)
})))
menuItems.append(.action(ContextMenuActionItem(text: presentationData.strings.EmojiPreview_SetAsStatus, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Smile"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
f(.default)
guard let strongSelf = self else {
return
}
if hasPremium {
setStatus(file)
} else {
var replaceImpl: ((ViewController) -> Void)?
let controller = PremiumDemoScreen(context: context, subject: .animatedEmoji, action: {
let controller = PremiumIntroScreen(context: context, source: .animatedEmoji)
replaceImpl?(controller)
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
strongSelf.interfaceInteraction?.getNavigationController()?.pushViewController(controller)
}
})))
menuItems.append(.action(ContextMenuActionItem(text: presentationData.strings.EmojiPreview_CopyEmoji, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
copyEmoji(file)
f(.default)
})))
}
if menuItems.isEmpty {
return nil
}
let content = StickerPreviewPeekContent(context: context, theme: presentationData.theme, strings: presentationData.strings, item: .pack(file), isLocked: isLocked, menu: menuItems, openPremiumIntro: { [weak self] in
guard let self else {
return
}
guard let interfaceInteraction = self.interfaceInteraction else {
return
}
let _ = self
let _ = interfaceInteraction
let controller = PremiumIntroScreen(context: context, source: .stickers)
//let _ = controller
interfaceInteraction.getNavigationController()?.pushViewController(controller)
})
let _ = content
//return nil
return (strongSelf.view, itemLayer.convert(itemLayer.bounds, to: strongSelf.view.layer), content)
}
}
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
}
var isScheduledMessages = false
if case .scheduledMessages = self.presentationInterfaceState?.subject {
isScheduledMessages = true
}
if let interfaceState = self.presentationInterfaceState {
if (interfaceState.slowmodeState != nil && !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
}
}
let _ = hideMicButton
// 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
var keepSendButtonEnabled = self.keepSendButtonEnabled
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
}
if case let .customChatContents(customChatContents) = presentationInterfaceState.subject {
switch customChatContents.kind {
case .hashTagSearch:
break
case .quickReplyMessageInput:
break
case .businessLinkSetup:
keepSendButtonEnabled = true
case .postSuggestions:
break
}
}
}
var animateWithBounce = false
if self.extendedSearchLayout {
hideMicButton = true
if !self.actionButtons.sendContainerNode.alpha.isZero {
self.actionButtons.sendContainerNode.alpha = 0.0
self.actionButtons.sendButtonRadialStatusNode?.alpha = 0.0
self.actionButtons.updateAccessibility()
if animated {
self.actionButtons.animatingSendButton = true
self.actionButtons.sendContainerNode.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.sendContainerNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.2)
self.actionButtons.sendButtonRadialStatusNode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
self.actionButtons.sendButtonRadialStatusNode?.layer.animateScale(from: 1.0, to: 0.2, 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)
}
}
let hasSlowModeButton = self.rightSlowModeInset > 0.0
if hasSlowModeButton {
hideMicButton = true
if self.slowModeButton.alpha.isZero {
self.slowModeButton.alpha = 1.0
if animated {
self.slowModeButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
if animateWithBounce {
self.slowModeButton.layer.animateSpring(from: NSNumber(value: Float(0.1)), to: NSNumber(value: Float(1.0)), keyPath: "transform.scale", duration: 0.6)
} else {
self.slowModeButton.layer.animateScale(from: 0.2, to: 1.0, duration: 0.25)
}
}
}
} else {
if !self.slowModeButton.alpha.isZero {
self.slowModeButton.alpha = 0.0
if animated {
self.slowModeButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
}
}
}
if (hasText || keepSendButtonEnabled && !mediaInputIsActive && !hasSlowModeButton) {
hideMicButton = true
if self.actionButtons.sendContainerNode.alpha.isZero && self.rightSlowModeInset.isZero {
self.actionButtons.sendContainerNode.alpha = 1.0
self.actionButtons.sendButtonRadialStatusNode?.alpha = 1.0
self.actionButtons.updateAccessibility()
if animated {
self.actionButtons.sendContainerNode.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.sendContainerNode.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.sendContainerNode.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.sendContainerNode.alpha.isZero {
self.actionButtons.sendContainerNode.alpha = 0.0
self.actionButtons.sendButtonRadialStatusNode?.alpha = 0.0
self.actionButtons.updateAccessibility()
if animated {
self.actionButtons.animatingSendButton = true
self.actionButtons.sendContainerNode.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)
}
}
}
}
let hideExpandMediaInput = hideMicButton
if mediaInputIsActive {
hideMicButton = true
}
if let interfaceState = self.presentationInterfaceState {
if case let .customChatContents(customChatContents) = interfaceState.subject {
switch customChatContents.kind {
case .hashTagSearch:
break
case .quickReplyMessageInput:
break
case .businessLinkSetup:
hideMicButton = true
case .postSuggestions:
break
}
}
}
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 && !hideExpandMediaInput {
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, _, additionalSideInsets, maxHeight, metrics, _, _) = self.validLayout {
let (_, textFieldHeight) = self.calculateTextFieldMetrics(width: width - leftInset - rightInset - additionalSideInsets.right - self.leftMenuInset - self.rightSlowModeInset + self.currentTextInputBackgroundWidthOffset, maxHeight: maxHeight, metrics: metrics)
let panelHeight = self.panelHeight(textFieldHeight: textFieldHeight, metrics: metrics)
if !self.bounds.size.height.isEqual(to: panelHeight) {
self.updateHeight(animated)
} else {
if let textInputNode = self.textInputNode {
self.updateInputField(textInputFrame: textInputNode.frame, transition: .immediate)
}
}
}
}
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()
}
}
func chatInputTextNodeShouldReturn() -> Bool {
if self.actionButtons.sendButton.supernode != nil && !self.actionButtons.sendButton.isHidden && !self.actionButtons.sendContainerNode.alpha.isZero {
self.sendButtonPressed()
}
return false
}
@objc func editableTextNodeShouldReturn(_ editableTextNode: ASEditableTextNode) -> Bool {
return self.chatInputTextNodeShouldReturn()
}
private func applyUpdateSendButtonIcon() {
if let interfaceState = self.presentationInterfaceState {
var sendButtonHasApplyIcon = interfaceState.interfaceState.editMessage != nil
if case let .customChatContents(customChatContents) = interfaceState.subject {
switch customChatContents.kind {
case .hashTagSearch:
break
case .quickReplyMessageInput:
break
case .businessLinkSetup:
sendButtonHasApplyIcon = true
case .postSuggestions:
break
}
}
if sendButtonHasApplyIcon != self.actionButtons.sendButtonHasApplyIcon {
self.actionButtons.sendButtonHasApplyIcon = sendButtonHasApplyIcon
if self.actionButtons.sendButtonHasApplyIcon {
self.actionButtons.sendButton.setImage(PresentationResourcesChat.chatInputPanelApplyIconImage(interfaceState.theme), for: [])
} else {
if case .scheduledMessages = interfaceState.subject {
self.actionButtons.sendButton.setImage(PresentationResourcesChat.chatInputPanelScheduleIconImage(interfaceState.theme), for: [])
} else {
self.actionButtons.sendButton.setImage(PresentationResourcesChat.chatInputPanelSendIconImage(interfaceState.theme), for: [])
}
}
}
}
}
func chatInputTextNodeDidChangeSelection(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.hide()
}
let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize)
refreshChatTextInputTypingAttributes(textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize)
self.updateSpoilersRevealed()
self.updateInputField(textInputFrame: textInputNode.frame, transition: .immediate)
}
}
@objc func editableTextNodeDidChangeSelection(_ editableTextNode: ASEditableTextNode, fromSelectedRange: NSRange, toSelectedRange: NSRange, dueToEditing: Bool) {
self.chatInputTextNodeDidChangeSelection(dueToEditing: dueToEditing)
}
func chatInputTextNodeDidBeginEditing() {
guard let interfaceInteraction = self.interfaceInteraction, let presentationInterfaceState = self.presentationInterfaceState else {
return
}
switch presentationInterfaceState.inputMode {
case .text:
break
case .media:
break
case .inputButtons, .none:
if self.textInputNode?.textView.inputView == nil {
interfaceInteraction.updateInputModeAndDismissedButtonKeyboardMessageId({ state in
return (.text, state.keyboardButtonsMessage?.id)
})
}
}
self.inputMenu.activate()
if let touchDownGestureRecognizer = self.touchDownGestureRecognizer {
self.textInputNode?.view.addGestureRecognizer(touchDownGestureRecognizer)
}
}
@objc func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) {
self.chatInputTextNodeDidBeginEditing()
}
var skipPresentationInterfaceStateUpdate = false
func chatInputTextNodeDidFinishEditing() {
guard let editableTextNode = self.textInputNode else {
return
}
self.storedInputLanguage = editableTextNode.textInputMode?.primaryLanguage
self.inputMenu.deactivate()
self.dismissedEmojiSuggestionPosition = nil
if let presentationInterfaceState = self.presentationInterfaceState, !self.skipPresentationInterfaceStateUpdate {
if let peer = presentationInterfaceState.renderedPeer?.peer as? TelegramUser, peer.botInfo != nil, let keyboardButtonsMessage = presentationInterfaceState.keyboardButtonsMessage, let keyboardMarkup = keyboardButtonsMessage.visibleButtonKeyboardMarkup, keyboardMarkup.flags.contains(.persistent) {
self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId { _ in
return (.inputButtons(persistent: true), nil)
}
} else {
switch presentationInterfaceState.inputMode {
case .text:
self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId { _ in
return (.none, nil)
}
case .media:
break
default:
break
}
}
}
if let touchDownGestureRecognizer = self.touchDownGestureRecognizer {
self.textInputNode?.view.removeGestureRecognizer(touchDownGestureRecognizer)
}
}
func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) {
self.chatInputTextNodeDidFinishEditing()
}
func chatInputTextNodeBackspaceWhileEmpty() {
}
func editableTextNodeTarget(forAction action: Selector) -> ASEditableTextNodeTargetForAction? {
if action == makeSelectorFromString("_accessibilitySpeak:") {
if case .format = self.inputMenu.state {
return ASEditableTextNodeTargetForAction(target: nil)
} else if let textInputNode = self.textInputNode, textInputNode.selectedRange.length > 0 {
return ASEditableTextNodeTargetForAction(target: self)
} else {
return ASEditableTextNodeTargetForAction(target: nil)
}
} else if action == makeSelectorFromString("_accessibilitySpeakSpellOut:") {
if case .format = self.inputMenu.state {
return ASEditableTextNodeTargetForAction(target: nil)
} else if let textInputNode = self.textInputNode, textInputNode.selectedRange.length > 0 {
return nil
} else {
return ASEditableTextNodeTargetForAction(target: nil)
}
}
else if action == makeSelectorFromString("_accessibilitySpeakLanguageSelection:") || action == makeSelectorFromString("_accessibilityPauseSpeaking:") || action == makeSelectorFromString("_accessibilitySpeakSentence:") {
return ASEditableTextNodeTargetForAction(target: nil)
} else if action == makeSelectorFromString("_showTextStyleOptions:") {
if #available(iOS 16.0, *) {
return ASEditableTextNodeTargetForAction(target: nil)
} else {
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(_:)) || action == #selector(self.formatAttributesSpoiler(_:)) || action == #selector(self.formatAttributesQuote(_:)) || action == #selector(self.formatAttributesCodeBlock(_:)) {
if case .format = self.inputMenu.state {
if action == #selector(self.formatAttributesSpoiler(_:)), let selectedRange = self.textInputNode?.selectedRange {
var intersectsMonospace = false
self.inputTextState.inputText.enumerateAttributes(in: selectedRange, options: [], using: { attributes, _, _ in
if let _ = attributes[ChatTextInputAttributes.monospace] {
intersectsMonospace = true
}
})
if !intersectsMonospace {
return ASEditableTextNodeTargetForAction(target: self)
} else {
return ASEditableTextNodeTargetForAction(target: nil)
}
} else if action == #selector(self.formatAttributesQuote(_:)), let selectedRange = self.textInputNode?.selectedRange {
let _ = selectedRange
return ASEditableTextNodeTargetForAction(target: self)
} else if action == #selector(self.formatAttributesCodeBlock(_:)), let selectedRange = self.textInputNode?.selectedRange {
let _ = selectedRange
return ASEditableTextNodeTargetForAction(target: self)
} else if action == #selector(self.formatAttributesMonospace(_:)), let selectedRange = self.textInputNode?.selectedRange {
var intersectsSpoiler = false
self.inputTextState.inputText.enumerateAttributes(in: selectedRange, options: [], using: { attributes, _, _ in
if let _ = attributes[ChatTextInputAttributes.spoiler] {
intersectsSpoiler = true
}
})
if !intersectsSpoiler {
return ASEditableTextNodeTargetForAction(target: self)
} else {
return ASEditableTextNodeTargetForAction(target: nil)
}
} else {
return ASEditableTextNodeTargetForAction(target: self)
}
} else {
return ASEditableTextNodeTargetForAction(target: nil)
}
}
if case .format = self.inputMenu.state {
return ASEditableTextNodeTargetForAction(target: nil)
}
return nil
}
var suggestedActionCounter: Int = 0
@available(iOS 13.0, *)
func chatInputTextNodeMenu(forTextRange textRange: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu {
guard let editableTextNode = self.textInputNode else {
return UIMenu(children: [])
}
var actions = suggestedActions
if #available(iOS 16.0, *) {
if let index = actions.firstIndex(where: { $0.description.contains("identifier = com.apple.menu.replace;") }), let subMenu = actions[index] as? UIMenu {
var filteredChildren = subMenu.children
if let subIndex = filteredChildren.firstIndex(where: { $0.description.contains("identifier = com.apple.menu.autofill;") }) {
filteredChildren.remove(at: subIndex)
}
actions[index] = UIMenu(title: subMenu.title, subtitle: subMenu.subtitle, image: subMenu.image, identifier: subMenu.identifier, options: subMenu.options, children: filteredChildren)
}
}
if editableTextNode.attributedText == nil || editableTextNode.attributedText!.length == 0 || editableTextNode.selectedRange.length == 0 {
} else {
var children: [UIAction] = []
var hasSpoilers = true
if self.presentationInterfaceState?.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat {
hasSpoilers = false
}
if hasSpoilers {
children.append(UIAction(title: self.strings?.TextFormat_Quote ?? "Quote", image: nil) { [weak self] (action) in
if let strongSelf = self {
strongSelf.formatAttributesQuote(strongSelf)
}
})
}
if hasSpoilers {
children.append(UIAction(title: self.strings?.TextFormat_Spoiler ?? "Spoiler", image: nil) { [weak self] (action) in
if let strongSelf = self {
strongSelf.formatAttributesSpoiler(strongSelf)
}
})
}
children.append(contentsOf: [
UIAction(title: self.strings?.TextFormat_Bold ?? "Bold", image: nil) { [weak self] (action) in
if let strongSelf = self {
strongSelf.formatAttributesBold(strongSelf)
}
},
UIAction(title: self.strings?.TextFormat_Italic ?? "Italic", image: nil) { [weak self] (action) in
if let strongSelf = self {
strongSelf.formatAttributesItalic(strongSelf)
}
},
UIAction(title: self.strings?.TextFormat_Monospace ?? "Monospace", image: nil) { [weak self] (action) in
if let strongSelf = self {
strongSelf.formatAttributesMonospace(strongSelf)
}
},
UIAction(title: self.strings?.TextFormat_Link ?? "Link", image: nil) { [weak self] (action) in
if let strongSelf = self {
strongSelf.formatAttributesLink(strongSelf)
}
},
UIAction(title: self.strings?.TextFormat_Strikethrough ?? "Strikethrough", image: nil) { [weak self] (action) in
if let strongSelf = self {
strongSelf.formatAttributesStrikethrough(strongSelf)
}
},
UIAction(title: self.strings?.TextFormat_Underline ?? "Underline", image: nil) { [weak self] (action) in
if let strongSelf = self {
strongSelf.formatAttributesUnderline(strongSelf)
}
}
] as [UIAction])
children.append(UIAction(title: self.strings?.TextFormat_Code ?? "Code", image: nil) { [weak self] (action) in
if let strongSelf = self {
strongSelf.formatAttributesCodeBlock(strongSelf)
}
})
let formatMenu = UIMenu(title: self.strings?.TextFormat_Format ?? "Format", image: nil, children: children)
actions.insert(formatMenu, at: 1)
}
return UIMenu(children: actions)
}
@available(iOS 16.0, *)
func editableTextNodeMenu(_ editableTextNode: ASEditableTextNode, forTextRange textRange: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu {
return chatInputTextNodeMenu(forTextRange: textRange, suggestedActions: suggestedActions)
}
private var currentSpeechHolder: SpeechSynthesizerHolder?
@objc func _accessibilitySpeak(_ sender: Any) {
var text = ""
self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
text = current.inputText.attributedSubstring(from: NSMakeRange(current.selectionRange.lowerBound, current.selectionRange.count)).string
return (current, inputMode)
}
if let context = self.context {
if let speechHolder = speakText(context: context, text: text) {
speechHolder.completion = { [weak self, weak speechHolder] in
if let strongSelf = self, strongSelf.currentSpeechHolder == speechHolder {
strongSelf.currentSpeechHolder = nil
}
}
self.currentSpeechHolder = speechHolder
}
}
if #available(iOS 13.0, *) {
UIMenuController.shared.hideMenu()
} else {
UIMenuController.shared.isMenuVisible = false
UIMenuController.shared.update()
}
}
@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, value: nil), inputMode)
}
}
@objc func formatAttributesItalic(_ sender: Any) {
self.inputMenu.back()
self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.italic, value: nil), inputMode)
}
}
@objc func formatAttributesMonospace(_ sender: Any) {
self.inputMenu.back()
self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.monospace, value: nil), 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, value: nil), inputMode)
}
}
@objc func formatAttributesUnderline(_ sender: Any) {
self.inputMenu.back()
self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.underline, value: nil), inputMode)
}
}
@objc func formatAttributesQuote(_ sender: Any) {
self.inputMenu.back()
self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .quote, isCollapsed: false)), inputMode)
}
}
@objc func formatAttributesCodeBlock(_ sender: Any) {
self.inputMenu.back()
self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .code(language: nil), isCollapsed: false)), inputMode)
}
}
@objc func formatAttributesSpoiler(_ sender: Any) {
self.inputMenu.back()
var animated = false
if let attributedText = self.textInputNode?.attributedText {
attributedText.enumerateAttributes(in: NSMakeRange(0, attributedText.length), options: [], using: { attributes, _, _ in
if let _ = attributes[ChatTextInputAttributes.spoiler] {
animated = true
}
})
}
self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.spoiler, value: nil), inputMode)
}
self.updateSpoilersRevealed(animated: animated)
}
func chatInputTextNode(shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
guard let editableTextNode = self.textInputNode, let context = self.context else {
return false
}
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
}
}
}
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(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize)
}
let cleanReplacementString = textAttributedStringForStateText(context: context, stateText: NSAttributedString(string: cleanText), fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed, availableEmojis: (self.context?.animatedEmojiStickersValue.keys).flatMap(Set.init) ?? Set(), emojiViewProvider: self.emojiViewProvider, makeCollapsedQuoteAttachment: { text, attributes in
return ChatInputTextCollapsedQuoteAttachmentImpl(text: text, attributes: attributes)
})
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 editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
return self.chatInputTextNode(shouldChangeTextIn: range, replacementText: text)
}
func chatInputTextNodeShouldCopy() -> 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 editableTextNodeShouldCopy(_ editableTextNode: ASEditableTextNode) -> Bool {
return self.chatInputTextNodeShouldCopy()
}
public func chatInputTextNodeShouldRespondToAction(action: Selector) -> Bool {
return true
}
public func chatInputTextNodeTargetForAction(action: Selector) -> ChatInputTextNode.TargetForAction? {
if let target = self.editableTextNodeTarget(forAction: action) {
return ChatInputTextNode.TargetForAction(target: target.target)
} else {
return nil
}
}
func chatInputTextNodeShouldPaste() -> Bool {
let pasteboard = UIPasteboard.general
var attributedString: NSAttributedString?
if let data = pasteboard.data(forPasteboardType: "private.telegramtext"), let value = chatInputStateStringFromAppSpecificString(data: data) {
attributedString = value
} else if let data = pasteboard.data(forPasteboardType: "public.rtf") {
attributedString = chatInputStateStringFromRTF(data, type: NSAttributedString.DocumentType.rtf)
} else if let data = pasteboard.data(forPasteboardType: "com.apple.flat-rtfd") {
if let _ = pasteboard.data(forPasteboardType: "com.apple.notes.richtext"), DeviceModel.current.isIpad, let htmlData = pasteboard.data(forPasteboardType: "public.html") {
attributedString = chatInputStateStringFromRTF(htmlData, type: NSAttributedString.DocumentType.html)
} else {
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 if let data = pasteboard.data(forPasteboardType: "public.mpeg-4") {
self.paste(.video(data))
return false
} else if let data = pasteboard.data(forPasteboardType: "public.heics") {
self.paste(.animatedSticker(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 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(image) && aspectRatio > 0.2) {
self.paste(.sticker(image, isMemoji))
return false
}
}
if !images.isEmpty {
self.paste(.images(images))
return false
}
}
return true
}
@objc func editableTextNodeShouldPaste(_ editableTextNode: ASEditableTextNode) -> Bool {
return self.chatInputTextNodeShouldPaste()
}
@objc func sendButtonPressed() {
if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState, let editMessage = presentationInterfaceState.interfaceState.editMessage, let inputTextMaxLength = editMessage.inputTextMaxLength {
let textCount = Int32(textInputNode.textView.text.count)
let remainingCount = inputTextMaxLength - textCount
if remainingCount < 0 {
textInputNode.layer.addShakeAnimation()
self.hapticFeedback.error()
return
}
}
self.sendMessage()
}
@objc func sendAsAvatarButtonPressed() {
self.interfaceInteraction?.openSendAsPeer(self.sendAsAvatarReferenceNode, nil)
}
@objc func menuButtonPressed() {
self.hapticFeedback.impact(.light)
guard let presentationInterfaceState = self.presentationInterfaceState else {
return
}
if let sendAsPeers = presentationInterfaceState.sendAsPeers, !sendAsPeers.isEmpty {
self.interfaceInteraction?.updateShowSendAsPeers { value in
return !value
}
} else if case let .webView(title, url) = presentationInterfaceState.botMenuButton {
let willShow = !(self.presentationInterfaceState?.showWebView ?? false)
if willShow || "".isEmpty {
self.interfaceInteraction?.openWebView(title, url, false, .menu)
} else {
self.interfaceInteraction?.updateShowWebView { _ in
return false
}
}
} else {
self.interfaceInteraction?.updateShowCommands { value in
return !value
}
}
}
@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.sendingTextDisabled {
return
}
if self.textInputNode == nil {
self.loadTextInputNode()
}
if !self.switching {
self.textInputNode?.becomeFirstResponder()
}
}
private var switching = false
func ensureFocusedOnTap() {
if self.textInputNode == nil {
self.loadTextInputNode()
}
if !self.switching {
self.switching = true
self.textInputNode?.becomeFirstResponder()
self.switchToTextInputIfNeeded?()
self.switching = false
}
}
func backwardsDeleteText() {
guard let textInputNode = self.textInputNode else {
return
}
textInputNode.textView.deleteBackward()
}
@objc func expandButtonPressed() {
self.toggleExpandMediaInput?()
/*self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in
if case let .media(mode, expanded, focused) = state.inputMode {
if let _ = expanded {
return (.media(mode: mode, expanded: nil, focused: focused), state.interfaceState.messageActionsState.closedButtonKeyboardMessageId)
} else {
return (.media(mode: mode, expanded: .content, focused: focused), 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 .input(isEnabled, inputMode), let .botInput(isEnabled, inputMode):
switch inputMode {
case .keyboard:
self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in
return (.text, state.keyboardButtonsMessage?.id)
})
case .stickers, .emoji:
if isEnabled {
self.interfaceInteraction?.openStickers()
} else {
self.interfaceInteraction?.displayRestrictedInfo(.stickers, .tooltip)
}
case .bot:
self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in
return (.inputButtons(persistent: state.keyboardButtonsMessage?.visibleButtonKeyboardMarkup?.flags.contains(.persistent) ?? false), 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()
case .gift:
self.interfaceInteraction?.openPremiumGift()
}
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
}
}
if self.bounds.contains(point), let textInputNode = self.textInputNode, let currentEmojiSuggestion = self.currentEmojiSuggestion, let currentEmojiSuggestionView = self.currentEmojiSuggestionView {
if let result = currentEmojiSuggestionView.hitTest(self.view.convert(point, to: currentEmojiSuggestionView), with: event) {
return result
}
self.dismissedEmojiSuggestionPosition = currentEmojiSuggestion.position
self.updateInputField(textInputFrame: textInputNode.frame, transition: .immediate)
}
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 frameForMenuButton() -> CGRect? {
if !self.menuButton.alpha.isZero {
return self.menuButton.frame
}
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 let .input(_, inputMode) = item, case .stickers = inputMode {
return button.frame.insetBy(dx: 0.0, dy: 6.0)
}
}
return nil
}
func frameForEmojiButton() -> CGRect? {
for (item, button) in self.accessoryItemButtons {
if case let .input(_, inputMode) = item, case .emoji = inputMode {
return button.frame.insetBy(dx: 0.0, dy: 6.0)
}
}
return nil
}
func frameForGiftButton() -> CGRect? {
for (item, button) in self.accessoryItemButtons {
if case .gift = item {
return button.frame.insetBy(dx: 0.0, dy: 6.0)
}
}
return nil
}
func makeSnapshotForTransition() -> ChatMessageTransitionNodeImpl.Source.TextInput? {
guard let backgroundImage = self.transparentTextInputBackgroundImage else {
return nil
}
guard let textInputNode = self.textInputNode else {
return nil
}
let backgroundView = UIImageView(image: backgroundImage)
backgroundView.frame = self.textInputBackgroundNode.frame
let caretColor = textInputNode.textView.tintColor
textInputNode.textView.tintColor = .clear
guard let contentView = textInputNode.view.snapshotView(afterScreenUpdates: true) else {
textInputNode.textView.tintColor = caretColor
return nil
}
textInputNode.textView.tintColor = caretColor
contentView.frame = textInputNode.frame
return ChatMessageTransitionNodeImpl.Source.TextInput(
backgroundView: backgroundView,
contentView: contentView,
sourceRect: self.view.convert(self.bounds, to: nil),
scrollOffset: textInputNode.textView.contentOffset.y
)
}
func makeAttachmentMenuTransition(accessoryPanelNode: ASDisplayNode?) -> AttachmentController.InputPanelTransition {
return AttachmentController.InputPanelTransition(inputNode: self, accessoryPanelNode: accessoryPanelNode, menuButtonNode: self.menuButton, menuButtonBackgroundNode: self.menuButtonBackgroundNode, menuIconNode: self.menuButtonIconNode, menuTextNode: self.menuButtonTextNode, prepareForDismiss: { self.menuButtonIconNode.enqueueState(.app, animated: false) })
}
}
private enum MenuIconNodeState: Equatable {
case menu
case app
case close
}
private final class MenuIconNode: ManagedAnimationNode {
private let duration: Double = 0.33
fileprivate var iconState: MenuIconNodeState = .menu
init() {
super.init(size: CGSize(width: 30.0, height: 30.0))
self.trackTo(item: ManagedAnimationItem(source: .local("anim_menuclose"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01))
}
func enqueueState(_ state: MenuIconNodeState, animated: Bool) {
guard self.iconState != state else {
return
}
let previousState = self.iconState
self.iconState = state
switch previousState {
case .close:
switch state {
case .menu:
if animated {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_closemenu"), frames: .range(startFrame: 0, endFrame: 20), duration: self.duration))
} else {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_menuclose"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01))
}
case .app:
if animated {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_webview"), frames: .range(startFrame: 0, endFrame: 22), duration: self.duration))
} else {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_webview"), frames: .range(startFrame: 22, endFrame: 22), duration: 0.01))
}
case .close:
break
}
case .menu:
switch state {
case .close:
if animated {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_menuclose"), frames: .range(startFrame: 0, endFrame: 20), duration: self.duration))
} else {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_menuclose"), frames: .range(startFrame: 20, endFrame: 20), duration: 0.01))
}
case .app:
self.trackTo(item: ManagedAnimationItem(source: .local("anim_webview"), frames: .range(startFrame: 22, endFrame: 22), duration: 0.01))
case .menu:
break
}
case .app:
switch state {
case .close:
if animated {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_webview"), frames: .range(startFrame: 22, endFrame: 0), duration: self.duration))
} else {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_webview"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01))
}
case .menu:
self.trackTo(item: ManagedAnimationItem(source: .local("anim_menuclose"), frames: .range(startFrame: 0, endFrame: 20), duration: 0.01))
case .app:
break
}
}
}
}
private func generateClearImage(color: UIColor) -> UIImage? {
return generateImage(CGSize(width: 17.0, height: 17.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(color.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
context.setBlendMode(.copy)
context.setStrokeColor(UIColor.clear.cgColor)
context.setLineCap(.round)
context.setLineWidth(1.66)
context.move(to: CGPoint(x: 6.0, y: 6.0))
context.addLine(to: CGPoint(x: 11.0, y: 11.0))
context.strokePath()
context.move(to: CGPoint(x: size.width - 6.0, y: 6.0))
context.addLine(to: CGPoint(x: size.width - 11.0, y: 11.0))
context.strokePath()
})
}
private final class BoostSlowModeButton: HighlightTrackingButtonNode {
let containerNode: ASDisplayNode
let backgroundNode: ASImageNode
let textNode: ImmediateAnimatedCountLabelNode
let iconNode: ASImageNode
private var updateTimer: SwiftSignalKit.Timer?
var requestUpdate: () -> Void = {}
override init(pointerStyle: PointerStyle? = nil) {
self.containerNode = ASDisplayNode()
self.backgroundNode = ASImageNode()
self.backgroundNode.displaysAsynchronously = false
self.backgroundNode.clipsToBounds = true
self.backgroundNode.image = generateGradientImage(size: CGSize(width: 100.0, height: 2.0), scale: 1.0, colors: [UIColor(rgb: 0x9076ff), UIColor(rgb: 0xbc6de8)], locations: [0.0, 1.0], direction: .horizontal)
self.iconNode = ASImageNode()
self.iconNode.displaysAsynchronously = false
self.iconNode.image = generateClearImage(color: .white)
self.textNode = ImmediateAnimatedCountLabelNode()
self.textNode.alwaysOneDirection = true
self.textNode.isUserInteractionEnabled = false
super.init(pointerStyle: pointerStyle)
self.addSubnode(self.containerNode)
self.containerNode.addSubnode(self.backgroundNode)
self.containerNode.addSubnode(self.iconNode)
self.containerNode.addSubnode(self.textNode)
self.highligthedChanged = { [weak self] highlighted in
if let self {
if highlighted {
self.containerNode.layer.animateScale(from: 1.0, to: 0.75, duration: 0.4, removeOnCompletion: false)
} else if let presentationLayer = self.containerNode.layer.presentation() {
self.containerNode.layer.animateScale(from: CGFloat((presentationLayer.value(forKeyPath: "transform.scale.y") as? NSNumber)?.floatValue ?? 1.0), to: 1.0, duration: 0.25, removeOnCompletion: false)
}
}
}
}
func update(size: CGSize, interfaceState: ChatPresentationInterfaceState) -> CGSize {
var text = ""
if let slowmodeState = interfaceState.slowmodeState {
let relativeTimestamp: CGFloat
switch slowmodeState.variant {
case let .timestamp(validUntilTimestamp):
let timestamp = CGFloat(Date().timeIntervalSince1970)
relativeTimestamp = CGFloat(validUntilTimestamp) - timestamp
case .pendingMessages:
relativeTimestamp = CGFloat(slowmodeState.timeout)
}
self.updateTimer?.invalidate()
if relativeTimestamp >= 0.0 {
text = stringForDuration(Int32(relativeTimestamp))
self.updateTimer = SwiftSignalKit.Timer(timeout: 1.0 / 60.0, repeat: false, completion: { [weak self] in
self?.requestUpdate()
}, queue: .mainQueue())
self.updateTimer?.start()
}
} else {
self.updateTimer?.invalidate()
self.updateTimer = nil
}
let font = Font.with(size: 15.0, design: .round, weight: .semibold, traits: [.monospacedNumbers])
let textColor = UIColor.white
var segments: [AnimatedCountLabelNode.Segment] = []
var textCount = 0
for char in text {
if let intValue = Int(String(char)) {
segments.append(.number(intValue, NSAttributedString(string: String(char), font: font, textColor: textColor)))
} else {
segments.append(.text(textCount, NSAttributedString(string: String(char), font: font, textColor: textColor)))
textCount += 1
}
}
self.textNode.segments = segments
let textSize = self.textNode.updateLayout(size: CGSize(width: 200.0, height: 100.0), animated: true)
let totalSize = CGSize(width: textSize.width > 0.0 ? textSize.width + 38.0 : 33.0, height: 33.0)
self.containerNode.bounds = CGRect(origin: .zero, size: totalSize)
self.containerNode.position = CGPoint(x: totalSize.width / 2.0, y: totalSize.height / 2.0)
self.backgroundNode.frame = CGRect(origin: .zero, size: totalSize)
self.backgroundNode.cornerRadius = totalSize.height / 2.0
self.textNode.frame = CGRect(origin: CGPoint(x: 9.0, y: floorToScreenPixels((totalSize.height - textSize.height) / 2.0)), size: textSize)
if let icon = self.iconNode.image {
self.iconNode.frame = CGRect(origin: CGPoint(x: totalSize.width - icon.size.width - 7.0, y: floorToScreenPixels((totalSize.height - icon.size.height) / 2.0)), size: icon.size)
}
return totalSize
}
}