Files
Telegram-iOS/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift
T
isaac b20cd3502b Postbox -> TelegramEngine waves 11-14: stateManager collapse + BUILD-dep sweep
Wave 11: ActionSheetPeerItem de-Postboxed. Revisits wave-1 abandonment.
`postbox: Postbox, network: Network` init params collapse to
`stateManager: AccountStateManager`; avatar setPeer call routes via
`item.stateManager.postbox` / `.network`. Module never names Postbox.
Sole caller (ShareController.swift:1146) migrated in place.

Wave 12: HorizontalPeerItem de-Postboxed (same pattern). Ripples the
collapse up into ChatListSearchRecentPeersNode's public init
(`postbox:/network:` -> `stateManager:`). That module still imports
Postbox for PostboxViewKey/UnreadMessageCountsView internals but its
public surface simplifies. 3 external caller sites migrated.

Wave 13: AttachmentTextInputPanelNode minor cleanup. Module was already
Postbox-free at source level (wave 6) but carried a dead BUILD dep and
had 2 raw `peerId?.namespace == Namespaces.Peer.SecretChat` checks.
Both now use existing `PeerId.isSecretChat` extension in TelegramCore.

Wave 14: BUILD-dep sweep mirroring wave 6's source sweep. 98 modules
had `//submodules/Postbox:Postbox` (or `//submodules/Postbox`) BUILD
deps despite no source file importing Postbox since wave 6. Single
iteration, zero restores -- Bazel Swift requires source-level `import`
for symbol resolution, so redundant BUILD deps are pure metadata.

Net: 110 files, +116/-149. Build verified green (debug_sim_arm64).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 20:26:58 +02:00

2409 lines
117 KiB
Swift

import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import MobileCoreServices
import TelegramPresentationData
import TextFormat
import AccountContext
import TouchDownGesture
import ActivityIndicator
import Speak
import ObjCRuntimeUtils
import LegacyComponents
import InvisibleInkDustNode
import TextInputMenu
import ChatPresentationInterfaceState
import Pasteboard
import EmojiTextAttachmentView
import ComponentFlow
import LottieAnimationComponent
import AnimationCache
import MultiAnimationRenderer
import TextNodeWithEntities
import ChatInputTextNode
import ChatEntityKeyboardInputNode
private let counterFont = Font.with(size: 14.0, design: .regular, traits: [.monospacedNumbers])
private let minInputFontSize: CGFloat = 5.0
private func calclulateTextFieldMinHeight(_ presentationInterfaceState: ChatPresentationInterfaceState, glass: Bool = false, 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)
}
if glass {
result = max(38.0, result)
}
return result
}
private func calculateTextFieldRealInsets(_ presentationInterfaceState: ChatPresentationInterfaceState, glass: Bool = false) -> UIEdgeInsets {
let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize)
let top: CGFloat
let bottom: CGFloat
if glass {
top = 4.0
bottom = 1.0
} else {
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
}
}
return UIEdgeInsets(top: 4.5 + top, left: 0.0, bottom: 5.5 + bottom, right: 32.0)
}
private var currentTextInputBackgroundImage: (UIColor, UIColor, CGFloat, UIImage)?
private func textInputBackgroundImage(backgroundColor: UIColor?, inputBackgroundColor: UIColor?, strokeColor: UIColor, diameter: CGFloat, caption: Bool) -> UIImage? {
if let backgroundColor = backgroundColor, let current = currentTextInputBackgroundImage {
if current.0.isEqual(backgroundColor) && current.1.isEqual(strokeColor) && current.2.isEqual(to: diameter) {
return current.3
}
}
let image = generateImage(CGSize(width: diameter, height: diameter), rotatedContext: { size, context in
context.clear(CGRect(x: 0.0, y: 0.0, width: diameter, height: diameter))
if caption {
context.setBlendMode(.normal)
context.setFillColor(strokeColor.cgColor)
} else 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))
if !caption {
context.setBlendMode(.normal)
context.setStrokeColor(strokeColor.cgColor)
let strokeWidth: CGFloat = 1.0
context.setLineWidth(strokeWidth)
context.strokeEllipse(in: CGRect(x: strokeWidth / 2.0, y: strokeWidth / 2.0, width: diameter - strokeWidth, height: diameter - strokeWidth))
}
})?.stretchableImage(withLeftCapWidth: Int(diameter) / 2, topCapHeight: Int(diameter) / 2)
if let image = image {
if let backgroundColor = backgroundColor {
currentTextInputBackgroundImage = (backgroundColor, strokeColor, diameter, image)
}
return image
} else {
return nil
}
}
private class CaptionEditableTextNode: ChatInputTextNode {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let previousAlpha = self.alpha
self.alpha = 1.0
let result = super.hitTest(point, with: event)
self.alpha = previousAlpha
return result
}
}
private enum AttachmentTextInputMode {
case text
case emoji
}
final class CustomEmojiContainerView: UIView {
private let emojiViewProvider: (ChatTextInputTextCustomEmojiAttribute) -> UIView?
private var emojiLayers: [InlineStickerItemLayer.Key: UIView] = [:]
init(emojiViewProvider: @escaping (ChatTextInputTextCustomEmojiAttribute) -> UIView?) {
self.emojiViewProvider = emojiViewProvider
super.init(frame: CGRect())
}
required init(coder: NSCoder) {
preconditionFailure()
}
func update(emojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)]) {
var nextIndexById: [Int64: Int] = [:]
var validKeys = Set<InlineStickerItemLayer.Key>()
for (rect, emoji) in emojiRects {
let index: Int
if let nextIndex = nextIndexById[emoji.fileId] {
index = nextIndex
} else {
index = 0
}
nextIndexById[emoji.fileId] = index + 1
let key = InlineStickerItemLayer.Key(id: emoji.fileId, index: index)
let view: UIView
if let current = self.emojiLayers[key] {
view = current
} else if let newView = self.emojiViewProvider(emoji) {
view = newView
self.addSubview(newView)
self.emojiLayers[key] = view
} else {
continue
}
let size = CGSize(width: 24.0, height: 24.0)
view.frame = CGRect(origin: CGPoint(x: floor(rect.midX - size.width / 2.0), y: floor(rect.midY - size.height / 2.0)), size: size)
validKeys.insert(key)
}
var removeKeys: [InlineStickerItemLayer.Key] = []
for (key, view) in self.emojiLayers {
if !validKeys.contains(key) {
removeKeys.append(key)
view.removeFromSuperview()
}
}
for key in removeKeys {
self.emojiLayers.removeValue(forKey: key)
}
}
}
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
)
)
}
public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, ASEditableTextNodeDelegate, ChatInputTextNodeDelegate {
private let context: AccountContext
private let glass: Bool
private let isCaption: Bool
private let isAttachment: Bool
private let customEmojiAvailable: Bool
private let presentController: (ViewController) -> Void
private let presentInGlobalOverlay: (ViewController) -> Void
private let getNavigationController: () -> NavigationController?
public var textPlaceholderNode: ImmediateTextNode
private let textInputContainerBackgroundNode: ASImageNode
public let textInputContainer: ASDisplayNode
public var textInputNode: ChatInputTextNode?
private var dustNode: InvisibleInkDustNode?
private var customEmojiContainerView: CustomEmojiContainerView?
private var oneLineNode: TextNodeWithEntities
private var oneLineNodeAttributedText: NSAttributedString?
private var oneLineDustNode: InvisibleInkDustNode?
let textInputBackgroundNode: ASDisplayNode
let textInputBackgroundImageNode: ASImageNode
private var transparentTextInputBackgroundImage: UIImage?
private let actionButtons: AttachmentTextInputActionButtonsNode
private let counterTextNode: ImmediateTextNode
private var aiButton: (button: HighlightTrackingButton, icon: UIImageView)?
private var heightDependentAiButtonAlpha: CGFloat = 0.0
public var isAIEnabled: Bool = false
public var opaqueActionButtons: ASDisplayNode {
return self.actionButtons
}
public let inputModeView: ComponentHostView<Empty>
private var validLayout: (width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, keyboardHeight: CGFloat, additionalSideInsets: UIEdgeInsets, textFieldMaxHeight: CGFloat, availableHeight: CGFloat, metrics: LayoutMetrics, isSecondary: Bool)?
private var currentInputMode: AttachmentTextInputMode = .text
private var currentAdditionalInputHeight: CGFloat = 0.0
private var currentSafeAreaInset: UIEdgeInsets = .zero
private var currentContainerBottomInset: CGFloat = 0.0
private var usesContainerLayout = false
private var currentIsCaptionAbove = false
private var currentHeight: CGFloat?
private var isTransitioningToTextKeyboard = false
private let inputMediaNodeDataPromise = Promise<ChatEntityKeyboardInputNode.InputData>()
private var inputMediaNodeData: ChatEntityKeyboardInputNode.InputData?
private var inputMediaNodeDataDisposable: Disposable?
private var inputMediaNodeStateContext = ChatEntityKeyboardInputNode.StateContext()
private var inputMediaInteraction: ChatEntityKeyboardInputNode.Interaction?
private var inputMediaNode: ChatEntityKeyboardInputNode?
public var sendMessage: (AttachmentTextInputPanelSendMode, ChatSendMessageActionSheetController.SendParameters?) -> Void = { _, _ in }
public var invokeAICompose: (() -> Void)?
public var updateHeight: (Bool) -> Void = { _ in }
private var updatingInputState = false
private var currentPlaceholder: String?
public var effectivePresentationInterfaceState: (() -> ChatPresentationInterfaceState?)?
private var presentationInterfaceState: ChatPresentationInterfaceState?
private var initializedPlaceholder = false
private let inputMenu: TextInputMenu
private var theme: PresentationTheme?
private var strings: PresentationStrings?
private let hapticFeedback = HapticFeedback()
public 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
}
}
}
public var interfaceInteraction: ChatPanelInterfaceInteraction?
public func animateIn(transition: ContainedViewLayoutTransition) {
self.actionButtons.animateIn(transition: transition)
}
public func updateSendButtonEnabled(_ enabled: Bool, animated: Bool) {
self.actionButtons.isUserInteractionEnabled = enabled
let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate
transition.updateAlpha(node: self.actionButtons, alpha: enabled ? 1.0 : 0.3)
}
public func updateInputTextState(_ state: ChatTextInputState, animated: Bool) {
if state.inputText.length != 0 && self.textInputNode == nil {
self.loadTextInputNode()
}
if let textInputNode = self.textInputNode, let _ = self.presentationInterfaceState, !self.skipUpdate {
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: self.context, stateText: state.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(self.context.animatedEmojiStickersValue.keys), emojiViewProvider: self.emojiViewProvider, makeCollapsedQuoteAttachment: { text, attributes in
return ChatInputTextCollapsedQuoteAttachmentImpl(text: text, attributes: attributes)
})
textInputNode.selectedRange = NSMakeRange(state.selectionRange.lowerBound, state.selectionRange.count)
self.updatingInputState = false
self.updateTextNodeText(animated: animated)
self.updateSpoiler()
}
}
public 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()
}
}
}
public func caption() -> NSAttributedString {
return self.textInputNode?.attributedText ?? NSAttributedString()
}
private let textInputViewInternalInsets = UIEdgeInsets(top: 1.0, left: 13.0, bottom: 1.0, right: 13.0)
private var spoilersRevealed = false
public var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?
private let animationCache: AnimationCache
private let animationRenderer: MultiAnimationRenderer
private var maxCaptionLength: Int32?
public init(context: AccountContext, presentationInterfaceState: ChatPresentationInterfaceState, glass: Bool = false, isCaption: Bool = false, isAttachment: Bool = false, isScheduledMessages: Bool = false, customEmojiAvailable: Bool, presentController: @escaping (ViewController) -> Void, presentInGlobalOverlay: @escaping (ViewController) -> Void, getNavigationController: @escaping () -> NavigationController?) {
self.context = context
self.presentationInterfaceState = presentationInterfaceState
self.glass = glass
self.isCaption = isCaption
self.isAttachment = isAttachment
self.customEmojiAvailable = customEmojiAvailable
self.presentController = presentController
self.presentInGlobalOverlay = presentInGlobalOverlay
self.getNavigationController = getNavigationController
self.animationCache = context.animationCache
self.animationRenderer = context.animationRenderer
var hasSpoilers = true
if presentationInterfaceState.chatLocation.peerId?.isSecretChat == true {
hasSpoilers = false
}
self.inputMenu = TextInputMenu(hasSpoilers: hasSpoilers)
self.textInputContainerBackgroundNode = ASImageNode()
self.textInputContainerBackgroundNode.isUserInteractionEnabled = false
self.textInputContainerBackgroundNode.displaysAsynchronously = false
self.textInputContainer = ASDisplayNode()
if !isCaption && !glass {
self.textInputContainer.addSubnode(self.textInputContainerBackgroundNode)
}
self.inputModeView = ComponentHostView<Empty>()
self.textInputContainer.view.addSubview(self.inputModeView)
self.textInputContainer.clipsToBounds = true
self.textInputBackgroundNode = ASDisplayNode()
self.textInputBackgroundImageNode = ASImageNode()
self.textInputBackgroundImageNode.displaysAsynchronously = false
self.textInputBackgroundImageNode.displayWithoutProcessing = true
self.textPlaceholderNode = ImmediateTextNode()
self.textPlaceholderNode.maximumNumberOfLines = 1
self.textPlaceholderNode.isUserInteractionEnabled = false
self.oneLineNode = TextNodeWithEntities()
self.oneLineNode.textNode.isUserInteractionEnabled = false
self.actionButtons = AttachmentTextInputActionButtonsNode(presentationInterfaceState: presentationInterfaceState, glass: glass, presentController: presentController)
self.counterTextNode = ImmediateTextNode()
self.counterTextNode.textAlignment = .center
super.init()
if !isScheduledMessages {
self.actionButtons.sendButtonLongPressed = { [weak self] node, gesture in
self?.interfaceInteraction?.displaySendMessageOptions(node, gesture)
}
self.actionButtons.sendButtonLongPressEnabled = true
} else {
self.actionButtons.sendButtonLongPressEnabled = false
}
self.actionButtons.sendButton.addTarget(self, action: #selector(self.sendButtonPressed), forControlEvents: .touchUpInside)
self.actionButtons.sendButton.alpha = 1.0
self.actionButtons.updateAccessibility()
self.addSubnode(self.textInputContainer)
self.addSubnode(self.textInputBackgroundNode)
if !glass {
self.textInputBackgroundNode.addSubnode(self.textInputBackgroundImageNode)
}
self.addSubnode(self.textPlaceholderNode)
self.addSubnode(self.actionButtons)
self.addSubnode(self.counterTextNode)
if isCaption {
self.addSubnode(self.oneLineNode.textNode)
}
self.textInputBackgroundImageNode.clipsToBounds = true
let recognizer = TouchDownGestureRecognizer(target: self, action: #selector(self.textInputBackgroundViewTap(_:)))
recognizer.touchDown = { [weak self] in
if let strongSelf = self {
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.textInputBackgroundNode.view.addGestureRecognizer(recognizer)
self.emojiViewProvider = { [weak self] emoji in
guard let strongSelf = self, let presentationInterfaceState = strongSelf.presentationInterfaceState else {
return UIView()
}
return EmojiTextAttachmentView(context: context, userLocation: .other, emoji: emoji, file: emoji.file, cache: strongSelf.animationCache, renderer: strongSelf.animationRenderer, placeholderColor: presentationInterfaceState.theme.chat.inputPanel.inputTextColor.withAlphaComponent(0.12), pointSize: CGSize(width: 24.0, height: 24.0))
}
self.updateSendButtonEnabled(isCaption || isAttachment, animated: false)
if self.isCaption || self.isAttachment {
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId))
|> mapToSignal { peer -> Signal<Int32, NoError> in
if let peer = peer {
return self.context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.UserLimits.init(isPremium: peer.isPremium))
|> map { limits in
return limits.maxCaptionLength
}
} else {
return .complete()
}
}
|> deliverOnMainQueue).startStandalone(next: { [weak self] maxCaptionLength in
self?.maxCaptionLength = maxCaptionLength
})
}
self.inputMediaNodeDataPromise.set(
ChatEntityKeyboardInputNode.inputData(
context: context,
chatPeerId: nil,
areCustomEmojiEnabled: customEmojiAvailable,
hasTrending: false,
hasStickers: false,
hasGifs: false,
sendGif: nil
)
)
self.inputMediaNodeDataDisposable = (self.inputMediaNodeDataPromise.get()
|> deliverOnMainQueue).start(next: { [weak self] value in
guard let self else {
return
}
self.inputMediaNodeData = value
if case .emoji = self.currentInputMode {
self.requestRelayout(animated: false)
}
})
self.inputMediaInteraction = ChatEntityKeyboardInputNode.Interaction(
sendSticker: { _, _, _, _, _, _, _, _, _ in
return false
},
sendEmoji: { _, _, _ in
},
sendGif: { _, _, _, _, _ in
return false
},
sendBotContextResultAsGif: { _, _, _, _, _, _ in
return false
},
editGif: { _, _ in
},
updateChoosingSticker: { _ in
},
switchToTextInput: { [weak self] in
self?.activateInput()
},
dismissTextInput: {
},
insertText: { [weak self] text in
self?.insertTextFromInputMedia(text)
},
backwardsDeleteText: { [weak self] in
self?.deleteBackwardsFromInputMedia()
},
openStickerEditor: {
},
presentController: { [weak self] controller, _ in
self?.presentController(controller)
},
presentGlobalOverlayController: { [weak self] controller, _ in
self?.presentInGlobalOverlay(controller)
},
getNavigationController: { [weak self] in
return self?.getNavigationController()
},
requestLayout: { [weak self] transition in
self?.requestRelayout(animated: transition.isAnimated)
}
)
self.inputMediaInteraction?.forceTheme = presentationInterfaceState.theme
}
public var sendPressed: ((NSAttributedString?) -> Void)?
public var focusUpdated: ((Bool) -> Void)?
public var heightUpdated: ((Bool) -> Void)?
public var timerUpdated: ((NSNumber?) -> Void)?
public var captionIsAboveUpdated: ((Bool) -> Void)?
public var additionalInputHeight: CGFloat {
return self.currentAdditionalInputHeight
}
public func updateLayoutSize(_ size: CGSize, keyboardHeight: CGFloat, sideInset: CGFloat, animated: Bool) -> CGFloat {
self.currentSafeAreaInset = .zero
self.currentContainerBottomInset = 0.0
self.usesContainerLayout = false
guard let presentationInterfaceState = self.presentationInterfaceState else {
return 0.0
}
return self.updateLayout(width: size.width, leftInset: sideInset, rightInset: sideInset, bottomInset: 0.0, keyboardHeight: keyboardHeight, additionalSideInsets: UIEdgeInsets(), textFieldMaxHeight: size.height, availableHeight: size.height, isSecondary: false, transition: animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate, interfaceState: presentationInterfaceState, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), isMediaInputExpanded: false)
}
@objc(updateContainerLayoutSize:safeAreaInset:bottomInset:keyboardHeight:animated:)
public func updateContainerLayoutSize(_ size: CGSize, safeAreaInset: UIEdgeInsets, bottomInset: CGFloat, keyboardHeight: CGFloat, animated: Bool) -> CGFloat {
self.currentSafeAreaInset = safeAreaInset
self.currentContainerBottomInset = bottomInset
self.usesContainerLayout = true
guard let presentationInterfaceState = self.presentationInterfaceState else {
return 0.0
}
return self.updateLayout(width: size.width, leftInset: 0.0, rightInset: 0.0, bottomInset: safeAreaInset.bottom, keyboardHeight: keyboardHeight, additionalSideInsets: UIEdgeInsets(), textFieldMaxHeight: size.height, availableHeight: size.height, isSecondary: false, transition: animated ? .animated(duration: 0.4, curve: .spring) : .immediate, interfaceState: presentationInterfaceState, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), isMediaInputExpanded: false)
}
public func setCaption(_ caption: NSAttributedString?) {
self.interfaceInteraction?.updateTextInputStateAndMode { state, inputMode in
return (ChatTextInputState(inputText: caption ?? NSAttributedString()), inputMode)
}
}
public func setTimeout(_ timeout: Int32, isVideo: Bool, isCaptionAbove: Bool) {
self.currentIsCaptionAbove = isCaptionAbove
}
public func animate(_ view: UIView, frame: CGRect) {
let transition = ComponentTransition.spring(duration: 0.4)
transition.setFrame(view: view, frame: frame)
}
public func onAnimateOut() {
}
public func activateInput() {
self.loadTextInputNodeIfNeeded()
let wasEmoji = self.currentInputMode == .emoji
self.currentInputMode = .text
if let textInputNode = self.textInputNode {
self.isTransitioningToTextKeyboard = wasEmoji && textInputNode.textView.isFirstResponder
self.applyCurrentInputMode(reload: textInputNode.textView.isFirstResponder)
if !textInputNode.textView.isFirstResponder {
textInputNode.textView.becomeFirstResponder()
}
}
self.requestRelayout(animated: wasEmoji)
}
public func dismissInput() -> Bool {
let wasEmoji = self.currentInputMode == .emoji
if wasEmoji {
self.currentInputMode = .text
self.isTransitioningToTextKeyboard = false
self.requestRelayout(animated: true)
}
self.textInputNode?.resignFirstResponder()
self.applyCurrentInputMode(reload: false)
return true
}
public func baseHeight() -> CGFloat {
return 45.0
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.inputMediaNodeDataDisposable?.dispose()
}
public func loadTextInputNodeIfNeeded() {
if self.textInputNode == nil {
self.loadTextInputNode()
}
}
private func loadTextInputNode() {
let textInputNode = CaptionEditableTextNode()
textInputNode.initialPrimaryLanguage = self.presentationInterfaceState?.interfaceState.inputLanguage
var textColor: UIColor = .black
var tintColor: UIColor = .blue
var baseFontSize: CGFloat = 17.0
var keyboardAppearance: UIKeyboardAppearance = UIKeyboardAppearance.default
if let presentationInterfaceState = self.presentationInterfaceState {
textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor
tintColor = presentationInterfaceState.theme.list.itemAccentColor
baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize)
keyboardAppearance = presentationInterfaceState.theme.rootController.keyboardColor.keyboardAppearance
textInputNode.textView.theme = makeTextInputTheme(context: self.context, interfaceState: presentationInterfaceState)
}
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
self.textInputNode = textInputNode
textInputNode.textView.inputAssistantItem.leadingBarButtonGroups = []
textInputNode.textView.inputAssistantItem.trailingBarButtonGroups = []
if let presentationInterfaceState = self.presentationInterfaceState {
refreshChatTextInputTypingAttributes(textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize)
textInputNode.textContainerInset = calculateTextFieldRealInsets(presentationInterfaceState, glass: self.glass)
}
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.view.layoutIfNeeded()
textInputNode.textView.updateLayout(size: textInputNode.bounds.size)
self.updateSpoiler()
}
self.textInputBackgroundNode.isUserInteractionEnabled = false
self.textInputBackgroundNode.view.removeGestureRecognizer(self.textInputBackgroundNode.view.gestureRecognizers![0])
let recognizer = TouchDownGestureRecognizer(target: self, action: #selector(self.textInputBackgroundViewTap(_:)))
recognizer.touchDown = { [weak self] in
if let strongSelf = self {
strongSelf.ensureFocused()
}
}
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
}
}
textInputNode.view.addGestureRecognizer(recognizer)
textInputNode.textView.accessibilityHint = self.textPlaceholderNode.attributedText?.string
self.applyCurrentInputMode(reload: false)
}
private func textFieldMaxHeight(_ maxHeight: CGFloat, metrics: LayoutMetrics) -> CGFloat {
let textFieldInsets = self.textFieldInsets(metrics: metrics)
return max(self.glass ? 40.0 : 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) {
var textFieldInsets = self.textFieldInsets(metrics: metrics)
if self.actionButtons.frame.width > 44.0 {
textFieldInsets.right = self.actionButtons.frame.width - 6.0
}
let fieldMaxHeight = textFieldMaxHeight(maxHeight, metrics: metrics)
var textFieldMinHeight: CGFloat = 35.0
var textFieldRealInsets = UIEdgeInsets()
if let presentationInterfaceState = self.presentationInterfaceState {
textFieldMinHeight = calclulateTextFieldMinHeight(presentationInterfaceState, glass: self.glass, metrics: metrics)
textFieldRealInsets = calculateTextFieldRealInsets(presentationInterfaceState, glass: self.glass)
}
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: textFieldRealInsets.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 (0.0, textFieldHeight)
}
private func textFieldInsets(metrics: LayoutMetrics) -> UIEdgeInsets {
var insets = UIEdgeInsets(top: 6.0, left: 6.0, bottom: 6.0, right: 42.0)
if self.glass {
insets.left = 8.0
insets.top = 0.0
insets.bottom = 0.0
insets.right += 3.0
} else 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
}
func minimalHeight(interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat {
let textFieldMinHeight = calclulateTextFieldMinHeight(interfaceState, glass: self.glass, metrics: metrics)
var minimalHeight: CGFloat = 14.0 + textFieldMinHeight
if case .regular = metrics.widthClass, case .regular = metrics.heightClass {
minimalHeight += 2.0
}
return minimalHeight
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard self.isUserInteractionEnabled else {
return nil
}
if let aiButton = self.aiButton, aiButton.button.alpha > 0.0 {
let aiButtonPoint = self.view.convert(point, to: aiButton.button)
if aiButton.button.bounds.contains(aiButtonPoint) {
return aiButton.button
}
}
if !self.inputModeView.isHidden, let result = self.inputModeView.hitTest(self.view.convert(point, to: self.inputModeView), with: event) {
return result
}
if let inputMediaNode = self.inputMediaNode, let result = inputMediaNode.view.hitTest(self.view.convert(point, to: inputMediaNode.view), with: event) {
return result
}
let result = super.hitTest(point, with: event)
if result === self.view {
return nil
}
return result
}
public 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 {
self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, keyboardHeight: 0.0, additionalSideInsets: additionalSideInsets, textFieldMaxHeight: maxHeight, availableHeight: maxHeight, isSecondary: isSecondary, transition: transition, interfaceState: interfaceState, metrics: metrics, isMediaInputExpanded: isMediaInputExpanded)
}
public func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, keyboardHeight: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat {
self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, keyboardHeight: keyboardHeight, additionalSideInsets: additionalSideInsets, textFieldMaxHeight: maxHeight, availableHeight: maxHeight, isSecondary: isSecondary, transition: transition, interfaceState: interfaceState, metrics: metrics, isMediaInputExpanded: isMediaInputExpanded)
}
public func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, keyboardHeight: CGFloat, additionalSideInsets: UIEdgeInsets, textFieldMaxHeight: CGFloat, availableHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat {
let hadLayout = self.validLayout != nil
let previousLayout = self.validLayout
self.validLayout = (width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, keyboardHeight: keyboardHeight, additionalSideInsets: additionalSideInsets, textFieldMaxHeight: textFieldMaxHeight, availableHeight: availableHeight, metrics: metrics, isSecondary: isSecondary)
let previousAdditionalSideInsets = previousLayout?.additionalSideInsets
let leftInset = leftInset + 8.0
let rightInset = rightInset + 8.0
var transition = transition
if keyboardHeight.isZero, let previousKeyboardHeight = previousLayout?.keyboardHeight, previousKeyboardHeight > 0.0, !transition.isAnimated {
transition = .animated(duration: 0.4, curve: .spring)
}
if let previousAdditionalSideInsets, previousAdditionalSideInsets.right != additionalSideInsets.right {
if case .animated = transition {
transition = .animated(duration: 0.2, curve: .easeInOut)
}
}
if self.presentationInterfaceState != interfaceState || !hadLayout {
let previousState = self.presentationInterfaceState
self.presentationInterfaceState = interfaceState
self.inputMediaInteraction?.forceTheme = interfaceState.theme
let themeUpdated = previousState?.theme !== interfaceState.theme
var updateSendButtonIcon = false
if (previousState?.interfaceState.editMessage != nil) != (interfaceState.interfaceState.editMessage != nil) {
updateSendButtonIcon = true
}
if self.theme !== interfaceState.theme {
updateSendButtonIcon = true
if self.theme == nil || !self.theme!.chat.inputPanel.inputTextColor.isEqual(interfaceState.theme.chat.inputPanel.inputTextColor) {
let textColor = interfaceState.theme.chat.inputPanel.inputTextColor
let baseFontSize = max(minInputFontSize, interfaceState.fontSize.baseDisplaySize)
if let textInputNode = self.textInputNode {
if let text = textInputNode.attributedText {
let selectedRange = textInputNode.selectedRange
let textRange = NSMakeRange(0, (text.string as NSString).length)
let updatedText = NSMutableAttributedString(attributedString: text)
updatedText.removeAttribute(.foregroundColor, range: textRange)
updatedText.addAttribute(.foregroundColor, value: textColor, range: textRange)
textInputNode.attributedText = updatedText
textInputNode.selectedRange = selectedRange
}
textInputNode.textView.typingAttributes = [NSAttributedString.Key.font: Font.regular(baseFontSize), NSAttributedString.Key.foregroundColor: textColor]
self.updateSpoiler()
}
}
let keyboardAppearance = interfaceState.theme.rootController.keyboardColor.keyboardAppearance
if let textInputNode = self.textInputNode, textInputNode.keyboardAppearance != keyboardAppearance, textInputNode.isFirstResponder() {
if textInputNode.isCurrentlyEmoji() {
textInputNode.initialPrimaryLanguage = "emoji"
textInputNode.resetInitialPrimaryLanguage()
}
textInputNode.keyboardAppearance = keyboardAppearance
}
self.theme = interfaceState.theme
self.actionButtons.updateTheme(theme: interfaceState.theme, wallpaper: interfaceState.chatWallpaper)
let textFieldMinHeight = calclulateTextFieldMinHeight(interfaceState, glass: self.glass, metrics: metrics)
let minimalInputHeight: CGFloat = 2.0 + textFieldMinHeight
let backgroundColor: UIColor
if case let .color(color) = interfaceState.chatWallpaper, UIColor(rgb: color).isEqual(interfaceState.theme.chat.inputPanel.panelBackgroundColorNoWallpaper) {
backgroundColor = interfaceState.theme.chat.inputPanel.panelBackgroundColorNoWallpaper
} else {
backgroundColor = interfaceState.theme.chat.inputPanel.panelBackgroundColor
}
self.textInputBackgroundImageNode.image = textInputBackgroundImage(backgroundColor: backgroundColor, inputBackgroundColor: nil, strokeColor: interfaceState.theme.chat.inputPanel.inputStrokeColor, diameter: minimalInputHeight, caption: self.isCaption)
self.transparentTextInputBackgroundImage = textInputBackgroundImage(backgroundColor: nil, inputBackgroundColor: interfaceState.theme.chat.inputPanel.inputBackgroundColor, strokeColor: interfaceState.theme.chat.inputPanel.inputStrokeColor, diameter: minimalInputHeight, caption: self.isCaption)
self.textInputContainerBackgroundNode.image = generateStretchableFilledCircleImage(diameter: minimalInputHeight, color: interfaceState.theme.chat.inputPanel.inputBackgroundColor)
} else if self.strings !== interfaceState.strings {
self.strings = interfaceState.strings
self.inputMenu.updateStrings(interfaceState.strings)
}
if themeUpdated || !self.initializedPlaceholder {
self.initializedPlaceholder = true
let placeholder = self.isCaption || self.isAttachment ? interfaceState.strings.MediaPicker_AddCaption : interfaceState.strings.Conversation_InputTextPlaceholder
if self.currentPlaceholder != placeholder || themeUpdated {
self.currentPlaceholder = placeholder
let baseFontSize = max(minInputFontSize, interfaceState.fontSize.baseDisplaySize)
self.textPlaceholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(baseFontSize), textColor: interfaceState.theme.chat.inputPanel.inputPlaceholderColor)
self.textInputNode?.textView.accessibilityHint = placeholder
let placeholderSize = self.textPlaceholderNode.updateLayout(CGSize(width: 320.0, height: CGFloat.greatestFiniteMagnitude))
if transition.isAnimated, let snapshotLayer = self.textPlaceholderNode.layer.snapshotContentTree() {
self.textPlaceholderNode.supernode?.layer.insertSublayer(snapshotLayer, above: self.textPlaceholderNode.layer)
snapshotLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.22, removeOnCompletion: false, completion: { [weak snapshotLayer] _ in
snapshotLayer?.removeFromSuperlayer()
})
self.textPlaceholderNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18)
}
self.textPlaceholderNode.frame = CGRect(origin: self.textPlaceholderNode.frame.origin, size: placeholderSize)
}
}
let sendButtonHasApplyIcon = self.isCaption || interfaceState.interfaceState.editMessage != nil
if updateSendButtonIcon, !self.actionButtons.animatingSendButton {
let imageNode = self.actionButtons.sendButton.imageNode
if transition.isAnimated && !self.actionButtons.sendButton.alpha.isZero && self.actionButtons.sendButton.layer.animation(forKey: "opacity") == nil, let previousImage = imageNode.image {
let tempView = UIImageView(image: previousImage)
self.actionButtons.sendButton.view.addSubview(tempView)
tempView.frame = imageNode.frame
tempView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak tempView] _ in
tempView?.removeFromSuperview()
})
tempView.layer.animateScale(from: 1.0, to: 0.2, duration: 0.2, removeOnCompletion: false)
imageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
imageNode.layer.animateScale(from: 0.2, to: 1.0, duration: 0.2)
}
self.actionButtons.sendButtonHasApplyIcon = sendButtonHasApplyIcon
if self.actionButtons.sendButtonHasApplyIcon {
self.actionButtons.setImage(PresentationResourcesChat.chatInputPanelApplyIconImage(interfaceState.theme))
} else {
self.actionButtons.setImage(PresentationResourcesChat.chatInputPanelSendIconImage(interfaceState.theme))
}
}
}
let isLandscape = width > availableHeight
let deviceMetrics = DeviceMetrics(screenSize: CGSize(width: width, height: availableHeight), scale: UIScreen.main.scale, statusBarHeight: 0.0, onScreenNavigationHeight: nil)
let standardInputHeight = deviceMetrics.standardInputHeight(inLandscape: isLandscape)
if keyboardHeight > 0.0 || !self.isFocused {
self.isTransitioningToTextKeyboard = false
}
var textFieldMinHeight: CGFloat = self.glass ? 40.0 : 33.0
if let presentationInterfaceState = self.presentationInterfaceState {
textFieldMinHeight = calclulateTextFieldMinHeight(presentationInterfaceState, glass: self.glass, metrics: metrics)
}
let minimalHeight: CGFloat = 14.0 + textFieldMinHeight
let baseWidth = width - leftInset - rightInset
let (_, textFieldHeight) = self.calculateTextFieldMetrics(width: baseWidth, maxHeight: textFieldMaxHeight, metrics: metrics)
var panelContentHeight = self.panelHeight(textFieldHeight: textFieldHeight, metrics: metrics)
var inputHasText = false
if let textInputNode = self.textInputNode, let attributedText = textInputNode.attributedText, attributedText.length != 0 {
inputHasText = true
}
var textFieldInsets = self.textFieldInsets(metrics: metrics)
if additionalSideInsets.right > 0.0 {
textFieldInsets.right += additionalSideInsets.right / 3.0
}
var textInputViewRealInsets = UIEdgeInsets()
if let presentationInterfaceState = self.presentationInterfaceState {
textInputViewRealInsets = calculateTextFieldRealInsets(presentationInterfaceState, glass: self.glass)
}
if self.isCaption {
if self.isFocused {
self.oneLineNode.textNode.alpha = 0.0
self.oneLineDustNode?.alpha = 0.0
self.textInputNode?.alpha = 1.0
transition.updateAlpha(node: self.actionButtons, alpha: 1.0)
transition.updateTransformScale(node: self.actionButtons, scale: 1.0)
transition.updateAlpha(node: self.textInputBackgroundImageNode, alpha: 1.0)
} else {
panelContentHeight = minimalHeight
transition.updateAlpha(node: self.oneLineNode.textNode, alpha: inputHasText ? 1.0 : 0.0)
if let oneLineDustNode = self.oneLineDustNode {
transition.updateAlpha(node: oneLineDustNode, alpha: inputHasText ? 1.0 : 0.0)
}
if let textInputNode = self.textInputNode {
transition.updateAlpha(node: textInputNode, alpha: inputHasText ? 0.0 : 1.0)
}
transition.updateAlpha(node: self.actionButtons, alpha: 0.0)
transition.updateTransformScale(node: self.actionButtons, scale: 0.001)
transition.updateAlpha(node: self.textInputBackgroundImageNode, alpha: inputHasText ? 1.0 : 0.0)
}
}
let inputPanelHeight = panelContentHeight + (self.glass ? 11.0 : 0.0)
var totalHeight = inputPanelHeight
var inputMediaHeight: CGFloat = 0.0
self.currentAdditionalInputHeight = 0.0
var inputMediaNodeForLayout: ChatEntityKeyboardInputNode?
var isNewInputMediaNode = false
if case .emoji = self.currentInputMode, let inputData = self.inputMediaNodeData {
let inputMediaNode: ChatEntityKeyboardInputNode
if let current = self.inputMediaNode {
inputMediaNode = current
} else {
isNewInputMediaNode = true
inputMediaNode = ChatEntityKeyboardInputNode(
context: self.context,
currentInputData: inputData,
updatedInputData: self.inputMediaNodeDataPromise.get(),
defaultToEmojiTab: true,
opaqueTopPanelBackground: false,
useOpaqueTheme: false,
interaction: self.inputMediaInteraction,
chatPeerId: nil,
stateContext: self.inputMediaNodeStateContext
)
inputMediaNode.clipsToBounds = true
inputMediaNode.externalTopPanelContainerImpl = nil
inputMediaNode.useExternalSearchContainer = true
self.inputMediaNode = inputMediaNode
}
if inputMediaNode.view.superview == nil {
self.view.addSubview(inputMediaNode.view)
}
inputMediaNodeForLayout = inputMediaNode
let heightAndOverflow = inputMediaNode.updateLayout(
width: width,
leftInset: 0.0,
rightInset: 0.0,
bottomInset: bottomInset,
standardInputHeight: standardInputHeight,
inputHeight: 0.0,
maximumHeight: availableHeight,
inputPanelHeight: 0.0,
transition: .immediate,
interfaceState: interfaceState,
layoutMetrics: metrics,
deviceMetrics: deviceMetrics,
isVisible: true,
isExpanded: false
)
inputMediaHeight = heightAndOverflow.0
self.currentAdditionalInputHeight = inputMediaHeight
totalHeight += inputMediaHeight
} else if let inputMediaNode = self.inputMediaNode {
self.inputMediaNode = nil
if transition.isAnimated {
var dismissingInputHeight = keyboardHeight
if self.isTransitioningToTextKeyboard && dismissingInputHeight.isZero && self.isFocused {
dismissingInputHeight = max(dismissingInputHeight, standardInputHeight)
}
let targetOriginY: CGFloat
if self.usesContainerLayout {
if dismissingInputHeight > 0.0 {
targetOriginY = availableHeight - dismissingInputHeight
} else {
targetOriginY = availableHeight
}
} else if dismissingInputHeight > 0.0 {
targetOriginY = inputPanelHeight
} else {
targetOriginY = inputPanelHeight + inputMediaNode.frame.height
}
let targetFrame = CGRect(origin: CGPoint(x: inputMediaNode.frame.minX, y: targetOriginY), size: inputMediaNode.frame.size)
transition.updateFrame(view: inputMediaNode.view, frame: targetFrame)
inputMediaNode.view.layer.animateAlpha(from: inputMediaNode.view.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak inputMediaNode] _ in
inputMediaNode?.view.removeFromSuperview()
})
} else {
inputMediaNode.view.removeFromSuperview()
}
}
var retainedInputHeight = keyboardHeight
var shouldRetainHiddenInputHeight = false
if self.isFocused {
if case .emoji = self.currentInputMode {
retainedInputHeight = max(retainedInputHeight, standardInputHeight)
shouldRetainHiddenInputHeight = true
} else if self.isTransitioningToTextKeyboard && retainedInputHeight.isZero {
retainedInputHeight = max(retainedInputHeight, standardInputHeight)
shouldRetainHiddenInputHeight = true
}
}
if self.currentAdditionalInputHeight.isZero && retainedInputHeight > 0.0 && shouldRetainHiddenInputHeight {
self.currentAdditionalInputHeight = retainedInputHeight
totalHeight += retainedInputHeight
}
let isLandscapePhone = width > availableHeight && UIDevice.current.userInterfaceIdiom != .pad
let collapsedCaptionTopInset = self.currentSafeAreaInset.top + 48.0
let expandedCaptionTopInset = self.currentSafeAreaInset.top + 8.0
var panelOriginY: CGFloat = 0.0
var inputMediaFrame = CGRect(origin: CGPoint(x: 0.0, y: inputPanelHeight), size: CGSize(width: width, height: inputMediaHeight))
if self.usesContainerLayout {
if isLandscapePhone {
panelOriginY = availableHeight + 16.0
inputMediaFrame.origin.y = availableHeight + 16.0
} else if case .emoji = self.currentInputMode {
inputMediaFrame.origin.y = availableHeight - inputMediaHeight
if self.currentIsCaptionAbove {
panelOriginY = expandedCaptionTopInset
} else {
panelOriginY = inputMediaFrame.minY - inputPanelHeight
}
} else {
if self.currentIsCaptionAbove {
panelOriginY = (retainedInputHeight > 0.0 ? expandedCaptionTopInset : collapsedCaptionTopInset)
} else {
let bottomOffset = max(self.currentContainerBottomInset, retainedInputHeight)
panelOriginY = availableHeight - inputPanelHeight - bottomOffset
}
inputMediaFrame.origin.y = availableHeight
}
}
if self.isCaption {
let makeOneLineLayout = TextNodeWithEntities.asyncLayout(self.oneLineNode)
let (oneLineLayout, oneLineApply) = makeOneLineLayout(TextNodeLayoutArguments(
attributedString: self.oneLineNodeAttributedText,
backgroundColor: nil,
minimumNumberOfLines: 1,
maximumNumberOfLines: 1,
truncationType: .end,
constrainedSize: CGSize(width: baseWidth - textFieldInsets.left - textFieldInsets.right, height: CGFloat.greatestFiniteMagnitude),
alignment: .left,
verticalAlignment: .top,
lineSpacing: 0.0,
cutout: nil, insets: UIEdgeInsets(),
lineColor: nil,
textShadowColor: nil,
textStroke: nil,
displaySpoilers: false,
displayEmbeddedItemsUnderSpoilers: false
))
let oneLineFrame = CGRect(
origin: CGPoint(
x: leftInset + textFieldInsets.left + self.textInputViewInternalInsets.left,
y: panelOriginY + textFieldInsets.top + self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel
),
size: oneLineLayout.size
)
self.oneLineNode.textNode.frame = oneLineFrame
let _ = oneLineApply(TextNodeWithEntities.Arguments(
context: self.context,
cache: self.animationCache,
renderer: self.animationRenderer,
placeholderColor: self.presentationInterfaceState?.theme.chat.inputPanel.inputTextColor.withAlphaComponent(0.12) ?? .lightGray,
attemptSynchronous: false
))
self.updateOneLineSpoiler()
}
self.textPlaceholderNode.isHidden = inputHasText
let textInputFrame = CGRect(
x: leftInset + textFieldInsets.left,
y: panelOriginY + textFieldInsets.top,
width: baseWidth - textFieldInsets.left - textFieldInsets.right,
height: panelContentHeight - textFieldInsets.top - textFieldInsets.bottom
)
let additionalRightInset = self.updateFieldAndButtonsLayout(inputHasText: inputHasText, panelHeight: panelContentHeight, panelOriginY: panelOriginY, textInputFrame: textInputFrame, transition: transition)
let updatedTextInputFrame = CGRect(
x: textInputFrame.minX,
y: textInputFrame.minY,
width: baseWidth - textFieldInsets.left - textFieldInsets.right - additionalRightInset,
height: textInputFrame.height
)
transition.updateFrame(node: self.textInputContainer, frame: updatedTextInputFrame)
if let textInputNode = self.textInputNode {
let textFieldFrame = CGRect(
origin: CGPoint(x: self.textInputViewInternalInsets.left, y: self.textInputViewInternalInsets.top),
size: CGSize(
width: updatedTextInputFrame.size.width - (self.textInputViewInternalInsets.left + self.textInputViewInternalInsets.right),
height: updatedTextInputFrame.size.height - self.textInputViewInternalInsets.top - self.textInputViewInternalInsets.bottom
)
)
let shouldUpdateLayout = textFieldFrame.size != textInputNode.frame.size
if let presentationInterfaceState = self.presentationInterfaceState {
textInputNode.textContainerInset = calculateTextFieldRealInsets(presentationInterfaceState, glass: self.glass)
}
transition.updateFrame(node: textInputNode, frame: textFieldFrame)
if shouldUpdateLayout {
textInputNode.layout()
}
}
self.updateCounterTextNode(transition: transition, panelHeight: panelContentHeight, panelOriginY: panelOriginY)
self.actionButtons.updateAccessibility()
if let inputMediaNode = inputMediaNodeForLayout {
if isNewInputMediaNode && transition.isAnimated {
inputMediaNode.view.frame = inputMediaFrame.offsetBy(dx: 0.0, dy: inputMediaHeight)
inputMediaNode.view.alpha = 0.0
inputMediaNode.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
inputMediaNode.view.alpha = 1.0
transition.updateFrame(view: inputMediaNode.view, frame: inputMediaFrame)
}
self.currentHeight = totalHeight
return totalHeight
}
private func updateFieldAndButtonsLayout(inputHasText: Bool, panelHeight: CGFloat, panelOriginY: CGFloat, textInputFrame: CGRect, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let layout = self.validLayout else {
return 0.0
}
let width = layout.width
let leftInset = layout.leftInset + 8.0
let rightInset = layout.rightInset + 8.0
var textFieldMinHeight: CGFloat = self.glass ? 40.0 : 33.0
if let presentationInterfaceState = self.presentationInterfaceState {
textFieldMinHeight = calclulateTextFieldMinHeight(presentationInterfaceState, glass: self.glass, metrics: layout.metrics)
}
var minimalHeight: CGFloat = textFieldMinHeight
if !self.glass {
minimalHeight += 14.0
}
var panelHeight = panelHeight
var composeButtonsOffset: CGFloat = 0.0
if self.isCaption {
if self.isFocused {
composeButtonsOffset = 0.0
} else {
composeButtonsOffset = 36.0
panelHeight = minimalHeight
}
}
let baseWidth = width - leftInset - rightInset
var textFieldInsets = self.textFieldInsets(metrics: layout.metrics)
if layout.additionalSideInsets.right > 0.0 {
textFieldInsets.right += layout.additionalSideInsets.right / 3.0
}
var isPaidMessage = false
var textBackgroundInset: CGFloat = 0.0
let actionButtonsSize: CGSize
if let presentationInterfaceState = self.presentationInterfaceState {
let isMinimized: Bool
let text: String
if let sendPaidMessageStars = presentationInterfaceState.sendPaidMessageStars {
isMinimized = false
let count = max(1, presentationInterfaceState.interfaceState.forwardMessageIds?.count ?? 1)
text = "⭐️\(sendPaidMessageStars.value * Int64(count))"
isPaidMessage = true
} else {
isMinimized = !self.isAttachment || inputHasText || self.glass
text = presentationInterfaceState.strings.MediaPicker_Send
}
actionButtonsSize = self.actionButtons.updateLayout(size: CGSize(width: 44.0, height: minimalHeight), transition: transition, minimized: isMinimized, text: text, interfaceState: presentationInterfaceState)
textBackgroundInset = actionButtonsSize.width - 44.0
} else {
actionButtonsSize = CGSize(width: 44.0, height: minimalHeight)
}
let actionButtonsOriginOffset: CGFloat = self.glass ? -6.0 : 0.0
let actionButtonsFrame = CGRect(origin: CGPoint(x: width - rightInset - actionButtonsSize.width + 1.0 - UIScreenPixel + composeButtonsOffset + actionButtonsOriginOffset, y: panelOriginY + panelHeight - minimalHeight - 1.0), size: actionButtonsSize)
transition.updateFrame(node: self.actionButtons, frame: actionButtonsFrame)
let textInputHeight = panelHeight - textFieldInsets.top - textFieldInsets.bottom
let textInputBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: baseWidth - textFieldInsets.left - textFieldInsets.right + composeButtonsOffset - textBackgroundInset, height: textInputHeight))
transition.updateFrame(node: self.textInputContainerBackgroundNode, frame: textInputBackgroundFrame)
transition.updateFrame(layer: self.textInputBackgroundNode.layer, frame: CGRect(x: leftInset + textFieldInsets.left, y: panelOriginY + textFieldInsets.top, width: baseWidth - textFieldInsets.left - textFieldInsets.right + composeButtonsOffset - textBackgroundInset, height: panelHeight - textFieldInsets.top - textFieldInsets.bottom))
transition.updateFrame(layer: self.textInputBackgroundImageNode.layer, frame: CGRect(x: 0.0, y: 0.0, width: baseWidth - textFieldInsets.left - textFieldInsets.right + composeButtonsOffset - textBackgroundInset, height: panelHeight - textFieldInsets.top - textFieldInsets.bottom))
if self.isAIEnabled {
let aiButton: (button: HighlightTrackingButton, icon: UIImageView)
if let current = self.aiButton {
aiButton = current
} else {
aiButton = (HighlightTrackingButton(), UIImageView())
self.aiButton = aiButton
aiButton.button.highligthedChanged = { [weak self] highlighted in
guard let self, let aiButton = self.aiButton else {
return
}
if highlighted {
aiButton.icon.alpha = 0.6
} else {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut)
transition.updateAlpha(layer: aiButton.icon.layer, alpha: 1.0)
}
}
aiButton.button.addTarget(self, action: #selector(self.aiButtonPressed), for: .touchUpInside)
aiButton.button.addSubview(aiButton.icon)
aiButton.icon.image = UIImage(bundleImageName: "Chat/Input/Text/InputAIIcon")?.withRenderingMode(.alwaysTemplate)
self.textInputBackgroundNode.view.addSubview(aiButton.icon)
self.textInputBackgroundNode.view.addSubview(aiButton.button)
}
if let presentationInterfaceState = self.presentationInterfaceState {
aiButton.icon.tintColor = presentationInterfaceState.theme.chat.inputPanel.inputControlColor
}
if let image = aiButton.icon.image {
let aiButtonSize = CGSize(width: 40.0, height: 40.0)
let aiButtonFrame = CGRect(origin: CGPoint(x: baseWidth - rightInset - aiButtonSize.width - 12.0, y: -1.0), size: aiButtonSize)
transition.updateFrame(view: aiButton.button, frame: aiButtonFrame)
transition.updateFrame(view: aiButton.icon, frame: image.size.centered(in: aiButtonFrame))
}
var aiButtonAlpha: CGFloat = textInputHeight >= 70.0 ? 1.0 : 0.0
self.heightDependentAiButtonAlpha = aiButtonAlpha
if !inputHasText {
aiButtonAlpha = 0.0
}
ComponentTransition(transition).setAlpha(view: aiButton.button, alpha: aiButtonAlpha)
ComponentTransition(transition).setAlpha(view: aiButton.icon, alpha: aiButtonAlpha)
} else if let aiButton = self.aiButton {
self.aiButton = nil
let aiButtonView = aiButton.button
let aiButtonIconView = aiButton.icon
transition.updateAlpha(layer: aiButton.button.layer, alpha: 0.0, completion: { [weak aiButtonView] _ in
aiButtonView?.removeFromSuperview()
})
transition.updateAlpha(layer: aiButton.icon.layer, alpha: 0.0, completion: { [weak aiButtonIconView] _ in
aiButtonIconView?.removeFromSuperview()
})
self.heightDependentAiButtonAlpha = 0.0
}
var textInputViewRealInsets = UIEdgeInsets()
if let presentationInterfaceState = self.presentationInterfaceState {
textInputViewRealInsets = calculateTextFieldRealInsets(presentationInterfaceState, glass: self.glass)
var colors: [String: UIColor] = [:]
let colorKeys: [String] = [
"__allcolors__"
]
let color = self.theme?.chat.inputPanel.inputControlColor ?? defaultDarkPresentationTheme.chat.inputPanel.inputControlColor
for colorKey in colorKeys {
colors[colorKey] = color
}
let animationComponent = LottieAnimationComponent(
animation: LottieAnimationComponent.AnimationItem(
name: self.currentInputMode == .text ? "input_anim_smileToKey" : "input_anim_keyToSmile",
mode: .still(position: .begin)
),
colors: colors,
size: CGSize(width: 32.0, height: 32.0)
)
let inputNodeSize = self.inputModeView.update(
transition: .immediate,
component: AnyComponent(Button(
content: AnyComponent(animationComponent),
action: { [weak self] in
self?.toggleInputMode()
})),
environment: {},
containerSize: CGSize(width: 32.0, height: 32.0)
)
var inputNodeOffset: CGPoint = CGPoint(x: -1.0, y: -1.0)
if self.glass {
inputNodeOffset = CGPoint(x: -6.0, y: -4.0)
}
transition.updateFrame(view: self.inputModeView, frame: CGRect(origin: CGPoint(x: textInputBackgroundFrame.maxX - inputNodeSize.width + inputNodeOffset.x, y: panelOriginY + textInputBackgroundFrame.maxY - inputNodeSize.height + inputNodeOffset.y), size: inputNodeSize))
}
let placeholderFrame: CGRect
if self.isCaption && !self.isFocused {
placeholderFrame = CGRect(origin: CGPoint(x: textInputFrame.minX + floorToScreenPixels((textInputBackgroundFrame.width - self.textPlaceholderNode.frame.width) / 2.0), y: panelOriginY + textFieldInsets.top + self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel), size: self.textPlaceholderNode.frame.size)
} else {
placeholderFrame = CGRect(origin: CGPoint(x: leftInset + textFieldInsets.left + self.textInputViewInternalInsets.left, y: panelOriginY + textFieldInsets.top + self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel), size: self.textPlaceholderNode.frame.size)
}
transition.updateFrame(node: self.textPlaceholderNode, frame: placeholderFrame)
return isPaidMessage ? textBackgroundInset : 0.0
}
private var skipUpdate = false
public func chatInputTextNodeDidUpdateText() {
if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState {
let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize)
refreshChatTextInputAttributes(context: self.context, textView: textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(self.context.animatedEmojiStickersValue.keys), 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.skipUpdate = true
self.interfaceInteraction?.updateTextInputStateAndMode({ _, inputMode in return (inputTextState, inputMode) })
self.interfaceInteraction?.updateInputLanguage({ _ in return textInputNode.textInputMode?.primaryLanguage })
if self.isCaption, let presentationInterfaceState = self.presentationInterfaceState {
self.presentationInterfaceState = presentationInterfaceState.updatedInterfaceState({
return $0.withUpdatedComposeInputState(inputTextState)
})
}
self.updateTextNodeText(animated: true)
if let aiButton = self.aiButton {
var aiButtonAlpha: CGFloat = self.heightDependentAiButtonAlpha
if let attributedText = textInputNode.attributedText, attributedText.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
aiButtonAlpha = 0.0
} else if textInputNode.attributedText == nil {
aiButtonAlpha = 0.0
}
ComponentTransition(.immediate).setAlpha(view: aiButton.button, alpha: aiButtonAlpha)
ComponentTransition(.immediate).setAlpha(view: aiButton.icon, alpha: aiButtonAlpha)
}
self.skipUpdate = false
}
}
@objc public 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)] = []
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 {
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 {
customEmojiRects.append((textRect.rect, value))
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)
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(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 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: self.context, textView: textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(self.context.animatedEmojiStickersValue.keys), emojiViewProvider: self.emojiViewProvider, makeCollapsedQuoteAttachment: { text, attributes in
return ChatInputTextCollapsedQuoteAttachmentImpl(text: text, attributes: attributes)
})
textInputNode.attributedText = textAttributedStringForStateText(context: self.context, stateText: self.inputTextState.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(self.context.animatedEmojiStickersValue.keys), 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 func updateCounterTextNode(transition: ContainedViewLayoutTransition, panelHeight: CGFloat, panelOriginY: CGFloat) {
let inputTextMaxLength: Int32?
if let maxCaptionLength = self.maxCaptionLength {
inputTextMaxLength = maxCaptionLength
} else {
inputTextMaxLength = nil
}
if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState, let inputTextMaxLength = 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 layout = self.validLayout {
let composeButtonsOffset: CGFloat = 0.0
let rightInset = layout.rightInset + 8.0
var textFieldMinHeight: CGFloat = 33.0
if let presentationInterfaceState = self.presentationInterfaceState {
textFieldMinHeight = calclulateTextFieldMinHeight(presentationInterfaceState, glass: self.glass, metrics: layout.metrics)
}
let minimalHeight: CGFloat = 14.0 + textFieldMinHeight
let counterSize = self.counterTextNode.updateLayout(CGSize(width: 44.0, height: 44.0))
let actionButtonsOriginX = layout.width - rightInset - 43.0 - UIScreenPixel + composeButtonsOffset
let counterFrame = CGRect(origin: CGPoint(x: actionButtonsOriginX, y: panelOriginY + panelHeight - minimalHeight - counterSize.height + 3.0), size: CGSize(width: layout.width - actionButtonsOriginX - rightInset, height: counterSize.height))
transition.updateFrame(node: self.counterTextNode, frame: counterFrame)
}
}
private func toggleInputMode() {
self.loadTextInputNodeIfNeeded()
guard let textInputNode = self.textInputNode else {
return
}
switch self.currentInputMode {
case .text:
self.isTransitioningToTextKeyboard = false
self.currentInputMode = .emoji
self.applyCurrentInputMode(reload: textInputNode.textView.isFirstResponder)
if !textInputNode.textView.isFirstResponder {
textInputNode.textView.becomeFirstResponder()
}
self.requestRelayout(animated: true)
case .emoji:
self.activateInput()
}
}
private func requestRelayout(animated: Bool) {
self.updateHeight(animated)
self.heightUpdated?(animated)
}
private func applyCurrentInputMode(reload: Bool) {
guard let textInputNode = self.textInputNode else {
return
}
switch self.currentInputMode {
case .text:
if textInputNode.textView.inputView != nil {
textInputNode.textView.inputView = nil
if reload && textInputNode.textView.isFirstResponder {
textInputNode.textView.reloadInputViews()
}
}
case .emoji:
if !(textInputNode.textView.inputView is EmptyInputView) {
textInputNode.textView.inputView = EmptyInputView()
if reload && textInputNode.textView.isFirstResponder {
textInputNode.textView.reloadInputViews()
}
}
}
}
private func insertTextFromInputMedia(_ text: NSAttributedString) {
self.loadTextInputNodeIfNeeded()
guard let textInputNode = self.textInputNode else {
return
}
let attributedText = NSMutableAttributedString(attributedString: textInputNode.attributedText ?? NSAttributedString())
let range = textInputNode.selectedRange
attributedText.replaceCharacters(in: range, with: text)
let selectionPosition = range.lowerBound + text.length
textInputNode.attributedText = attributedText
textInputNode.selectedRange = NSRange(location: selectionPosition, length: 0)
self.chatInputTextNodeDidUpdateText()
}
private func deleteBackwardsFromInputMedia() {
self.loadTextInputNodeIfNeeded()
guard let textInputNode = self.textInputNode else {
return
}
if textInputNode.textView.isFirstResponder {
textInputNode.textView.deleteBackward()
return
}
let attributedText = NSMutableAttributedString(attributedString: textInputNode.attributedText ?? NSAttributedString())
let range = textInputNode.selectedRange
if range.length > 0 {
attributedText.deleteCharacters(in: range)
textInputNode.attributedText = attributedText
textInputNode.selectedRange = NSRange(location: range.location, length: 0)
self.chatInputTextNodeDidUpdateText()
} else if range.location > 0 {
let deleteRange = NSRange(location: range.location - 1, length: 1)
attributedText.deleteCharacters(in: deleteRange)
textInputNode.attributedText = attributedText
textInputNode.selectedRange = NSRange(location: deleteRange.location, length: 0)
self.chatInputTextNodeDidUpdateText()
}
}
private func updateTextNodeText(animated: Bool) {
var inputHasText = false
if let textInputNode = self.textInputNode, let attributedText = textInputNode.attributedText, attributedText.length != 0 {
inputHasText = true
}
if let presentationInterfaceState = self.presentationInterfaceState {
self.textPlaceholderNode.isHidden = inputHasText
let textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor
let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize)
let textFont = Font.regular(baseFontSize)
let accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor
let attributedText = textAttributedStringForStateText(context: self.context, stateText: self.inputTextState.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: false, availableEmojis: Set(self.context.animatedEmojiStickersValue.keys), emojiViewProvider: self.emojiViewProvider, makeCollapsedQuoteAttachment: { text, attributes in
return ChatInputTextCollapsedQuoteAttachmentImpl(text: text, attributes: attributes)
})
let range = (attributedText.string as NSString).range(of: "\n")
if range.location != NSNotFound {
let trimmedText = NSMutableAttributedString(attributedString: attributedText.attributedSubstring(from: NSMakeRange(0, range.location)))
trimmedText.append(NSAttributedString(string: "\u{2026}", font: textFont, textColor: textColor))
self.oneLineNodeAttributedText = trimmedText
} else {
self.oneLineNodeAttributedText = attributedText
}
} else {
self.oneLineNodeAttributedText = nil
}
let panelHeight = self.updateTextHeight(animated: animated)
if self.isAttachment, let panelHeight = panelHeight, let layout = self.validLayout {
let leftInset = layout.leftInset + 8.0
let rightInset = layout.rightInset + 8.0
let baseWidth = layout.width - leftInset - rightInset
var textFieldInsets = self.textFieldInsets(metrics: layout.metrics)
if layout.additionalSideInsets.right > 0.0 {
textFieldInsets.right += layout.additionalSideInsets.right / 3.0
}
let textInputFrame = CGRect(
x: leftInset + textFieldInsets.left,
y: textFieldInsets.top,
width: baseWidth - textFieldInsets.left - textFieldInsets.right,
height: panelHeight - textFieldInsets.top - textFieldInsets.bottom
)
let _ = self.updateFieldAndButtonsLayout(
inputHasText: inputHasText,
panelHeight: panelHeight,
panelOriginY: 0.0,
textInputFrame: textInputFrame,
transition: .animated(duration: 0.2, curve: .easeInOut)
)
}
}
private func updateOneLineSpoiler() {
if let textLayout = self.oneLineNode.textNode.cachedLayout, !textLayout.spoilers.isEmpty {
if self.oneLineDustNode == nil {
let oneLineDustNode = InvisibleInkDustNode(textNode: nil, enableAnimations: self.context.sharedContext.energyUsageSettings.fullTranslucency)
self.oneLineDustNode = oneLineDustNode
self.oneLineNode.textNode.supernode?.insertSubnode(oneLineDustNode, aboveSubnode: self.oneLineNode.textNode)
}
if let oneLineDustNode = self.oneLineDustNode {
let textFrame = self.oneLineNode.textNode.frame.insetBy(dx: 0.0, dy: -3.0)
oneLineDustNode.update(size: textFrame.size, color: .white, textColor: .white, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 0.0, dy: 3.0) }, wordRects: textLayout.spoilerWords.map { $0.1.offsetBy(dx: 0.0, dy: 3.0) })
oneLineDustNode.frame = textFrame
}
} else {
if let oneLineDustNode = self.oneLineDustNode {
self.oneLineDustNode = nil
oneLineDustNode.removeFromSupernode()
}
}
}
private func updateTextHeight(animated: Bool) -> CGFloat? {
if let layout = self.validLayout {
let leftInset = layout.leftInset + 8.0
let rightInset = layout.rightInset + 8.0
let (_, textFieldHeight) = self.calculateTextFieldMetrics(width: layout.width - leftInset - rightInset - layout.additionalSideInsets.right, maxHeight: layout.textFieldMaxHeight, metrics: layout.metrics)
let panelContentHeight = self.panelHeight(textFieldHeight: textFieldHeight, metrics: layout.metrics)
let totalHeight = panelContentHeight + (self.glass ? 11.0 : 0.0) + self.currentAdditionalInputHeight
if self.currentHeight != totalHeight {
self.updateHeight(animated)
self.heightUpdated?(animated)
}
return panelContentHeight
} else {
return nil
}
}
public func chatInputTextNodeShouldReturn(modifierFlags: UIKeyModifierFlags) -> Bool {
if self.actionButtons.sendButton.supernode != nil && !self.actionButtons.sendButton.isHidden && !self.actionButtons.sendButton.alpha.isZero {
self.sendButtonPressed()
}
return false
}
@objc public func editableTextNodeShouldReturn(_ editableTextNode: ASEditableTextNode) -> Bool {
return self.chatInputTextNodeShouldReturn(modifierFlags: [])
}
private func applyUpdateSendButtonIcon() {
if let interfaceState = self.presentationInterfaceState {
let sendButtonHasApplyIcon = interfaceState.interfaceState.editMessage != nil
if sendButtonHasApplyIcon != self.actionButtons.sendButtonHasApplyIcon {
self.actionButtons.sendButtonHasApplyIcon = sendButtonHasApplyIcon
if self.actionButtons.sendButtonHasApplyIcon {
self.actionButtons.setImage(PresentationResourcesChat.chatInputPanelApplyIconImage(interfaceState.theme))
} else {
if case .scheduledMessages = interfaceState.subject {
self.actionButtons.setImage(PresentationResourcesChat.chatInputPanelScheduleButtonImage(interfaceState.theme))
} else {
self.actionButtons.setImage(PresentationResourcesChat.chatInputPanelSendIconImage(interfaceState.theme))
}
}
}
}
}
public func chatInputTextNodeDidChangeSelection(dueToEditing: Bool) {
if !dueToEditing && !self.updatingInputState {
let inputTextState = self.inputTextState
self.skipUpdate = true
self.interfaceInteraction?.updateTextInputStateAndMode({ _, inputMode in return (inputTextState, inputMode) })
self.skipUpdate = false
}
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()
}
}
@objc public func editableTextNodeDidChangeSelection(_ editableTextNode: ASEditableTextNode, fromSelectedRange: NSRange, toSelectedRange: NSRange, dueToEditing: Bool) {
self.chatInputTextNodeDidChangeSelection(dueToEditing: dueToEditing)
}
public func chatInputTextNodeDidBeginEditing() {
self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in
return (.text, state.keyboardButtonsMessage?.id)
})
self.inputMenu.activate()
self.focusUpdated?(true)
if self.isCaption || self.currentInputMode != .text {
self.requestRelayout(animated: true)
}
}
@objc public func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) {
self.chatInputTextNodeDidBeginEditing()
}
public func chatInputTextNodeDidFinishEditing() {
guard let editableTextNode = self.textInputNode else {
return
}
let shouldUpdateLayout = self.isCaption || self.currentInputMode != .text
self.storedInputLanguage = editableTextNode.textInputMode?.primaryLanguage
self.inputMenu.deactivate()
self.focusUpdated?(false)
if self.currentInputMode != .text {
self.currentInputMode = .text
self.applyCurrentInputMode(reload: false)
}
if shouldUpdateLayout {
self.requestRelayout(animated: true)
}
}
public func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) {
self.chatInputTextNodeDidFinishEditing()
}
public 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 {
return ASEditableTextNodeTargetForAction(target: self)
} else {
return ASEditableTextNodeTargetForAction(target: nil)
}
}
if case .format = self.inputMenu.state {
return ASEditableTextNodeTargetForAction(target: nil)
}
return nil
}
@available(iOS 13.0, *)
public func chatInputTextNodeMenu(forTextRange textRange: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu {
guard let editableTextNode = self.textInputNode else {
return UIMenu(children: suggestedActions)
}
var actions = suggestedActions
if editableTextNode.attributedText == nil || editableTextNode.attributedText!.length == 0 || editableTextNode.selectedRange.length == 0 {
} else {
var children: [UIAction] = [
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)
}
}
]
var hasSpoilers = true
if self.presentationInterfaceState?.chatLocation.peerId?.isSecretChat == true {
hasSpoilers = false
}
if hasSpoilers {
children.insert(UIAction(title: self.strings?.TextFormat_Quote ?? "Quote", image: nil) { [weak self] (action) in
if let strongSelf = self {
strongSelf.formatAttributesQuote(strongSelf)
}
}, at: 0)
children.append(UIAction(title: self.strings?.TextFormat_Spoiler ?? "Spoiler", image: nil) { [weak self] (action) in
if let strongSelf = self {
strongSelf.formatAttributesSpoiler(strongSelf)
}
})
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: 3)
}
return UIMenu(children: actions)
}
@available(iOS 16.0, *)
public func editableTextNodeMenu(_ editableTextNode: ASEditableTextNode, forTextRange textRange: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu {
return self.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 speechHolder = speakText(context: self.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)
}
}
private var imitateFocus = false
@objc func formatAttributesLink(_ sender: Any) {
self.inputMenu.back()
if self.isCaption {
self.imitateFocus = true
}
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 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)
}
@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)
}
}
public func chatInputTextNode(shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
guard let editableTextNode = self.textInputNode else {
return true
}
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: self.context, stateText: NSAttributedString(string: cleanText), fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(self.context.animatedEmojiStickersValue.keys), 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 public func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
return self.chatInputTextNode(shouldChangeTextIn: range, replacementText: text)
}
public 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 public func editableTextNodeShouldCopy(_ editableTextNode: ASEditableTextNode) -> Bool {
return self.chatInputTextNodeShouldCopy()
}
public func chatInputTextNodeShouldPaste() -> Bool {
let pasteboard = UIPasteboard.general
var attributedString: NSAttributedString?
if let data = pasteboard.data(forPasteboardType: kUTTypeRTF as String) {
attributedString = chatInputStateStringFromRTF(data, type: NSAttributedString.DocumentType.rtf)
} else if let data = pasteboard.data(forPasteboardType: "com.apple.flat-rtfd") {
attributedString = chatInputStateStringFromRTF(data, type: NSAttributedString.DocumentType.rtfd)
}
if let attributedString = attributedString {
self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
if let inputText = current.inputText.mutableCopy() as? NSMutableAttributedString {
inputText.replaceCharacters(in: NSMakeRange(current.selectionRange.lowerBound, current.selectionRange.count), with: attributedString)
let updatedRange = current.selectionRange.lowerBound + attributedString.length
return (ChatTextInputState(inputText: inputText, selectionRange: updatedRange ..< updatedRange), inputMode)
} else {
return (ChatTextInputState(inputText: attributedString), inputMode)
}
}
return false
}
return true
}
public func chatInputTextNodeShouldRespondToAction(action: Selector) -> Bool {
return true
}
public func chatInputTextNodeTargetForAction(action: Selector) -> ChatInputTextNode.TargetForAction? {
return nil
}
@objc public func editableTextNodeShouldPaste(_ editableTextNode: ASEditableTextNode) -> Bool {
return self.chatInputTextNodeShouldPaste()
}
public func chatInputTextNodeBackspaceWhileEmpty() {
}
@objc private func aiButtonPressed() {
self.invokeAICompose?()
}
@objc func sendButtonPressed() {
let inputTextMaxLength: Int32?
if let maxCaptionLength = self.maxCaptionLength {
inputTextMaxLength = maxCaptionLength
} else {
inputTextMaxLength = nil
}
if let textInputNode = self.textInputNode, let inputTextMaxLength = inputTextMaxLength {
let textCount = Int32(textInputNode.textView.text.count)
let remainingCount = inputTextMaxLength - textCount
if remainingCount < 0 {
textInputNode.layer.addShakeAnimation()
self.hapticFeedback.error()
return
}
}
if let sendPressed = self.sendPressed, let presentationInterfaceState = self.effectivePresentationInterfaceState?() {
let _ = self.dismissInput()
let effectiveInputText = presentationInterfaceState.interfaceState.composeInputState.inputText
sendPressed(effectiveInputText)
return
}
self.sendMessage(.generic, nil)
}
@objc func textInputBackgroundViewTap(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.ensureFocused()
}
}
public var isFocused: Bool {
if self.imitateFocus {
return true
}
return self.textInputNode?.isFirstResponder() ?? false
}
public func ensureUnfocused() {
self.isTransitioningToTextKeyboard = false
self.textInputNode?.resignFirstResponder()
if self.currentInputMode != .text {
self.currentInputMode = .text
self.requestRelayout(animated: true)
}
self.applyCurrentInputMode(reload: false)
}
public func ensureFocused() {
self.imitateFocus = false
if self.textInputNode == nil {
self.loadTextInputNode()
}
self.applyCurrentInputMode(reload: false)
self.textInputNode?.becomeFirstResponder()
}
public func frameForInputActionButton() -> CGRect? {
if !self.actionButtons.alpha.isZero {
return self.actionButtons.frame.insetBy(dx: 0.0, dy: self.glass ? 0.0 : 6.0).offsetBy(dx: 4.0, dy: 0.0)
}
return nil
}
}