mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2026-04-05 21:21:15 +00:00
Fixes
fix localeWithStrings globally (#30)
Fix badge on zoomed devices. closes #9
Hide channel bottom panel closes #27
Another attempt to fix badge on some Zoomed devices
Force System Share sheet tg://sg/debug
fixes for device badge
New Crowdin updates (#34)
* New translations sglocalizable.strings (Chinese Traditional)
* New translations sglocalizable.strings (Chinese Simplified)
* New translations sglocalizable.strings (Chinese Traditional)
Fix input panel hidden on selection (#31)
* added if check for selectionState != nil
* same order of subnodes
Revert "Fix input panel hidden on selection (#31)"
This reverts commit e8a8bb1496.
Fix input panel for channels Closes #37
Quickly share links with system's share menu
force tabbar when editing
increase height for correct animation
New translations sglocalizable.strings (Ukrainian) (#38)
Hide Post Story button
Fix 10.15.1
Fix archive option for long-tap
Enable in-app Safari
Disable some unsupported purchases
disableDeleteChatSwipeOption + refactor restart alert
Hide bot in suggestions list
Fix merge v11.0
Fix exceptions for safari webview controller
New Crowdin updates (#47)
* New translations sglocalizable.strings (Romanian)
* New translations sglocalizable.strings (French)
* New translations sglocalizable.strings (Spanish)
* New translations sglocalizable.strings (Afrikaans)
* New translations sglocalizable.strings (Arabic)
* New translations sglocalizable.strings (Catalan)
* New translations sglocalizable.strings (Czech)
* New translations sglocalizable.strings (Danish)
* New translations sglocalizable.strings (German)
* New translations sglocalizable.strings (Greek)
* New translations sglocalizable.strings (Finnish)
* New translations sglocalizable.strings (Hebrew)
* New translations sglocalizable.strings (Hungarian)
* New translations sglocalizable.strings (Italian)
* New translations sglocalizable.strings (Japanese)
* New translations sglocalizable.strings (Korean)
* New translations sglocalizable.strings (Dutch)
* New translations sglocalizable.strings (Norwegian)
* New translations sglocalizable.strings (Polish)
* New translations sglocalizable.strings (Portuguese)
* New translations sglocalizable.strings (Serbian (Cyrillic))
* New translations sglocalizable.strings (Swedish)
* New translations sglocalizable.strings (Turkish)
* New translations sglocalizable.strings (Vietnamese)
* New translations sglocalizable.strings (Indonesian)
* New translations sglocalizable.strings (Hindi)
* New translations sglocalizable.strings (Uzbek)
New Crowdin updates (#49)
* New translations sglocalizable.strings (Arabic)
* New translations sglocalizable.strings (Arabic)
New translations sglocalizable.strings (Russian) (#51)
Call confirmation
WIP Settings search
Settings Search
Localize placeholder
Update AccountUtils.swift
mark mutual contact
Align back context action to left
New Crowdin updates (#54)
* New translations sglocalizable.strings (Chinese Simplified)
* New translations sglocalizable.strings (Chinese Traditional)
* New translations sglocalizable.strings (Ukrainian)
Independent Playground app for simulator
New translations sglocalizable.strings (Ukrainian) (#55)
Playground UIKit base and controllers
Inject SwiftUI view with overflow to AsyncDisplayKit
Launch Playgound project on simulator
Create .swiftformat
Move Playground to example
Update .swiftformat
Init SwiftUIViewController
wip
New translations sglocalizable.strings (Chinese Traditional) (#57)
Xcode 16 fixes
Fix
New translations sglocalizable.strings (Italian) (#59)
New translations sglocalizable.strings (Chinese Simplified) (#63)
Force disable CallKit integration due to missing NSE Entitlement
Fix merge
Fix whole chat translator
Sweetpad config
Bump version
11.3.1 fixes
Mutual contact placement fix
Disable Video PIP swipe
Update versions.json
Fix PIP crash
5045 lines
263 KiB
Swift
5045 lines
263 KiB
Swift
// MARK: Swiftgram
|
|
import TelegramUIPreferences
|
|
import SGSimpleSettings
|
|
|
|
import Foundation
|
|
import UIKit
|
|
import Display
|
|
import AsyncDisplayKit
|
|
import SwiftSignalKit
|
|
import Postbox
|
|
import TelegramCore
|
|
import MobileCoreServices
|
|
import TelegramPresentationData
|
|
import TextFormat
|
|
import AccountContext
|
|
import TouchDownGesture
|
|
import ImageTransparency
|
|
import ActivityIndicator
|
|
import AnimationUI
|
|
import Speak
|
|
import ObjCRuntimeUtils
|
|
import AvatarNode
|
|
import ContextUI
|
|
import InvisibleInkDustNode
|
|
import TextInputMenu
|
|
import Pasteboard
|
|
import ChatPresentationInterfaceState
|
|
import ManagedAnimationNode
|
|
import AttachmentUI
|
|
import EditableChatTextNode
|
|
import EmojiTextAttachmentView
|
|
import LottieAnimationComponent
|
|
import ComponentFlow
|
|
import EmojiSuggestionsComponent
|
|
import AudioToolbox
|
|
import ChatControllerInteraction
|
|
import UndoUI
|
|
import PremiumUI
|
|
import StickerPeekUI
|
|
import LottieComponent
|
|
import SolidRoundedButtonNode
|
|
import TooltipUI
|
|
import ChatTextInputMediaRecordingButton
|
|
import ChatContextQuery
|
|
import ChatInputTextNode
|
|
import ChatInputPanelNode
|
|
import TelegramNotices
|
|
import AnimatedCountLabelNode
|
|
import TelegramStringFormatting
|
|
|
|
private let accessoryButtonFont = Font.medium(14.0)
|
|
private let counterFont = Font.with(size: 14.0, design: .regular, traits: [.monospacedNumbers])
|
|
|
|
private final class AccessoryItemIconButtonNode: HighlightTrackingButtonNode {
|
|
private var item: ChatTextInputAccessoryItem
|
|
private var theme: PresentationTheme
|
|
private var strings: PresentationStrings
|
|
private var width: CGFloat
|
|
private let iconImageNode: ASImageNode
|
|
private var animationView: ComponentView<Empty>?
|
|
private var imageEdgeInsets = UIEdgeInsets()
|
|
|
|
init(item: ChatTextInputAccessoryItem, theme: PresentationTheme, strings: PresentationStrings) {
|
|
self.item = item
|
|
self.theme = theme
|
|
self.strings = strings
|
|
|
|
self.iconImageNode = ASImageNode()
|
|
|
|
let (image, text, accessibilityLabel, alpha, insets) = AccessoryItemIconButtonNode.imageAndInsets(item: item, theme: theme, strings: strings)
|
|
|
|
self.width = AccessoryItemIconButtonNode.calculateWidth(item: item, image: image, text: text, strings: strings)
|
|
|
|
super.init(pointerStyle: .circle(30.0))
|
|
|
|
self.isAccessibilityElement = true
|
|
self.accessibilityTraits = [.button]
|
|
|
|
self.iconImageNode.isUserInteractionEnabled = false
|
|
self.addSubnode(self.iconImageNode)
|
|
|
|
switch item {
|
|
case .input, .botInput, .silentPost:
|
|
self.iconImageNode.isHidden = true
|
|
self.animationView = ComponentView<Empty>()
|
|
default:
|
|
break
|
|
}
|
|
|
|
if let text = text {
|
|
self.setAttributedTitle(NSAttributedString(string: text, font: accessoryButtonFont, textColor: theme.chat.inputPanel.inputControlColor), for: .normal)
|
|
} else {
|
|
self.setAttributedTitle(NSAttributedString(), for: .normal)
|
|
}
|
|
|
|
self.iconImageNode.image = image
|
|
self.iconImageNode.alpha = alpha
|
|
self.imageEdgeInsets = insets
|
|
|
|
self.accessibilityLabel = accessibilityLabel
|
|
|
|
self.highligthedChanged = { [weak self] highlighted in
|
|
if let strongSelf = self {
|
|
if highlighted {
|
|
strongSelf.layer.removeAnimation(forKey: "opacity")
|
|
strongSelf.alpha = 0.4
|
|
strongSelf.layer.allowsGroupOpacity = true
|
|
} else {
|
|
strongSelf.alpha = 1.0
|
|
strongSelf.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
|
strongSelf.layer.allowsGroupOpacity = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
|
|
self.theme = theme
|
|
self.strings = strings
|
|
|
|
let (image, text, accessibilityLabel, alpha, insets) = AccessoryItemIconButtonNode.imageAndInsets(item: item, theme: theme, strings: strings)
|
|
|
|
self.width = AccessoryItemIconButtonNode.calculateWidth(item: item, image: image, text: text, strings: strings)
|
|
|
|
if let text = text {
|
|
self.setAttributedTitle(NSAttributedString(string: text, font: accessoryButtonFont, textColor: theme.chat.inputPanel.inputControlColor), for: .normal)
|
|
} else {
|
|
self.setAttributedTitle(NSAttributedString(), for: .normal)
|
|
}
|
|
|
|
self.iconImageNode.image = image
|
|
self.imageEdgeInsets = insets
|
|
self.iconImageNode.alpha = alpha
|
|
|
|
self.accessibilityLabel = accessibilityLabel
|
|
}
|
|
|
|
required init?(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
private static func imageAndInsets(item: ChatTextInputAccessoryItem, theme: PresentationTheme, strings: PresentationStrings) -> (UIImage?, String?, String, CGFloat, UIEdgeInsets) {
|
|
switch item {
|
|
case let .input(isEnabled, inputMode), let .botInput(isEnabled, inputMode):
|
|
switch inputMode {
|
|
case .keyboard:
|
|
return (PresentationResourcesChat.chatInputTextFieldKeyboardImage(theme), nil, strings.VoiceOver_Keyboard, 1.0, UIEdgeInsets())
|
|
case .stickers, .emoji:
|
|
return (PresentationResourcesChat.chatInputTextFieldStickersImage(theme), nil, strings.VoiceOver_Stickers, isEnabled ? 1.0 : 0.4, UIEdgeInsets())
|
|
case .bot:
|
|
return (PresentationResourcesChat.chatInputTextFieldInputButtonsImage(theme), nil, strings.VoiceOver_BotKeyboard, 1.0, UIEdgeInsets())
|
|
}
|
|
case .commands:
|
|
return (PresentationResourcesChat.chatInputTextFieldCommandsImage(theme), nil, strings.VoiceOver_BotCommands, 1.0, UIEdgeInsets())
|
|
case let .silentPost(value):
|
|
if value {
|
|
return (PresentationResourcesChat.chatInputTextFieldSilentPostOnImage(theme), nil, strings.VoiceOver_SilentPostOn, 1.0, UIEdgeInsets())
|
|
} else {
|
|
return (PresentationResourcesChat.chatInputTextFieldSilentPostOffImage(theme), nil, strings.VoiceOver_SilentPostOff, 1.0, UIEdgeInsets())
|
|
}
|
|
case let .messageAutoremoveTimeout(timeout):
|
|
if let timeout = timeout {
|
|
return (nil, shortTimeIntervalString(strings: strings, value: timeout), strings.VoiceOver_SelfDestructTimerOn(timeIntervalString(strings: strings, value: timeout)).string, 1.0, UIEdgeInsets())
|
|
} else {
|
|
return (PresentationResourcesChat.chatInputTextFieldTimerImage(theme), nil, strings.VoiceOver_SelfDestructTimerOff, 1.0, UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0))
|
|
}
|
|
case .scheduledMessages:
|
|
return (PresentationResourcesChat.chatInputTextFieldScheduleImage(theme), nil, strings.VoiceOver_ScheduledMessages, 1.0, UIEdgeInsets())
|
|
case .gift:
|
|
return (PresentationResourcesChat.chatInputTextFieldGiftImage(theme), nil, strings.VoiceOver_GiftPremium, 1.0, UIEdgeInsets())
|
|
}
|
|
}
|
|
|
|
private static func calculateWidth(item: ChatTextInputAccessoryItem, image: UIImage?, text: String?, strings: PresentationStrings) -> CGFloat {
|
|
switch item {
|
|
case .input, .botInput, .silentPost, .commands, .scheduledMessages, .gift:
|
|
return 32.0
|
|
case let .messageAutoremoveTimeout(timeout):
|
|
var imageWidth = (image?.size.width ?? 0.0) + CGFloat(8.0)
|
|
if let _ = timeout, let text = text {
|
|
imageWidth = ceil((text as NSString).size(withAttributes: [.font: accessoryButtonFont]).width) + 10.0
|
|
}
|
|
|
|
return max(imageWidth, 24.0)
|
|
}
|
|
}
|
|
|
|
func updateLayout(item: ChatTextInputAccessoryItem, size: CGSize) {
|
|
let previousItem = self.item
|
|
self.item = item
|
|
|
|
let (updatedImage, text, _, _, _) = AccessoryItemIconButtonNode.imageAndInsets(item: item, theme: self.theme, strings: self.strings)
|
|
|
|
if let image = self.iconImageNode.image {
|
|
self.iconImageNode.image = updatedImage
|
|
|
|
let bottomInset: CGFloat = 0.0
|
|
let imageFrame = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0) - bottomInset), size: image.size)
|
|
self.iconImageNode.frame = imageFrame
|
|
|
|
if let animationView = self.animationView {
|
|
let width = AccessoryItemIconButtonNode.calculateWidth(item: item, image: image, text: "", strings: self.strings)
|
|
//let iconSize = CGSize(width: width, height: width)
|
|
|
|
let animationFrame = CGRect(origin: CGPoint(x: floor((size.width - width) / 2.0), y: floor((size.height - width) / 2.0) - bottomInset), size: CGSize(width: width, height: width))
|
|
|
|
//let colorKeys: [String] = ["__allcolors__"]
|
|
let animationName: String
|
|
var animationMode: LottieAnimationComponent.AnimationItem.Mode = .still(position: .end)
|
|
|
|
if case let .silentPost(muted) = item {
|
|
if case let .silentPost(previousMuted) = previousItem {
|
|
if muted {
|
|
animationName = "input_anim_channelMute"
|
|
} else {
|
|
animationName = "input_anim_channelUnmute"
|
|
}
|
|
if muted != previousMuted {
|
|
animationMode = .animating(loop: false)
|
|
}
|
|
} else {
|
|
animationName = "input_anim_channelMute"
|
|
}
|
|
} else {
|
|
var previousInputMode: ChatTextInputAccessoryItem.InputMode?
|
|
var inputMode: ChatTextInputAccessoryItem.InputMode?
|
|
|
|
switch previousItem {
|
|
case let .input(_, itemInputMode), let .botInput(_, itemInputMode):
|
|
previousInputMode = itemInputMode
|
|
default:
|
|
break
|
|
}
|
|
switch item {
|
|
case let .input(_, itemInputMode), let .botInput(_, itemInputMode):
|
|
inputMode = itemInputMode
|
|
default:
|
|
break
|
|
}
|
|
|
|
if let inputMode = inputMode {
|
|
switch inputMode {
|
|
case .keyboard:
|
|
if let previousInputMode = previousInputMode {
|
|
if case .stickers = previousInputMode {
|
|
animationName = "input_anim_stickerToKey"
|
|
animationMode = .animating(loop: false)
|
|
} else if case .emoji = previousInputMode {
|
|
animationName = "input_anim_smileToKey"
|
|
animationMode = .animating(loop: false)
|
|
} else if case .bot = previousInputMode {
|
|
animationName = "input_anim_botToKey"
|
|
animationMode = .animating(loop: false)
|
|
} else {
|
|
animationName = "input_anim_stickerToKey"
|
|
}
|
|
} else {
|
|
animationName = "input_anim_stickerToKey"
|
|
}
|
|
case .stickers:
|
|
if let previousInputMode = previousInputMode {
|
|
if case .keyboard = previousInputMode {
|
|
animationName = "input_anim_keyToSticker"
|
|
animationMode = .animating(loop: false)
|
|
} else if case .emoji = previousInputMode {
|
|
animationName = "input_anim_smileToSticker"
|
|
animationMode = .animating(loop: false)
|
|
} else {
|
|
animationName = "input_anim_keyToSticker"
|
|
}
|
|
} else {
|
|
animationName = "input_anim_keyToSticker"
|
|
}
|
|
case .emoji:
|
|
if let previousInputMode = previousInputMode {
|
|
if case .keyboard = previousInputMode {
|
|
animationName = "input_anim_keyToSmile"
|
|
animationMode = .animating(loop: false)
|
|
} else if case .stickers = previousInputMode {
|
|
animationName = "input_anim_stickerToSmile"
|
|
animationMode = .animating(loop: false)
|
|
} else {
|
|
animationName = "input_anim_keyToSmile"
|
|
}
|
|
} else {
|
|
animationName = "input_anim_keyToSmile"
|
|
}
|
|
case .bot:
|
|
if let previousInputMode = previousInputMode {
|
|
if case .keyboard = previousInputMode {
|
|
animationName = "input_anim_keyToBot"
|
|
animationMode = .animating(loop: false)
|
|
} else {
|
|
animationName = "input_anim_keyToBot"
|
|
}
|
|
} else {
|
|
animationName = "input_anim_keyToBot"
|
|
}
|
|
}
|
|
} else {
|
|
animationName = ""
|
|
}
|
|
}
|
|
|
|
/*var colors: [String: UIColor] = [:]
|
|
for colorKey in colorKeys {
|
|
colors[colorKey] = self.theme.chat.inputPanel.inputControlColor.blitOver(self.theme.chat.inputPanel.inputBackgroundColor, alpha: 1.0)
|
|
}*/
|
|
|
|
let animationSize = animationView.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(LottieComponent(
|
|
content: LottieComponent.AppBundleContent(name: animationName),
|
|
color: self.theme.chat.inputPanel.inputControlColor.blitOver(self.theme.chat.inputPanel.inputBackgroundColor, alpha: 1.0)
|
|
)),
|
|
environment: {},
|
|
containerSize: animationFrame.size
|
|
)
|
|
if let view = animationView.view as? LottieComponent.View {
|
|
view.isUserInteractionEnabled = false
|
|
if view.superview == nil {
|
|
self.view.addSubview(view)
|
|
}
|
|
view.frame = CGRect(origin: CGPoint(x: animationFrame.minX + floor((animationFrame.width - animationSize.width) / 2.0), y: animationFrame.minY + floor((animationFrame.height - animationSize.height) / 2.0)), size: animationSize)
|
|
|
|
if case .animating = animationMode {
|
|
view.playOnce()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if let text = text {
|
|
self.setAttributedTitle(NSAttributedString(string: text, font: accessoryButtonFont, textColor: self.theme.chat.inputPanel.inputControlColor), for: .normal)
|
|
} else {
|
|
self.setAttributedTitle(NSAttributedString(), for: .normal)
|
|
}
|
|
}
|
|
|
|
var buttonWidth: CGFloat {
|
|
return self.width
|
|
}
|
|
}
|
|
|
|
let chatTextInputMinFontSize: CGFloat = 5.0
|
|
|
|
private let minInputFontSize = chatTextInputMinFontSize
|
|
|
|
private func calclulateTextFieldMinHeight(_ presentationInterfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat {
|
|
let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize)
|
|
var result: CGFloat
|
|
if baseFontSize.isEqual(to: 26.0) {
|
|
result = 42.0
|
|
} else if baseFontSize.isEqual(to: 23.0) {
|
|
result = 38.0
|
|
} else if baseFontSize.isEqual(to: 17.0) {
|
|
result = 31.0
|
|
} else if baseFontSize.isEqual(to: 19.0) {
|
|
result = 33.0
|
|
} else if baseFontSize.isEqual(to: 21.0) {
|
|
result = 35.0
|
|
} else {
|
|
result = 31.0
|
|
}
|
|
|
|
if case .regular = metrics.widthClass {
|
|
result = max(33.0, result)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
private func calculateTextFieldRealInsets(presentationInterfaceState: ChatPresentationInterfaceState, accessoryButtonsWidth: CGFloat) -> UIEdgeInsets {
|
|
let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize)
|
|
let top: CGFloat
|
|
let bottom: CGFloat
|
|
if baseFontSize.isEqual(to: 14.0) {
|
|
top = 2.0
|
|
bottom = 1.0
|
|
} else if baseFontSize.isEqual(to: 15.0) {
|
|
top = 1.0
|
|
bottom = 1.0
|
|
} else if baseFontSize.isEqual(to: 16.0) {
|
|
top = 0.5
|
|
bottom = 0.0
|
|
} else {
|
|
top = 0.0
|
|
bottom = 0.0
|
|
}
|
|
|
|
var right: CGFloat = 0.0
|
|
right += max(0.0, accessoryButtonsWidth - 14.0)
|
|
|
|
return UIEdgeInsets(top: 4.5 + top, left: 0.0, bottom: 5.5 + bottom, right: right)
|
|
}
|
|
|
|
private var currentTextInputBackgroundImage: (UIColor, UIColor, CGFloat, CGFloat, UIImage)?
|
|
private func textInputBackgroundImage(backgroundColor: UIColor?, inputBackgroundColor: UIColor?, strokeColor: UIColor, diameter: CGFloat, strokeWidth: CGFloat) -> UIImage? {
|
|
if let backgroundColor = backgroundColor, let current = currentTextInputBackgroundImage {
|
|
if current.0.isEqual(backgroundColor) && current.1.isEqual(strokeColor) && current.2.isEqual(to: diameter) && current.3.isEqual(to: strokeWidth) {
|
|
return current.4
|
|
}
|
|
}
|
|
|
|
let image = generateImage(CGSize(width: diameter, height: diameter), rotatedContext: { size, context in
|
|
context.clear(CGRect(x: 0.0, y: 0.0, width: diameter, height: diameter))
|
|
|
|
if let inputBackgroundColor = inputBackgroundColor {
|
|
context.setBlendMode(.normal)
|
|
context.setFillColor(inputBackgroundColor.cgColor)
|
|
} else {
|
|
context.setBlendMode(.clear)
|
|
context.setFillColor(UIColor.clear.cgColor)
|
|
}
|
|
context.fillEllipse(in: CGRect(x: 0.0, y: 0.0, width: diameter, height: diameter))
|
|
|
|
context.setBlendMode(.normal)
|
|
context.setStrokeColor(strokeColor.cgColor)
|
|
context.setLineWidth(strokeWidth)
|
|
context.strokeEllipse(in: CGRect(x: strokeWidth / 2.0, y: strokeWidth / 2.0, width: diameter - strokeWidth, height: diameter - strokeWidth))
|
|
})?.stretchableImage(withLeftCapWidth: Int(diameter) / 2, topCapHeight: Int(diameter) / 2)
|
|
if let image = image {
|
|
if let backgroundColor = backgroundColor {
|
|
currentTextInputBackgroundImage = (backgroundColor, strokeColor, diameter, strokeWidth, image)
|
|
}
|
|
return image
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
enum ChatTextInputPanelPasteData {
|
|
case images([UIImage])
|
|
case video(Data)
|
|
case gif(Data)
|
|
case sticker(UIImage, Bool)
|
|
case animatedSticker(Data)
|
|
}
|
|
|
|
final class ChatTextViewForOverlayContent: UIView, ChatInputPanelViewForOverlayContent {
|
|
let ignoreHit: (UIView, CGPoint) -> Bool
|
|
let dismissSuggestions: () -> Void
|
|
|
|
init(ignoreHit: @escaping (UIView, CGPoint) -> Bool, dismissSuggestions: @escaping () -> Void) {
|
|
self.ignoreHit = ignoreHit
|
|
self.dismissSuggestions = dismissSuggestions
|
|
|
|
super.init(frame: CGRect())
|
|
}
|
|
|
|
required init(coder: NSCoder) {
|
|
preconditionFailure()
|
|
}
|
|
|
|
func maybeDismissContent(point: CGPoint) {
|
|
for subview in self.subviews.reversed() {
|
|
if let _ = subview.hitTest(self.convert(point, to: subview), with: nil) {
|
|
return
|
|
}
|
|
}
|
|
|
|
self.dismissSuggestions()
|
|
}
|
|
|
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
for subview in self.subviews.reversed() {
|
|
if let result = subview.hitTest(self.convert(point, to: subview), with: event) {
|
|
return result
|
|
}
|
|
}
|
|
|
|
if event == nil || self.ignoreHit(self, point) {
|
|
return nil
|
|
}
|
|
|
|
self.dismissSuggestions()
|
|
return nil
|
|
}
|
|
}
|
|
|
|
private func makeTextInputTheme(context: AccountContext, interfaceState: ChatPresentationInterfaceState) -> ChatInputTextView.Theme {
|
|
let lineStyle: ChatInputTextView.Theme.Quote.LineStyle
|
|
let authorNameColor: UIColor
|
|
|
|
if let peer = interfaceState.renderedPeer?.peer as? TelegramChannel, case .broadcast = peer.info, let nameColor = peer.nameColor {
|
|
let colors = context.peerNameColors.get(nameColor)
|
|
authorNameColor = colors.main
|
|
|
|
if let secondary = colors.secondary, let tertiary = colors.tertiary {
|
|
lineStyle = .tripleDashed(mainColor: colors.main, secondaryColor: secondary, tertiaryColor: tertiary)
|
|
} else if let secondary = colors.secondary {
|
|
lineStyle = .doubleDashed(mainColor: colors.main, secondaryColor: secondary)
|
|
} else {
|
|
lineStyle = .solid(color: colors.main)
|
|
}
|
|
} else if let accountPeerColor = interfaceState.accountPeerColor {
|
|
authorNameColor = interfaceState.theme.list.itemAccentColor
|
|
|
|
switch accountPeerColor.style {
|
|
case .solid:
|
|
lineStyle = .solid(color: authorNameColor)
|
|
case .doubleDashed:
|
|
lineStyle = .doubleDashed(mainColor: authorNameColor, secondaryColor: .clear)
|
|
case .tripleDashed:
|
|
lineStyle = .tripleDashed(mainColor: authorNameColor, secondaryColor: .clear, tertiaryColor: .clear)
|
|
}
|
|
} else {
|
|
lineStyle = .solid(color: interfaceState.theme.list.itemAccentColor)
|
|
authorNameColor = interfaceState.theme.list.itemAccentColor
|
|
}
|
|
|
|
let codeBackgroundColor: UIColor
|
|
if interfaceState.theme.overallDarkAppearance {
|
|
codeBackgroundColor = UIColor(white: 1.0, alpha: 0.05)
|
|
} else {
|
|
codeBackgroundColor = UIColor(white: 0.0, alpha: 0.05)
|
|
}
|
|
|
|
return ChatInputTextView.Theme(
|
|
quote: ChatInputTextView.Theme.Quote(
|
|
background: authorNameColor.withMultipliedAlpha(interfaceState.theme.overallDarkAppearance ? 0.2 : 0.1),
|
|
foreground: authorNameColor,
|
|
lineStyle: lineStyle,
|
|
codeBackground: codeBackgroundColor,
|
|
codeForeground: authorNameColor
|
|
)
|
|
)
|
|
}
|
|
|
|
class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, ChatInputTextNodeDelegate {
|
|
let clippingNode: ASDisplayNode
|
|
var textPlaceholderNode: ImmediateTextNode
|
|
var textLockIconNode: ASImageNode?
|
|
var contextPlaceholderNode: TextNode?
|
|
var slowmodePlaceholderNode: ChatTextInputSlowmodePlaceholderNode?
|
|
let textInputContainerBackgroundNode: ASImageNode
|
|
let textInputContainer: ASDisplayNode
|
|
var textInputNode: ChatInputTextNode?
|
|
var dustNode: InvisibleInkDustNode?
|
|
var customEmojiContainerView: CustomEmojiContainerView?
|
|
|
|
let textInputBackgroundNode: ASImageNode
|
|
private var transparentTextInputBackgroundImage: UIImage?
|
|
let actionButtons: ChatTextInputActionButtonsNode
|
|
private let slowModeButton: BoostSlowModeButton
|
|
var mediaRecordingAccessibilityArea: AccessibilityAreaNode?
|
|
private let counterTextNode: ImmediateTextNode
|
|
|
|
let menuButton: HighlightTrackingButtonNode
|
|
private let menuButtonBackgroundNode: ASDisplayNode
|
|
private let menuButtonClippingNode: ASDisplayNode
|
|
private let menuButtonIconNode: MenuIconNode
|
|
private let menuButtonTextNode: ImmediateTextNode
|
|
|
|
private let startButton: SolidRoundedButtonNode
|
|
|
|
let sendAsAvatarButtonNode: HighlightableButtonNode
|
|
let sendAsAvatarReferenceNode: ContextReferenceContentNode
|
|
let sendAsAvatarContainerNode: ContextControllerSourceNode
|
|
private let sendAsAvatarNode: AvatarNode
|
|
|
|
let attachmentButton: HighlightableButtonNode
|
|
let attachmentButtonDisabledNode: HighlightableButtonNode
|
|
let searchLayoutClearButton: HighlightableButton
|
|
private let searchLayoutClearImageNode: ASImageNode
|
|
private var searchActivityIndicator: ActivityIndicator?
|
|
var audioRecordingInfoContainerNode: ASDisplayNode?
|
|
var audioRecordingDotNode: AnimationNode?
|
|
var audioRecordingDotNodeDismissed = false
|
|
var audioRecordingTimeNode: ChatTextInputAudioRecordingTimeNode?
|
|
var audioRecordingCancelIndicator: ChatTextInputAudioRecordingCancelIndicator?
|
|
var animatingBinNode: AnimationNode?
|
|
|
|
var viewOnce = false
|
|
let viewOnceButton: ChatRecordingViewOnceButtonNode
|
|
|
|
private var accessoryItemButtons: [(ChatTextInputAccessoryItem, AccessoryItemIconButtonNode)] = []
|
|
|
|
private var validLayout: (CGFloat, CGFloat, CGFloat, CGFloat, UIEdgeInsets, CGFloat, LayoutMetrics, Bool, Bool)?
|
|
private var leftMenuInset: CGFloat = 0.0
|
|
private var rightSlowModeInset: CGFloat = 0.0
|
|
private var currentTextInputBackgroundWidthOffset: CGFloat = 0.0
|
|
|
|
var displayAttachmentMenu: () -> Void = { }
|
|
var sendMessage: () -> Void = { }
|
|
var paste: (ChatTextInputPanelPasteData) -> Void = { _ in }
|
|
var updateHeight: (Bool) -> Void = { _ in }
|
|
var toggleExpandMediaInput: (() -> Void)?
|
|
var switchToTextInputIfNeeded: (() -> Void)?
|
|
|
|
var updateActivity: () -> Void = { }
|
|
|
|
private var updatingInputState = false
|
|
|
|
private var currentPlaceholder: String?
|
|
private var sendingTextDisabled: Bool = false
|
|
|
|
private var presentationInterfaceState: ChatPresentationInterfaceState?
|
|
private var initializedPlaceholder = false
|
|
|
|
private var keepSendButtonEnabled = false
|
|
private var extendedSearchLayout = false
|
|
|
|
var isMediaDeleted: Bool = false
|
|
private var recordingPaused = false
|
|
|
|
private let inputMenu: TextInputMenu
|
|
|
|
private var theme: PresentationTheme?
|
|
private var strings: PresentationStrings?
|
|
|
|
private let hapticFeedback = HapticFeedback()
|
|
|
|
// MARK: Swiftgram
|
|
private var sendWithReturnKey: Bool
|
|
private var sendWithReturnKeyDisposable: Disposable?
|
|
|
|
var inputTextState: ChatTextInputState {
|
|
if let textInputNode = self.textInputNode {
|
|
let selectionRange: Range<Int> = textInputNode.selectedRange.location ..< (textInputNode.selectedRange.location + textInputNode.selectedRange.length)
|
|
return ChatTextInputState(inputText: stateAttributedStringForText(textInputNode.attributedText ?? NSAttributedString()), selectionRange: selectionRange)
|
|
} else {
|
|
return ChatTextInputState()
|
|
}
|
|
}
|
|
|
|
var storedInputLanguage: String?
|
|
var effectiveInputLanguage: String? {
|
|
if let textInputNode = textInputNode, textInputNode.isFirstResponder() {
|
|
return textInputNode.textInputMode?.primaryLanguage
|
|
} else {
|
|
return self.storedInputLanguage
|
|
}
|
|
}
|
|
|
|
var enablePredictiveInput: Bool = true {
|
|
didSet {
|
|
if let textInputNode = self.textInputNode {
|
|
textInputNode.textView.autocorrectionType = self.enablePredictiveInput ? .default : .no
|
|
}
|
|
}
|
|
}
|
|
|
|
override var context: AccountContext? {
|
|
didSet {
|
|
self.actionButtons.micButton.statusBarHost = self.context?.sharedContext.mainWindow?.statusBarHost
|
|
}
|
|
}
|
|
|
|
var micButton: ChatTextInputMediaRecordingButton? {
|
|
return self.actionButtons.micButton
|
|
}
|
|
|
|
private let startingBotDisposable = MetaDisposable()
|
|
private let statusDisposable = MetaDisposable()
|
|
override var interfaceInteraction: ChatPanelInterfaceInteraction? {
|
|
didSet {
|
|
if let statuses = self.interfaceInteraction?.statuses {
|
|
self.statusDisposable.set((statuses.inlineSearch
|
|
|> distinctUntilChanged
|
|
|> deliverOnMainQueue).startStrict(next: { [weak self] value in
|
|
self?.updateIsProcessingInlineRequest(value)
|
|
}).strict())
|
|
}
|
|
if let startingBot = self.interfaceInteraction?.statuses?.startingBot {
|
|
self.startingBotDisposable.set((startingBot |> deliverOnMainQueue).startStrict(next: { [weak self] value in
|
|
if let strongSelf = self {
|
|
strongSelf.startingBotProgress = value
|
|
}
|
|
}).strict())
|
|
}
|
|
}
|
|
}
|
|
|
|
private var startingBotProgress = false {
|
|
didSet {
|
|
// if self.startingBotProgress != oldValue {
|
|
// if self.startingBotProgress {
|
|
// self.startButton.transitionToProgress()
|
|
// } else {
|
|
// self.startButton.transitionFromProgress()
|
|
// }
|
|
// }
|
|
}
|
|
}
|
|
|
|
func updateInputTextState(_ state: ChatTextInputState, keepSendButtonEnabled: Bool, extendedSearchLayout: Bool, accessoryItems: [ChatTextInputAccessoryItem], animated: Bool) {
|
|
if let currentState = self.presentationInterfaceState {
|
|
var updateAccessoryButtons = false
|
|
if accessoryItems.count == self.accessoryItemButtons.count {
|
|
for i in 0 ..< accessoryItems.count {
|
|
if accessoryItems[i] != self.accessoryItemButtons[i].0 {
|
|
updateAccessoryButtons = true
|
|
break
|
|
}
|
|
}
|
|
} else {
|
|
updateAccessoryButtons = true
|
|
}
|
|
|
|
if updateAccessoryButtons {
|
|
var updatedButtons: [(ChatTextInputAccessoryItem, AccessoryItemIconButtonNode)] = []
|
|
for item in accessoryItems {
|
|
var itemAndButton: (ChatTextInputAccessoryItem, AccessoryItemIconButtonNode)?
|
|
for i in 0 ..< self.accessoryItemButtons.count {
|
|
if self.accessoryItemButtons[i].0.key == item.key {
|
|
itemAndButton = self.accessoryItemButtons[i]
|
|
itemAndButton?.0 = item
|
|
self.accessoryItemButtons.remove(at: i)
|
|
break
|
|
}
|
|
}
|
|
if itemAndButton == nil {
|
|
let button = AccessoryItemIconButtonNode(item: item, theme: currentState.theme, strings: currentState.strings)
|
|
button.addTarget(self, action: #selector(self.accessoryItemButtonPressed(_:)), forControlEvents: .touchUpInside)
|
|
itemAndButton = (item, button)
|
|
}
|
|
updatedButtons.append(itemAndButton!)
|
|
}
|
|
for (_, button) in self.accessoryItemButtons {
|
|
button.removeFromSupernode()
|
|
}
|
|
self.accessoryItemButtons = updatedButtons
|
|
}
|
|
}
|
|
|
|
if state.inputText.length != 0 && self.textInputNode == nil {
|
|
self.loadTextInputNode()
|
|
}
|
|
|
|
if let textInputNode = self.textInputNode, let _ = self.presentationInterfaceState, let context = self.context {
|
|
self.updatingInputState = true
|
|
|
|
var textColor: UIColor = .black
|
|
var accentTextColor: UIColor = .blue
|
|
var baseFontSize: CGFloat = 17.0
|
|
if let presentationInterfaceState = self.presentationInterfaceState {
|
|
textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor
|
|
accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor
|
|
baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize)
|
|
}
|
|
textInputNode.attributedText = textAttributedStringForStateText(context: context, stateText: state.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed, availableEmojis: (self.context?.animatedEmojiStickersValue.keys).flatMap(Set.init) ?? Set(), emojiViewProvider: self.emojiViewProvider, makeCollapsedQuoteAttachment: { text, attributes in
|
|
return ChatInputTextCollapsedQuoteAttachmentImpl(text: text, attributes: attributes)
|
|
})
|
|
textInputNode.selectedRange = NSMakeRange(state.selectionRange.lowerBound, state.selectionRange.count)
|
|
|
|
if let presentationInterfaceState = self.presentationInterfaceState {
|
|
refreshChatTextInputAttributes(context: context, textView: textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: (self.context?.animatedEmojiStickersValue.keys).flatMap(Set.init) ?? Set(), emojiViewProvider: self.emojiViewProvider, makeCollapsedQuoteAttachment: { text, attributes in
|
|
return ChatInputTextCollapsedQuoteAttachmentImpl(text: text, attributes: attributes)
|
|
})
|
|
}
|
|
|
|
self.updatingInputState = false
|
|
self.keepSendButtonEnabled = keepSendButtonEnabled
|
|
self.extendedSearchLayout = extendedSearchLayout
|
|
self.updateTextNodeText(animated: animated)
|
|
self.updateSpoiler()
|
|
}
|
|
}
|
|
|
|
func updateKeepSendButtonEnabled(keepSendButtonEnabled: Bool, extendedSearchLayout: Bool, animated: Bool) {
|
|
if keepSendButtonEnabled != self.keepSendButtonEnabled || extendedSearchLayout != self.extendedSearchLayout {
|
|
self.keepSendButtonEnabled = keepSendButtonEnabled
|
|
self.extendedSearchLayout = extendedSearchLayout
|
|
self.updateTextNodeText(animated: animated)
|
|
}
|
|
}
|
|
|
|
var text: String {
|
|
get {
|
|
return self.textInputNode?.attributedText?.string ?? ""
|
|
} set(value) {
|
|
if let textInputNode = self.textInputNode {
|
|
var textColor: UIColor = .black
|
|
var baseFontSize: CGFloat = 17.0
|
|
if let presentationInterfaceState = self.presentationInterfaceState {
|
|
textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor
|
|
baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize)
|
|
}
|
|
textInputNode.attributedText = NSAttributedString(string: value, font: Font.regular(baseFontSize), textColor: textColor)
|
|
self.chatInputTextNodeDidUpdateText()
|
|
}
|
|
}
|
|
}
|
|
|
|
private let textInputViewInternalInsets: UIEdgeInsets
|
|
private let accessoryButtonSpacing: CGFloat = 0.0
|
|
private let accessoryButtonInset: CGFloat = 2.0
|
|
|
|
private var spoilersRevealed = false
|
|
|
|
private var animatingTransition = false
|
|
var finishedTransitionToPreview: Bool?
|
|
|
|
private var touchDownGestureRecognizer: TouchDownGestureRecognizer?
|
|
|
|
var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?
|
|
|
|
private let presentationContext: ChatPresentationContext?
|
|
|
|
private var tooltipController: TooltipScreen?
|
|
|
|
init(context: AccountContext, presentationInterfaceState: ChatPresentationInterfaceState, presentationContext: ChatPresentationContext?, presentController: @escaping (ViewController) -> Void) {
|
|
self.presentationInterfaceState = presentationInterfaceState
|
|
self.presentationContext = presentationContext
|
|
|
|
self.textInputViewInternalInsets = UIEdgeInsets(top: 1.0, left: 13.0, bottom: 1.0, right: 13.0)
|
|
|
|
var hasSpoilers = true
|
|
var hasQuotes = true
|
|
if presentationInterfaceState.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat {
|
|
hasSpoilers = false
|
|
hasQuotes = false
|
|
}
|
|
self.inputMenu = TextInputMenu(hasSpoilers: hasSpoilers, hasQuotes: hasQuotes)
|
|
|
|
self.clippingNode = ASDisplayNode()
|
|
self.clippingNode.clipsToBounds = true
|
|
|
|
self.textInputContainerBackgroundNode = ASImageNode()
|
|
self.textInputContainerBackgroundNode.isUserInteractionEnabled = false
|
|
self.textInputContainerBackgroundNode.displaysAsynchronously = false
|
|
|
|
self.textInputContainer = ASDisplayNode()
|
|
self.textInputContainer.addSubnode(self.textInputContainerBackgroundNode)
|
|
self.textInputContainer.clipsToBounds = true
|
|
|
|
self.textInputBackgroundNode = ASImageNode()
|
|
self.textInputBackgroundNode.displaysAsynchronously = false
|
|
self.textInputBackgroundNode.displayWithoutProcessing = true
|
|
self.textPlaceholderNode = ImmediateTextNode()
|
|
self.textPlaceholderNode.contentMode = .topLeft
|
|
self.textPlaceholderNode.contentsScale = UIScreenScale
|
|
self.textPlaceholderNode.maximumNumberOfLines = 1
|
|
self.textPlaceholderNode.isUserInteractionEnabled = false
|
|
|
|
self.menuButton = HighlightTrackingButtonNode()
|
|
self.menuButton.clipsToBounds = true
|
|
self.menuButton.cornerRadius = 16.0
|
|
self.menuButton.accessibilityLabel = presentationInterfaceState.strings.Conversation_InputMenu
|
|
self.menuButtonBackgroundNode = ASDisplayNode()
|
|
self.menuButtonBackgroundNode.isUserInteractionEnabled = false
|
|
self.menuButtonClippingNode = ASDisplayNode()
|
|
self.menuButtonClippingNode.clipsToBounds = true
|
|
self.menuButtonClippingNode.isUserInteractionEnabled = false
|
|
|
|
self.menuButtonIconNode = MenuIconNode()
|
|
self.menuButtonIconNode.isUserInteractionEnabled = false
|
|
self.menuButtonIconNode.customColor = presentationInterfaceState.theme.chat.inputPanel.actionControlForegroundColor
|
|
self.menuButtonTextNode = ImmediateTextNode()
|
|
|
|
self.startButton = SolidRoundedButtonNode(title: presentationInterfaceState.strings.Bot_Start, theme: SolidRoundedButtonTheme(theme: presentationInterfaceState.theme), height: 50.0, cornerRadius: 11.0, gloss: true)
|
|
self.startButton.progressType = .embedded
|
|
self.startButton.isHidden = true
|
|
|
|
self.sendAsAvatarButtonNode = HighlightableButtonNode()
|
|
self.sendAsAvatarReferenceNode = ContextReferenceContentNode()
|
|
self.sendAsAvatarContainerNode = ContextControllerSourceNode()
|
|
self.sendAsAvatarContainerNode.animateScale = false
|
|
self.sendAsAvatarNode = AvatarNode(font: avatarPlaceholderFont(size: 16.0))
|
|
|
|
self.attachmentButton = HighlightableButtonNode(pointerStyle: .circle(36.0))
|
|
self.attachmentButton.accessibilityLabel = presentationInterfaceState.strings.VoiceOver_AttachMedia
|
|
self.attachmentButton.accessibilityTraits = [.button]
|
|
self.attachmentButton.isAccessibilityElement = true
|
|
self.attachmentButtonDisabledNode = HighlightableButtonNode()
|
|
self.searchLayoutClearButton = HighlightableButton()
|
|
self.searchLayoutClearImageNode = ASImageNode()
|
|
self.searchLayoutClearImageNode.isUserInteractionEnabled = false
|
|
self.searchLayoutClearButton.addSubnode(self.searchLayoutClearImageNode)
|
|
|
|
self.actionButtons = ChatTextInputActionButtonsNode(context: context, presentationInterfaceState: presentationInterfaceState, presentationContext: presentationContext, presentController: presentController)
|
|
self.counterTextNode = ImmediateTextNode()
|
|
self.counterTextNode.textAlignment = .center
|
|
|
|
self.slowModeButton = BoostSlowModeButton()
|
|
self.slowModeButton.alpha = 0.0
|
|
|
|
self.viewOnceButton = ChatRecordingViewOnceButtonNode(icon: .viewOnce)
|
|
self.sendWithReturnKey = SGUISettings.default.sendWithReturnKey
|
|
|
|
super.init()
|
|
|
|
self.slowModeButton.requestUpdate = { [weak self] in
|
|
self?.requestLayout(transition: .animated(duration: 0.2, curve: .easeInOut))
|
|
}
|
|
self.slowModeButton.addTarget(self, action: #selector(self.slowModeButtonPressed), forControlEvents: .touchUpInside)
|
|
|
|
self.viewForOverlayContent = ChatTextViewForOverlayContent(
|
|
ignoreHit: { [weak self] view, point in
|
|
guard let strongSelf = self else {
|
|
return false
|
|
}
|
|
if strongSelf.view.hitTest(view.convert(point, to: strongSelf.view), with: nil) != nil {
|
|
return true
|
|
}
|
|
if view.convert(point, to: strongSelf.view).y > strongSelf.view.bounds.maxY {
|
|
return true
|
|
}
|
|
return false
|
|
},
|
|
dismissSuggestions: { [weak self] in
|
|
guard let strongSelf = self, let currentEmojiSuggestion = strongSelf.currentEmojiSuggestion, let textInputNode = strongSelf.textInputNode else {
|
|
return
|
|
}
|
|
|
|
strongSelf.dismissedEmojiSuggestionPosition = currentEmojiSuggestion.position
|
|
strongSelf.updateInputField(textInputFrame: textInputNode.frame, transition: .immediate)
|
|
}
|
|
)
|
|
|
|
self.context = context
|
|
|
|
// MARK: Swiftgram
|
|
let sendWithReturnKeySignal = context.account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.SGUISettings])
|
|
|> map { view -> Bool in
|
|
let settings: SGUISettings = view.values[ApplicationSpecificPreferencesKeys.SGUISettings]?.get(SGUISettings.self) ?? .default
|
|
return settings.sendWithReturnKey
|
|
}
|
|
|> distinctUntilChanged
|
|
|
|
self.sendWithReturnKeyDisposable = (sendWithReturnKeySignal
|
|
|> deliverOnMainQueue).startStrict(next: { [weak self] value in
|
|
if let strongSelf = self {
|
|
strongSelf.sendWithReturnKey = value
|
|
if let textInputNode = strongSelf.textInputNode {
|
|
textInputNode.textView.returnKeyType = strongSelf.sendWithReturnKey ? .send : .default
|
|
textInputNode.textView.reloadInputViews()
|
|
}
|
|
}
|
|
})
|
|
|
|
self.addSubnode(self.clippingNode)
|
|
|
|
self.sendAsAvatarContainerNode.activated = { [weak self] gesture, _ in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.interfaceInteraction?.openSendAsPeer(strongSelf.sendAsAvatarReferenceNode, gesture)
|
|
}
|
|
|
|
self.sendAsAvatarButtonNode.addTarget(self, action: #selector(self.sendAsAvatarButtonPressed), forControlEvents: .touchUpInside)
|
|
self.sendAsAvatarButtonNode.highligthedChanged = { [weak self] highlighted in
|
|
if let strongSelf = self {
|
|
if highlighted {
|
|
let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .spring)
|
|
transition.updateTransformScale(node: strongSelf.sendAsAvatarButtonNode, scale: 0.85)
|
|
} else {
|
|
let transition: ContainedViewLayoutTransition = .animated(duration: 0.5, curve: .spring)
|
|
transition.updateTransformScale(node: strongSelf.sendAsAvatarButtonNode, scale: 1.0)
|
|
}
|
|
}
|
|
}
|
|
|
|
self.menuButton.addTarget(self, action: #selector(self.menuButtonPressed), forControlEvents: .touchUpInside)
|
|
self.menuButton.highligthedChanged = { [weak self] highlighted in
|
|
if let strongSelf = self {
|
|
if highlighted {
|
|
let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .spring)
|
|
transition.updateTransformScale(node: strongSelf.menuButton, scale: 0.85)
|
|
} else {
|
|
let transition: ContainedViewLayoutTransition = .animated(duration: 0.5, curve: .spring)
|
|
transition.updateTransformScale(node: strongSelf.menuButton, scale: 1.0)
|
|
}
|
|
}
|
|
}
|
|
|
|
self.startButton.pressed = { [weak self] in
|
|
guard let self, let presentationInterfaceState = self.presentationInterfaceState else {
|
|
return
|
|
}
|
|
if presentationInterfaceState.peerIsBlocked {
|
|
self.interfaceInteraction?.unblockPeer()
|
|
} else {
|
|
self.interfaceInteraction?.sendBotStart(presentationInterfaceState.botStartPayload)
|
|
}
|
|
|
|
if let tooltipController = self.tooltipController {
|
|
self.tooltipController = nil
|
|
tooltipController.dismiss()
|
|
}
|
|
}
|
|
|
|
self.attachmentButton.addTarget(self, action: #selector(self.attachmentButtonPressed), forControlEvents: .touchUpInside)
|
|
self.attachmentButtonDisabledNode.addTarget(self, action: #selector(self.attachmentButtonPressed), forControlEvents: .touchUpInside)
|
|
// MARK: Swiftgram
|
|
let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(self.attachmentButtonLongPressed(_:)))
|
|
longPressGesture.minimumPressDuration = 1.0
|
|
self.attachmentButton.view.addGestureRecognizer(longPressGesture)
|
|
|
|
self.actionButtons.sendButtonLongPressed = { [weak self] node, gesture in
|
|
self?.interfaceInteraction?.displaySendMessageOptions(node, gesture)
|
|
}
|
|
|
|
self.actionButtons.micButton.recordingDisabled = { [weak self] in
|
|
if let strongSelf = self {
|
|
if strongSelf.presentationInterfaceState?.voiceMessagesAvailable == false {
|
|
self?.interfaceInteraction?.displayRestrictedInfo(.premiumVoiceMessages, .tooltip)
|
|
} else {
|
|
self?.interfaceInteraction?.displayRestrictedInfo(.mediaRecording, .tooltip)
|
|
}
|
|
}
|
|
}
|
|
|
|
self.actionButtons.micButton.beginRecording = { [weak self] in
|
|
if let strongSelf = self, let presentationInterfaceState = strongSelf.presentationInterfaceState, let interfaceInteraction = strongSelf.interfaceInteraction {
|
|
let isVideo: Bool
|
|
switch presentationInterfaceState.interfaceState.mediaRecordingMode {
|
|
case .audio:
|
|
isVideo = false
|
|
case .video:
|
|
isVideo = true
|
|
}
|
|
interfaceInteraction.beginMediaRecording(isVideo)
|
|
}
|
|
}
|
|
self.actionButtons.micButton.endRecording = { [weak self] sendMedia in
|
|
if let strongSelf = self, let interfaceState = strongSelf.presentationInterfaceState, let interfaceInteraction = strongSelf.interfaceInteraction {
|
|
if let _ = interfaceState.inputTextPanelState.mediaRecordingState {
|
|
if sendMedia {
|
|
interfaceInteraction.finishMediaRecording(.send(viewOnce: strongSelf.viewOnce))
|
|
} else {
|
|
interfaceInteraction.finishMediaRecording(.dismiss)
|
|
}
|
|
} else {
|
|
// interfaceInteraction.finishMediaRecording(.dismiss)
|
|
}
|
|
strongSelf.viewOnce = false
|
|
strongSelf.tooltipController?.dismiss()
|
|
}
|
|
}
|
|
self.actionButtons.micButton.offsetRecordingControls = { [weak self] in
|
|
if let strongSelf = self, let presentationInterfaceState = strongSelf.presentationInterfaceState {
|
|
if let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, metrics, isSecondary, isMediaInputExpanded) = strongSelf.validLayout {
|
|
let _ = strongSelf.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: .immediate, interfaceState: presentationInterfaceState, metrics: metrics, isMediaInputExpanded: isMediaInputExpanded)
|
|
}
|
|
}
|
|
}
|
|
self.actionButtons.micButton.updateCancelTranslation = { [weak self] in
|
|
if let strongSelf = self, let presentationInterfaceState = strongSelf.presentationInterfaceState {
|
|
if let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, metrics, isSecondary, isMediaInputExpanded) = strongSelf.validLayout {
|
|
let _ = strongSelf.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: .immediate, interfaceState: presentationInterfaceState, metrics: metrics, isMediaInputExpanded: isMediaInputExpanded)
|
|
}
|
|
}
|
|
}
|
|
self.actionButtons.micButton.stopRecording = { [weak self] in
|
|
if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction {
|
|
interfaceInteraction.stopMediaRecording()
|
|
|
|
strongSelf.tooltipController?.dismiss()
|
|
}
|
|
}
|
|
self.actionButtons.micButton.updateLocked = { [weak self] _ in
|
|
if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction {
|
|
interfaceInteraction.lockMediaRecording()
|
|
}
|
|
}
|
|
self.actionButtons.micButton.switchMode = { [weak self] in
|
|
if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction {
|
|
interfaceInteraction.switchMediaRecordingMode()
|
|
}
|
|
}
|
|
|
|
self.actionButtons.sendButton.addTarget(self, action: #selector(self.sendButtonPressed), forControlEvents: .touchUpInside)
|
|
self.actionButtons.sendContainerNode.alpha = 0.0
|
|
self.actionButtons.updateAccessibility()
|
|
|
|
self.actionButtons.expandMediaInputButton.addTarget(self, action: #selector(self.expandButtonPressed), forControlEvents: .touchUpInside)
|
|
self.actionButtons.expandMediaInputButton.alpha = 0.0
|
|
|
|
self.searchLayoutClearButton.addTarget(self, action: #selector(self.searchLayoutClearButtonPressed), for: .touchUpInside)
|
|
self.searchLayoutClearButton.alpha = 0.0
|
|
|
|
self.clippingNode.addSubnode(self.textInputContainer)
|
|
self.clippingNode.addSubnode(self.textInputBackgroundNode)
|
|
|
|
self.clippingNode.addSubnode(self.textPlaceholderNode)
|
|
|
|
self.menuButton.addSubnode(self.menuButtonBackgroundNode)
|
|
self.menuButton.addSubnode(self.menuButtonClippingNode)
|
|
self.menuButtonClippingNode.addSubnode(self.menuButtonTextNode)
|
|
self.menuButton.addSubnode(self.menuButtonIconNode)
|
|
|
|
self.sendAsAvatarContainerNode.addSubnode(self.sendAsAvatarReferenceNode)
|
|
self.sendAsAvatarReferenceNode.addSubnode(self.sendAsAvatarNode)
|
|
self.sendAsAvatarButtonNode.addSubnode(self.sendAsAvatarContainerNode)
|
|
self.clippingNode.addSubnode(self.sendAsAvatarButtonNode)
|
|
|
|
self.clippingNode.addSubnode(self.menuButton)
|
|
self.clippingNode.addSubnode(self.attachmentButton)
|
|
self.clippingNode.addSubnode(self.attachmentButtonDisabledNode)
|
|
|
|
self.clippingNode.addSubnode(self.startButton)
|
|
|
|
self.clippingNode.addSubnode(self.actionButtons)
|
|
self.clippingNode.addSubnode(self.counterTextNode)
|
|
|
|
self.clippingNode.addSubnode(self.slowModeButton)
|
|
|
|
self.clippingNode.view.addSubview(self.searchLayoutClearButton)
|
|
|
|
self.textInputBackgroundNode.clipsToBounds = true
|
|
let recognizer = TouchDownGestureRecognizer(target: self, action: #selector(self.textInputBackgroundViewTap(_:)))
|
|
recognizer.touchDown = { [weak self] in
|
|
if let strongSelf = self {
|
|
if strongSelf.sendingTextDisabled {
|
|
guard let controller = strongSelf.interfaceInteraction?.chatController() as? ChatControllerImpl else {
|
|
return
|
|
}
|
|
|
|
if let boostsToUnrestrict = strongSelf.presentationInterfaceState?.boostsToUnrestrict, boostsToUnrestrict > 0 {
|
|
strongSelf.interfaceInteraction?.openBoostToUnrestrict()
|
|
return
|
|
}
|
|
|
|
controller.controllerInteraction?.displayUndo(.universal(animation: "premium_unlock", scale: 1.0, colors: ["__allcolors__": UIColor(white: 1.0, alpha: 1.0)], title: nil, text: controller.restrictedSendingContentsText(), customUndoText: nil, timeout: nil))
|
|
} else {
|
|
strongSelf.ensureFocused()
|
|
}
|
|
}
|
|
}
|
|
recognizer.waitForTouchUp = { [weak self] in
|
|
guard let strongSelf = self, let textInputNode = strongSelf.textInputNode else {
|
|
return true
|
|
}
|
|
|
|
if textInputNode.textView.isFirstResponder {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
self.textInputBackgroundNode.isUserInteractionEnabled = true
|
|
self.textInputBackgroundNode.view.addGestureRecognizer(recognizer)
|
|
|
|
if let presentationContext = presentationContext {
|
|
self.emojiViewProvider = { [weak self, weak presentationContext] emoji in
|
|
guard let strongSelf = self, let presentationContext = presentationContext, let presentationInterfaceState = strongSelf.presentationInterfaceState, let context = strongSelf.context else {
|
|
return UIView()
|
|
}
|
|
|
|
let pointSize = floor(24.0 * 1.3)
|
|
return EmojiTextAttachmentView(context: context, userLocation: .other, emoji: emoji, file: emoji.file, cache: presentationContext.animationCache, renderer: presentationContext.animationRenderer, placeholderColor: presentationInterfaceState.theme.chat.inputPanel.inputTextColor.withAlphaComponent(0.12), pointSize: CGSize(width: pointSize, height: pointSize))
|
|
}
|
|
}
|
|
|
|
self.viewOnceButton.addTarget(self, action: #selector(self.viewOncePressed), forControlEvents: [.touchUpInside])
|
|
}
|
|
|
|
required init?(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
deinit {
|
|
self.statusDisposable.dispose()
|
|
self.sendWithReturnKeyDisposable?.dispose()
|
|
self.startingBotDisposable.dispose()
|
|
self.tooltipController?.dismiss()
|
|
self.currentEmojiSuggestion?.disposable.dispose()
|
|
}
|
|
|
|
override func didLoad() {
|
|
super.didLoad()
|
|
|
|
if let viewForOverlayContent = self.viewForOverlayContent {
|
|
viewForOverlayContent.addSubnode(self.viewOnceButton)
|
|
}
|
|
}
|
|
|
|
func loadTextInputNodeIfNeeded() {
|
|
if self.textInputNode == nil {
|
|
self.loadTextInputNode()
|
|
}
|
|
}
|
|
|
|
private func loadTextInputNode() {
|
|
let textInputNode = ChatInputTextNode()
|
|
textInputNode.initialPrimaryLanguage = self.presentationInterfaceState?.interfaceState.inputLanguage
|
|
var textColor: UIColor = .black
|
|
var tintColor: UIColor = .blue
|
|
var baseFontSize: CGFloat = 17.0
|
|
var keyboardAppearance: UIKeyboardAppearance = UIKeyboardAppearance.default
|
|
if let context = self.context, let presentationInterfaceState = self.presentationInterfaceState {
|
|
textInputNode.textView.theme = makeTextInputTheme(context: context, interfaceState: presentationInterfaceState)
|
|
|
|
textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor
|
|
tintColor = presentationInterfaceState.theme.list.itemAccentColor
|
|
baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize)
|
|
keyboardAppearance = presentationInterfaceState.theme.rootController.keyboardColor.keyboardAppearance
|
|
}
|
|
|
|
let paragraphStyle = NSMutableParagraphStyle()
|
|
paragraphStyle.lineSpacing = 1.0
|
|
paragraphStyle.lineHeightMultiple = 1.0
|
|
paragraphStyle.paragraphSpacing = 1.0
|
|
paragraphStyle.maximumLineHeight = 20.0
|
|
paragraphStyle.minimumLineHeight = 20.0
|
|
|
|
textInputNode.textView.typingAttributes = [NSAttributedString.Key.font: Font.regular(max(minInputFontSize, baseFontSize)), NSAttributedString.Key.foregroundColor: textColor, NSAttributedString.Key.paragraphStyle: paragraphStyle]
|
|
textInputNode.clipsToBounds = false
|
|
textInputNode.textView.clipsToBounds = false
|
|
textInputNode.delegate = self
|
|
textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0)
|
|
textInputNode.keyboardAppearance = keyboardAppearance
|
|
textInputNode.tintColor = tintColor
|
|
textInputNode.textView.scrollIndicatorInsets = UIEdgeInsets(top: 9.0, left: 0.0, bottom: 9.0, right: -13.0)
|
|
self.textInputContainer.addSubnode(textInputNode)
|
|
textInputNode.view.disablesInteractiveTransitionGestureRecognizer = true
|
|
textInputNode.isUserInteractionEnabled = !self.sendingTextDisabled
|
|
textInputNode.textView.returnKeyType = self.sendWithReturnKey ? .send : .default
|
|
self.textInputNode = textInputNode
|
|
|
|
var accessoryButtonsWidth: CGFloat = 0.0
|
|
var firstButton = true
|
|
for (_, button) in self.accessoryItemButtons {
|
|
if firstButton {
|
|
firstButton = false
|
|
accessoryButtonsWidth += accessoryButtonInset
|
|
} else {
|
|
accessoryButtonsWidth += accessoryButtonSpacing
|
|
}
|
|
accessoryButtonsWidth += button.buttonWidth
|
|
}
|
|
|
|
if let presentationInterfaceState = self.presentationInterfaceState {
|
|
refreshChatTextInputTypingAttributes(textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize)
|
|
textInputNode.textContainerInset = calculateTextFieldRealInsets(presentationInterfaceState: presentationInterfaceState, accessoryButtonsWidth: accessoryButtonsWidth)
|
|
}
|
|
|
|
if !self.textInputContainer.bounds.size.width.isZero {
|
|
let textInputFrame = self.textInputContainer.frame
|
|
|
|
textInputNode.frame = CGRect(origin: CGPoint(x: self.textInputViewInternalInsets.left, y: self.textInputViewInternalInsets.top), size: CGSize(width: textInputFrame.size.width - (self.textInputViewInternalInsets.left + self.textInputViewInternalInsets.right), height: textInputFrame.size.height - self.textInputViewInternalInsets.top - self.textInputViewInternalInsets.bottom))
|
|
textInputNode.updateLayout(size: textInputNode.bounds.size)
|
|
textInputNode.view.layoutIfNeeded()
|
|
self.updateSpoiler()
|
|
}
|
|
|
|
self.textInputBackgroundNode.isUserInteractionEnabled = !textInputNode.isUserInteractionEnabled
|
|
//self.textInputBackgroundNode.view.removeGestureRecognizer(self.textInputBackgroundNode.view.gestureRecognizers![0])
|
|
|
|
textInputNode.textView.toggleQuoteCollapse = { [weak self] range in
|
|
guard let self else {
|
|
return
|
|
}
|
|
|
|
self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
|
|
let result = NSMutableAttributedString(attributedString: current.inputText)
|
|
var selectionRange = current.selectionRange
|
|
|
|
if let _ = result.attribute(ChatTextInputAttributes.block, at: range.lowerBound, effectiveRange: nil) as? ChatTextInputTextQuoteAttribute {
|
|
let blockString = NSMutableAttributedString(attributedString: result.attributedSubstring(from: range))
|
|
blockString.removeAttribute(ChatTextInputAttributes.block, range: NSRange(location: 0, length: blockString.length))
|
|
|
|
result.replaceCharacters(in: range, with: "")
|
|
result.insert(NSAttributedString(string: " ", attributes: [
|
|
ChatTextInputAttributes.collapsedBlock: blockString
|
|
]), at: range.lowerBound)
|
|
|
|
if selectionRange.lowerBound >= range.lowerBound && selectionRange.upperBound < range.upperBound {
|
|
selectionRange = range.lowerBound ..< range.lowerBound
|
|
} else if selectionRange.lowerBound >= range.upperBound {
|
|
let deltaLength = 1 - range.length
|
|
selectionRange = (selectionRange.lowerBound + deltaLength) ..< (selectionRange.lowerBound + deltaLength)
|
|
}
|
|
} else if let current = result.attribute(ChatTextInputAttributes.collapsedBlock, at: range.lowerBound, effectiveRange: nil) as? NSAttributedString {
|
|
result.replaceCharacters(in: range, with: "")
|
|
|
|
let updatedBlockString = NSMutableAttributedString(attributedString: current)
|
|
updatedBlockString.addAttribute(ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .quote, isCollapsed: false), range: NSRange(location: 0, length: updatedBlockString.length))
|
|
|
|
result.insert(updatedBlockString, at: range.lowerBound)
|
|
|
|
if selectionRange.lowerBound >= range.upperBound {
|
|
let deltaLength = updatedBlockString.length - 1
|
|
selectionRange = (selectionRange.lowerBound + deltaLength) ..< (selectionRange.lowerBound + deltaLength)
|
|
}
|
|
}
|
|
|
|
let stateResult = stateAttributedStringForText(result)
|
|
if selectionRange.lowerBound < 0 {
|
|
selectionRange = 0 ..< selectionRange.upperBound
|
|
}
|
|
if selectionRange.upperBound > stateResult.length {
|
|
selectionRange = selectionRange.lowerBound ..< stateResult.length
|
|
}
|
|
|
|
return (ChatTextInputState(
|
|
inputText: stateResult,
|
|
selectionRange: selectionRange
|
|
), inputMode)
|
|
}
|
|
}
|
|
|
|
let recognizer = TouchDownGestureRecognizer(target: self, action: #selector(self.textInputBackgroundViewTap(_:)))
|
|
recognizer.touchDown = { [weak self] in
|
|
if let strongSelf = self {
|
|
if strongSelf.textInputNode?.isFirstResponder() == true {
|
|
Queue.mainQueue().after(0.05) {
|
|
strongSelf.ensureFocusedOnTap()
|
|
}
|
|
} else {
|
|
strongSelf.ensureFocusedOnTap()
|
|
}
|
|
}
|
|
}
|
|
recognizer.waitForTouchUp = { [weak self] in
|
|
guard let strongSelf = self, let textInputNode = strongSelf.textInputNode else {
|
|
return true
|
|
}
|
|
|
|
if textInputNode.textView.isFirstResponder {
|
|
return true
|
|
} else if let (_, _, _, bottomInset, _, _, metrics, _, _) = strongSelf.validLayout {
|
|
let textFieldWaitsForTouchUp: Bool
|
|
if case .regular = metrics.widthClass, bottomInset.isZero {
|
|
textFieldWaitsForTouchUp = true
|
|
} else if !textInputNode.textView.text.isEmpty {
|
|
textFieldWaitsForTouchUp = true
|
|
} else {
|
|
textFieldWaitsForTouchUp = false
|
|
}
|
|
|
|
return textFieldWaitsForTouchUp
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
textInputNode.view.addGestureRecognizer(recognizer)
|
|
self.touchDownGestureRecognizer = recognizer
|
|
|
|
textInputNode.textView.accessibilityHint = self.textPlaceholderNode.attributedText?.string
|
|
}
|
|
|
|
private func textFieldMaxHeight(_ maxHeight: CGFloat, metrics: LayoutMetrics) -> CGFloat {
|
|
let textFieldInsets = self.textFieldInsets(metrics: metrics)
|
|
return max(33.0, maxHeight - (textFieldInsets.top + textFieldInsets.bottom + self.textInputViewInternalInsets.top + self.textInputViewInternalInsets.bottom))
|
|
}
|
|
|
|
private func calculateTextFieldMetrics(width: CGFloat, maxHeight: CGFloat, metrics: LayoutMetrics) -> (accessoryButtonsWidth: CGFloat, textFieldHeight: CGFloat) {
|
|
let accessoryButtonInset = self.accessoryButtonInset
|
|
let accessoryButtonSpacing = self.accessoryButtonSpacing
|
|
|
|
let textFieldInsets = self.textFieldInsets(metrics: metrics)
|
|
|
|
let fieldMaxHeight = textFieldMaxHeight(maxHeight, metrics: metrics)
|
|
|
|
var accessoryButtonsWidth: CGFloat = 0.0
|
|
var firstButton = true
|
|
for (_, button) in self.accessoryItemButtons {
|
|
if firstButton {
|
|
firstButton = false
|
|
accessoryButtonsWidth += accessoryButtonInset
|
|
} else {
|
|
accessoryButtonsWidth += accessoryButtonSpacing
|
|
}
|
|
accessoryButtonsWidth += button.buttonWidth
|
|
}
|
|
|
|
var textFieldMinHeight: CGFloat = 35.0
|
|
var textInputViewRealInsets = UIEdgeInsets()
|
|
if let presentationInterfaceState = self.presentationInterfaceState {
|
|
textFieldMinHeight = calclulateTextFieldMinHeight(presentationInterfaceState, metrics: metrics)
|
|
}
|
|
|
|
if let presentationInterfaceState = self.presentationInterfaceState {
|
|
textInputViewRealInsets = calculateTextFieldRealInsets(presentationInterfaceState: presentationInterfaceState, accessoryButtonsWidth: accessoryButtonsWidth)
|
|
}
|
|
|
|
let textFieldHeight: CGFloat
|
|
if let textInputNode = self.textInputNode {
|
|
let maxTextWidth = width - textFieldInsets.left - textFieldInsets.right - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right
|
|
let measuredHeight = textInputNode.textHeightForWidth(maxTextWidth, rightInset: textInputViewRealInsets.right)
|
|
|
|
let unboundTextFieldHeight = max(textFieldMinHeight, ceil(measuredHeight))
|
|
|
|
let maxNumberOfLines = min(12, (Int(fieldMaxHeight - 11.0) - 33) / 22)
|
|
|
|
let updatedMaxHeight = (CGFloat(maxNumberOfLines) * (22.0 + 2.0) + 10.0)
|
|
|
|
textFieldHeight = max(textFieldMinHeight, min(updatedMaxHeight, unboundTextFieldHeight))
|
|
} else {
|
|
textFieldHeight = textFieldMinHeight
|
|
}
|
|
|
|
return (accessoryButtonsWidth, textFieldHeight)
|
|
}
|
|
|
|
private func textFieldInsets(metrics: LayoutMetrics) -> UIEdgeInsets {
|
|
var insets = UIEdgeInsets(top: 6.0, left: 42.0, bottom: 6.0, right: 42.0)
|
|
if case .regular = metrics.widthClass, case .regular = metrics.heightClass {
|
|
insets.top += 1.0
|
|
insets.bottom += 1.0
|
|
}
|
|
return insets
|
|
}
|
|
|
|
private func panelHeight(textFieldHeight: CGFloat, metrics: LayoutMetrics) -> CGFloat {
|
|
let textFieldInsets = self.textFieldInsets(metrics: metrics)
|
|
let result = textFieldHeight + textFieldInsets.top + textFieldInsets.bottom + self.textInputViewInternalInsets.top + self.textInputViewInternalInsets.bottom
|
|
return result
|
|
}
|
|
|
|
override func minimalHeight(interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat {
|
|
let textFieldMinHeight = calclulateTextFieldMinHeight(interfaceState, metrics: metrics)
|
|
var minimalHeight: CGFloat = 14.0 + textFieldMinHeight
|
|
if case .regular = metrics.widthClass, case .regular = metrics.heightClass {
|
|
minimalHeight += 2.0
|
|
}
|
|
return minimalHeight
|
|
}
|
|
|
|
private func animateBotButtonInFromMenu(transition: ContainedViewLayoutTransition) {
|
|
guard !self.animatingTransition else {
|
|
return
|
|
}
|
|
guard let menuIconSnapshotView = self.menuButtonIconNode.view.snapshotView(afterScreenUpdates: false), let menuTextSnapshotView = self.menuButtonTextNode.view.snapshotView(afterScreenUpdates: false) else {
|
|
self.startButton.highlightEnabled = true
|
|
self.menuButton.isHidden = true
|
|
return
|
|
}
|
|
if transition.isAnimated {
|
|
self.animatingTransition = true
|
|
self.startButton.highlightEnabled = false
|
|
}
|
|
|
|
self.menuButton.isHidden = true
|
|
|
|
transition.animateFrame(layer: self.startButton.layer, from: self.menuButton.frame)
|
|
transition.animateFrame(layer: self.startButton.buttonBackgroundNode.layer, from: CGRect(origin: .zero, size: self.menuButton.frame.size))
|
|
transition.animatePosition(node: self.startButton.titleNode, from: CGPoint(x: self.menuButton.frame.width / 2.0, y: self.menuButton.frame.height / 2.0))
|
|
|
|
let targetButtonCornerRadius = self.startButton.buttonCornerRadius
|
|
self.startButton.buttonBackgroundNode.cornerRadius = self.menuButton.cornerRadius
|
|
transition.updateCornerRadius(node: self.startButton.buttonBackgroundNode, cornerRadius: targetButtonCornerRadius)
|
|
transition.animateTransformScale(node: self.startButton.titleNode, from: 0.4)
|
|
self.startButton.titleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
|
|
let menuContentDelta = (self.startButton.frame.width - self.menuButton.frame.width) / 2.0
|
|
menuIconSnapshotView.frame = self.menuButtonIconNode.frame.offsetBy(dx: self.menuButton.frame.minX, dy: self.menuButton.frame.minY)
|
|
self.view.addSubview(menuIconSnapshotView)
|
|
menuIconSnapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak menuIconSnapshotView] _ in
|
|
menuIconSnapshotView?.removeFromSuperview()
|
|
})
|
|
transition.updatePosition(layer: menuIconSnapshotView.layer, position: CGPoint(x: menuIconSnapshotView.center.x + menuContentDelta, y: self.startButton.position.y))
|
|
|
|
menuTextSnapshotView.frame = self.menuButtonTextNode.frame.offsetBy(dx: self.menuButton.frame.minX + 19.0, dy: self.menuButton.frame.minY)
|
|
self.view.addSubview(menuTextSnapshotView)
|
|
menuTextSnapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak menuTextSnapshotView, weak self] _ in
|
|
menuTextSnapshotView?.removeFromSuperview()
|
|
self?.animatingTransition = false
|
|
self?.startButton.highlightEnabled = true
|
|
})
|
|
transition.updatePosition(layer: menuTextSnapshotView.layer, position: CGPoint(x: menuTextSnapshotView.center.x + menuContentDelta, y: self.startButton.position.y))
|
|
}
|
|
|
|
func animateBotButtonOutToMenu(transition: ContainedViewLayoutTransition) {
|
|
guard !self.animatingTransition else {
|
|
return
|
|
}
|
|
|
|
guard let menuIconSnapshotView = self.menuButtonIconNode.view.snapshotView(afterScreenUpdates: false), let menuTextSnapshotView = self.menuButtonTextNode.view.snapshotView(afterScreenUpdates: false) else {
|
|
self.startButton.highlightEnabled = true
|
|
self.menuButton.isHidden = false
|
|
return
|
|
}
|
|
|
|
if transition.isAnimated {
|
|
self.animatingTransition = true
|
|
self.startButton.highlightEnabled = false
|
|
}
|
|
|
|
let sourceButtonFrame = self.startButton.frame
|
|
transition.updateFrame(node: self.startButton, frame: self.menuButton.frame)
|
|
transition.updateFrame(node: self.startButton.buttonBackgroundNode, frame: CGRect(origin: .zero, size: self.menuButton.frame.size))
|
|
let sourceButtonTextPosition = self.startButton.titleNode.position
|
|
transition.updatePosition(node: self.startButton.titleNode, position: CGPoint(x: self.menuButton.frame.width / 2.0, y: self.menuButton.frame.height / 2.0))
|
|
|
|
let sourceButtonCornerRadius = self.startButton.buttonCornerRadius
|
|
transition.updateCornerRadius(node: self.startButton.buttonBackgroundNode, cornerRadius: self.menuButton.cornerRadius)
|
|
transition.animateTransformScale(layer: self.startButton.titleNode.layer, from: CGPoint(x: 1.0, y: 1.0), to: CGPoint(x: 0.4, y: 0.4))
|
|
Queue.mainQueue().justDispatch {
|
|
self.startButton.titleNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
|
}
|
|
|
|
let menuContentDelta = (sourceButtonFrame.width - self.menuButton.frame.width) / 2.0
|
|
var menuIconSnapshotViewFrame = self.menuButtonIconNode.frame.offsetBy(dx: self.menuButton.frame.minX + menuContentDelta, dy: self.menuButton.frame.minY)
|
|
menuIconSnapshotViewFrame.origin.y = self.startButton.position.y - menuIconSnapshotViewFrame.height / 2.0
|
|
menuIconSnapshotView.frame = menuIconSnapshotViewFrame
|
|
self.view.addSubview(menuIconSnapshotView)
|
|
menuIconSnapshotView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
transition.updatePosition(layer: menuIconSnapshotView.layer, position: CGPoint(x: menuIconSnapshotView.center.x - menuContentDelta, y: self.menuButton.position.y))
|
|
|
|
var menuTextSnapshotViewFrame = self.menuButtonTextNode.frame.offsetBy(dx: self.menuButton.frame.minX + 19.0 + menuContentDelta, dy: self.menuButton.frame.minY)
|
|
menuTextSnapshotViewFrame.origin.y = self.startButton.position.y - menuTextSnapshotViewFrame.height / 2.0
|
|
menuTextSnapshotView.frame = menuTextSnapshotViewFrame
|
|
self.view.addSubview(menuTextSnapshotView)
|
|
menuTextSnapshotView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
transition.updatePosition(layer: menuTextSnapshotView.layer, position: CGPoint(x: menuTextSnapshotView.center.x - menuContentDelta, y: self.menuButton.position.y), completion: { [weak self, weak menuIconSnapshotView, weak menuTextSnapshotView] _ in
|
|
self?.animatingTransition = false
|
|
|
|
menuIconSnapshotView?.removeFromSuperview()
|
|
menuTextSnapshotView?.removeFromSuperview()
|
|
|
|
self?.menuButton.isHidden = false
|
|
self?.startButton.isHidden = true
|
|
self?.startButton.frame = sourceButtonFrame
|
|
self?.startButton.buttonBackgroundNode.frame = CGRect(origin: .zero, size: sourceButtonFrame.size)
|
|
self?.startButton.titleNode.position = sourceButtonTextPosition
|
|
self?.startButton.titleNode.layer.removeAllAnimations()
|
|
self?.startButton.buttonBackgroundNode.cornerRadius = sourceButtonCornerRadius
|
|
self?.startButton.highlightEnabled = true
|
|
})
|
|
}
|
|
|
|
private var absoluteRect: (CGRect, CGSize)?
|
|
override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition) {
|
|
self.absoluteRect = (rect, containerSize)
|
|
|
|
if !self.actionButtons.frame.width.isZero {
|
|
self.actionButtons.updateAbsoluteRect(CGRect(origin: rect.origin.offsetBy(dx: self.actionButtons.frame.minX, dy: self.actionButtons.frame.minY), size: self.actionButtons.frame.size), within: containerSize, transition: transition)
|
|
}
|
|
|
|
let absoluteFrame = self.startButton.view.convert(self.startButton.bounds, to: nil)
|
|
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY - 1.0), size: CGSize())
|
|
|
|
if let tooltipController = self.tooltipController, self.view.window != nil {
|
|
tooltipController.location = .point(location, .bottom)
|
|
}
|
|
}
|
|
|
|
func requestLayout(transition: ContainedViewLayoutTransition = .immediate) {
|
|
guard let presentationInterfaceState = self.presentationInterfaceState, let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, metrics, isSecondary, isMediaInputExpanded) = self.validLayout else {
|
|
return
|
|
}
|
|
let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: transition, interfaceState: presentationInterfaceState, metrics: metrics, isMediaInputExpanded: isMediaInputExpanded)
|
|
}
|
|
|
|
override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat {
|
|
let previousAdditionalSideInsets = self.validLayout?.4
|
|
self.validLayout = (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, metrics, isSecondary, isMediaInputExpanded)
|
|
|
|
var transition = transition
|
|
var additionalOffset: CGFloat = 0.0
|
|
if let previousAdditionalSideInsets = previousAdditionalSideInsets, previousAdditionalSideInsets.right != additionalSideInsets.right {
|
|
additionalOffset = (previousAdditionalSideInsets.right - additionalSideInsets.right) / 3.0
|
|
|
|
if case .animated = transition {
|
|
transition = .animated(duration: 0.2, curve: .easeInOut)
|
|
}
|
|
}
|
|
|
|
var wasEditingMedia = false
|
|
if let interfaceState = self.presentationInterfaceState, let editMessageState = interfaceState.editMessageState {
|
|
if case let .media(value) = editMessageState.content {
|
|
wasEditingMedia = !value.isEmpty
|
|
}
|
|
}
|
|
|
|
var isMediaEnabled = true
|
|
var isEditingMedia = false
|
|
if let editMessageState = interfaceState.editMessageState {
|
|
if case let .media(value) = editMessageState.content {
|
|
isEditingMedia = !value.isEmpty
|
|
isMediaEnabled = !value.isEmpty
|
|
} else {
|
|
isMediaEnabled = true
|
|
}
|
|
}
|
|
|
|
var isRecording = false
|
|
if let _ = interfaceState.inputTextPanelState.mediaRecordingState {
|
|
isRecording = true
|
|
}
|
|
|
|
var isScheduledMessages = false
|
|
if case .scheduledMessages = interfaceState.subject {
|
|
isScheduledMessages = true
|
|
}
|
|
|
|
var isSlowmodeActive = false
|
|
if interfaceState.slowmodeState != nil && !isScheduledMessages {
|
|
isSlowmodeActive = true
|
|
if !isEditingMedia {
|
|
isMediaEnabled = false
|
|
}
|
|
}
|
|
|
|
var displayMediaButton = true
|
|
if case let .customChatContents(customChatContents) = interfaceState.subject {
|
|
switch customChatContents.kind {
|
|
case .hashTagSearch:
|
|
break
|
|
case .quickReplyMessageInput:
|
|
break
|
|
case .businessLinkSetup:
|
|
displayMediaButton = false
|
|
}
|
|
}
|
|
|
|
let attachmentButtonAlpha: CGFloat
|
|
if displayMediaButton {
|
|
attachmentButtonAlpha = isMediaEnabled ? 1.0 : 0.4
|
|
} else {
|
|
attachmentButtonAlpha = 0.0
|
|
}
|
|
transition.updateAlpha(layer: self.attachmentButton.layer, alpha: attachmentButtonAlpha)
|
|
self.attachmentButton.isEnabled = isMediaEnabled && !isRecording
|
|
self.attachmentButton.accessibilityTraits = (!isSlowmodeActive || isMediaEnabled) ? [.button] : [.button, .notEnabled]
|
|
self.attachmentButtonDisabledNode.isHidden = !isSlowmodeActive || isMediaEnabled
|
|
|
|
let canBypassRestrictions = canBypassRestrictions(chatPresentationInterfaceState: interfaceState)
|
|
|
|
var sendingTextDisabled = false
|
|
if interfaceState.interfaceState.editMessage == nil {
|
|
if let peer = interfaceState.renderedPeer?.peer {
|
|
if let channel = peer as? TelegramChannel, channel.hasBannedPermission(.banSendText, ignoreDefault: canBypassRestrictions) != nil {
|
|
sendingTextDisabled = true
|
|
} else if let group = peer as? TelegramGroup, group.hasBannedPermission(.banSendText) {
|
|
sendingTextDisabled = true
|
|
}
|
|
}
|
|
}
|
|
self.sendingTextDisabled = sendingTextDisabled
|
|
|
|
self.textInputNode?.isUserInteractionEnabled = !sendingTextDisabled
|
|
|
|
var displayBotStartButton = false
|
|
if case .scheduledMessages = interfaceState.subject {
|
|
|
|
} else {
|
|
if let user = interfaceState.renderedPeer?.peer as? TelegramUser, user.botInfo != nil {
|
|
if let chatHistoryState = interfaceState.chatHistoryState, case .loaded(true, _) = chatHistoryState {
|
|
displayBotStartButton = true
|
|
} else if interfaceState.peerIsBlocked {
|
|
displayBotStartButton = true
|
|
}
|
|
}
|
|
}
|
|
|
|
var inputHasText = false
|
|
if let textInputNode = self.textInputNode, let attributedText = textInputNode.attributedText, attributedText.length != 0 {
|
|
inputHasText = true
|
|
}
|
|
|
|
var hasMenuButton = false
|
|
var menuButtonExpanded = false
|
|
var isSendAsButton = false
|
|
|
|
var shouldDisplayMenuButton = false
|
|
if interfaceState.hasBotCommands {
|
|
shouldDisplayMenuButton = true
|
|
} else if case .webView = interfaceState.botMenuButton {
|
|
shouldDisplayMenuButton = true
|
|
}
|
|
|
|
let mediaRecordingState = interfaceState.inputTextPanelState.mediaRecordingState
|
|
if !SGSimpleSettings.shared.disableSendAsButton, let sendAsPeers = interfaceState.sendAsPeers, !sendAsPeers.isEmpty && interfaceState.editMessageState == nil {
|
|
hasMenuButton = true
|
|
menuButtonExpanded = false
|
|
isSendAsButton = true
|
|
self.sendAsAvatarNode.isHidden = false
|
|
|
|
var currentPeer = sendAsPeers.first(where: { $0.peer.id == interfaceState.currentSendAsPeerId})?.peer
|
|
if currentPeer == nil {
|
|
currentPeer = sendAsPeers.first?.peer
|
|
}
|
|
if let context = self.context, let peer = currentPeer {
|
|
self.sendAsAvatarNode.setPeer(context: context, theme: interfaceState.theme, peer: EnginePeer(peer), emptyColor: interfaceState.theme.list.mediaPlaceholderColor)
|
|
}
|
|
} else if let peer = interfaceState.renderedPeer?.peer as? TelegramUser, let _ = peer.botInfo, shouldDisplayMenuButton && interfaceState.editMessageState == nil {
|
|
hasMenuButton = true
|
|
|
|
if !inputHasText {
|
|
switch interfaceState.inputMode {
|
|
case .none, .inputButtons:
|
|
menuButtonExpanded = true
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
self.sendAsAvatarNode.isHidden = true
|
|
} else {
|
|
self.sendAsAvatarNode.isHidden = true
|
|
}
|
|
if mediaRecordingState != nil {
|
|
hasMenuButton = false
|
|
}
|
|
|
|
let buttonInset: CGFloat = max(leftInset, 16.0)
|
|
let maximumButtonWidth: CGFloat = min(430.0, width)
|
|
let buttonHeight = self.startButton.updateLayout(width: maximumButtonWidth - buttonInset * 2.0, transition: transition)
|
|
let buttonSize = CGSize(width: maximumButtonWidth - buttonInset * 2.0, height: buttonHeight)
|
|
self.startButton.frame = CGRect(origin: CGPoint(x: leftInset + floor((width - leftInset - rightInset - buttonSize.width) / 2.0), y: 6.0), size: buttonSize)
|
|
|
|
var hideOffset: CGPoint = .zero
|
|
if displayBotStartButton {
|
|
if hasMenuButton {
|
|
hideOffset = CGPoint(x: width, y: 0.0)
|
|
} else {
|
|
hideOffset = CGPoint(x: 0.0, y: 80.0)
|
|
}
|
|
if self.startButton.isHidden {
|
|
self.startButton.isHidden = false
|
|
if hasMenuButton {
|
|
self.animateBotButtonInFromMenu(transition: transition)
|
|
} else {
|
|
transition.animatePosition(layer: self.startButton.layer, from: CGPoint(x: 0.0, y: 80.0), to: CGPoint(), additive: true)
|
|
}
|
|
if let context = self.context {
|
|
let parentFrame = self.view.convert(self.bounds, to: nil)
|
|
let absoluteFrame = self.startButton.view.convert(self.startButton.bounds, to: nil).offsetBy(dx: -parentFrame.minX, dy: 0.0)
|
|
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY - 1.0), size: CGSize())
|
|
|
|
if let tooltipController = self.tooltipController {
|
|
if self.view.window != nil {
|
|
tooltipController.location = .point(location, .bottom)
|
|
}
|
|
} else {
|
|
let controller = TooltipScreen(account: context.account, sharedContext: context.sharedContext, text: .plain(text: interfaceState.strings.Bot_TapToUse), icon: .downArrows, location: .point(location, .bottom), displayDuration: .infinite, shouldDismissOnTouch: { _, _ in
|
|
return .ignore
|
|
})
|
|
controller.alwaysVisible = true
|
|
self.tooltipController = controller
|
|
|
|
let delay: Double
|
|
if case .regular = metrics.widthClass {
|
|
delay = 0.1
|
|
} else {
|
|
delay = 0.35
|
|
}
|
|
Queue.mainQueue().after(delay, {
|
|
let parentFrame = self.view.convert(self.bounds, to: nil)
|
|
let absoluteFrame = self.startButton.view.convert(self.startButton.bounds, to: nil).offsetBy(dx: -parentFrame.minX, dy: 0.0)
|
|
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY - 1.0), size: CGSize())
|
|
controller.location = .point(location, .bottom)
|
|
self.interfaceInteraction?.presentControllerInCurrent(controller, nil)
|
|
})
|
|
}
|
|
}
|
|
} else {
|
|
if hasMenuButton && !self.animatingTransition {
|
|
self.menuButton.isHidden = true
|
|
}
|
|
}
|
|
} else if !self.startButton.isHidden {
|
|
if hasMenuButton {
|
|
self.animateBotButtonOutToMenu(transition: transition)
|
|
} else {
|
|
transition.animatePosition(node: self.startButton, to: CGPoint(x: 0.0, y: 80.0), removeOnCompletion: false, additive: true, completion: { _ in
|
|
self.startButton.isHidden = true
|
|
self.startButton.layer.removeAllAnimations()
|
|
})
|
|
}
|
|
}
|
|
|
|
var updatedPlaceholder: String?
|
|
|
|
let themeUpdated = self.presentationInterfaceState?.theme !== interfaceState.theme
|
|
|
|
var buttonTitleUpdated = false
|
|
var menuTextSize = self.menuButtonTextNode.frame.size
|
|
if self.presentationInterfaceState != interfaceState {
|
|
let previousState = self.presentationInterfaceState
|
|
self.presentationInterfaceState = interfaceState
|
|
|
|
if case .webView = interfaceState.botMenuButton, self.menuButtonIconNode.iconState == .menu {
|
|
self.menuButtonIconNode.enqueueState(.app, animated: false)
|
|
} else if case .commands = interfaceState.botMenuButton, self.menuButtonIconNode.iconState == .app {
|
|
self.menuButtonIconNode.enqueueState(.menu, animated: false)
|
|
}
|
|
if themeUpdated {
|
|
self.menuButtonIconNode.customColor = interfaceState.theme.chat.inputPanel.actionControlForegroundColor
|
|
self.startButton.updateTheme(SolidRoundedButtonTheme(theme: interfaceState.theme))
|
|
}
|
|
if let sendAsPeers = interfaceState.sendAsPeers, !sendAsPeers.isEmpty {
|
|
self.menuButtonIconNode.enqueueState(.close, animated: false)
|
|
} else if case .webView = interfaceState.botMenuButton, let previousShowWebView = previousState?.showWebView, previousShowWebView != interfaceState.showWebView {
|
|
if interfaceState.showWebView {
|
|
// self.menuButtonIconNode.enqueueState(.close, animated: true)
|
|
} else {
|
|
self.menuButtonIconNode.enqueueState(.app, animated: true)
|
|
}
|
|
} else if let previousShowCommands = previousState?.showCommands, previousShowCommands != interfaceState.showCommands {
|
|
if interfaceState.showCommands {
|
|
self.menuButtonIconNode.enqueueState(.close, animated: true)
|
|
} else {
|
|
self.menuButtonIconNode.enqueueState(.menu, animated: true)
|
|
}
|
|
}
|
|
|
|
let buttonTitle: String
|
|
if case let .webView(title, _) = interfaceState.botMenuButton {
|
|
buttonTitle = title
|
|
} else {
|
|
buttonTitle = interfaceState.strings.Conversation_InputMenu
|
|
}
|
|
|
|
buttonTitleUpdated = self.menuButtonTextNode.attributedText != nil && self.menuButtonTextNode.attributedText?.string != buttonTitle
|
|
|
|
self.menuButtonTextNode.attributedText = NSAttributedString(string: buttonTitle, font: Font.with(size: 16.0, design: .round, weight: .medium, traits: []), textColor: interfaceState.theme.chat.inputPanel.actionControlForegroundColor)
|
|
self.menuButton.accessibilityLabel = self.menuButtonTextNode.attributedText?.string
|
|
|
|
if buttonTitleUpdated, let buttonTextSnapshotView = self.menuButtonTextNode.view.snapshotView(afterScreenUpdates: false) {
|
|
buttonTextSnapshotView.frame = self.menuButtonTextNode.view.frame
|
|
self.menuButtonTextNode.view.superview?.addSubview(buttonTextSnapshotView)
|
|
buttonTextSnapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak buttonTextSnapshotView] _ in
|
|
buttonTextSnapshotView?.removeFromSuperview()
|
|
})
|
|
self.menuButtonTextNode.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
}
|
|
menuTextSize = self.menuButtonTextNode.updateLayout(CGSize(width: width / 2.0 - 60.0, height: 44.0))
|
|
|
|
var updateSendButtonIcon = false
|
|
if (previousState?.interfaceState.editMessage != nil) != (interfaceState.interfaceState.editMessage != nil) {
|
|
updateSendButtonIcon = true
|
|
}
|
|
if self.theme !== interfaceState.theme {
|
|
updateSendButtonIcon = true
|
|
|
|
if self.theme == nil || !self.theme!.chat.inputPanel.inputTextColor.isEqual(interfaceState.theme.chat.inputPanel.inputTextColor) {
|
|
let textColor = interfaceState.theme.chat.inputPanel.inputTextColor
|
|
let baseFontSize = max(minInputFontSize, interfaceState.fontSize.baseDisplaySize)
|
|
|
|
if let textInputNode = self.textInputNode {
|
|
if let text = textInputNode.attributedText {
|
|
let range = textInputNode.selectedRange
|
|
let updatedText = NSMutableAttributedString(attributedString: text)
|
|
updatedText.addAttribute(NSAttributedString.Key.foregroundColor, value: textColor, range: NSRange(location: 0, length: updatedText.length))
|
|
textInputNode.attributedText = updatedText
|
|
textInputNode.selectedRange = range
|
|
}
|
|
textInputNode.textView.typingAttributes = [NSAttributedString.Key.font: Font.regular(baseFontSize), NSAttributedString.Key.foregroundColor: textColor]
|
|
|
|
self.updateSpoiler()
|
|
}
|
|
}
|
|
|
|
let tintColor = interfaceState.theme.list.itemAccentColor
|
|
if let textInputNode = self.textInputNode, tintColor != textInputNode.tintColor {
|
|
textInputNode.tintColor = tintColor
|
|
textInputNode.tintColorDidChange()
|
|
}
|
|
|
|
if let textInputNode = self.textInputNode, let context = self.context {
|
|
textInputNode.textView.theme = makeTextInputTheme(context: context, interfaceState: interfaceState)
|
|
}
|
|
|
|
let keyboardAppearance = interfaceState.theme.rootController.keyboardColor.keyboardAppearance
|
|
if let textInputNode = self.textInputNode, textInputNode.keyboardAppearance != keyboardAppearance {
|
|
if textInputNode.isFirstResponder() && textInputNode.isCurrentlyEmoji() {
|
|
textInputNode.initialPrimaryLanguage = "emoji"
|
|
textInputNode.resetInitialPrimaryLanguage()
|
|
}
|
|
textInputNode.keyboardAppearance = keyboardAppearance
|
|
}
|
|
|
|
self.theme = interfaceState.theme
|
|
|
|
self.menuButtonBackgroundNode.backgroundColor = interfaceState.theme.chat.inputPanel.actionControlFillColor
|
|
|
|
if isEditingMedia {
|
|
self.attachmentButton.setImage(PresentationResourcesChat.chatInputPanelEditAttachmentButtonImage(interfaceState.theme), for: [])
|
|
} else {
|
|
self.attachmentButton.setImage(PresentationResourcesChat.chatInputPanelAttachmentButtonImage(interfaceState.theme), for: [])
|
|
}
|
|
|
|
self.actionButtons.updateTheme(theme: interfaceState.theme, wallpaper: interfaceState.chatWallpaper)
|
|
|
|
let textFieldMinHeight = calclulateTextFieldMinHeight(interfaceState, metrics: metrics)
|
|
let minimalInputHeight: CGFloat = 2.0 + textFieldMinHeight
|
|
|
|
let strokeWidth: CGFloat
|
|
let backgroundColor: UIColor
|
|
if case let .color(color) = interfaceState.chatWallpaper, UIColor(rgb: color).isEqual(interfaceState.theme.chat.inputPanel.panelBackgroundColorNoWallpaper) {
|
|
backgroundColor = interfaceState.theme.chat.inputPanel.panelBackgroundColorNoWallpaper
|
|
strokeWidth = 1.0 - UIScreenPixel
|
|
} else {
|
|
backgroundColor = interfaceState.theme.chat.inputPanel.panelBackgroundColor
|
|
strokeWidth = UIScreenPixel
|
|
}
|
|
|
|
self.textInputBackgroundNode.image = textInputBackgroundImage(backgroundColor: backgroundColor, inputBackgroundColor: nil, strokeColor: interfaceState.theme.chat.inputPanel.inputStrokeColor, diameter: minimalInputHeight, strokeWidth: strokeWidth)
|
|
self.transparentTextInputBackgroundImage = textInputBackgroundImage(backgroundColor: nil, inputBackgroundColor: interfaceState.theme.chat.inputPanel.inputBackgroundColor, strokeColor: interfaceState.theme.chat.inputPanel.inputStrokeColor, diameter: minimalInputHeight, strokeWidth: strokeWidth)
|
|
self.textInputContainerBackgroundNode.image = generateStretchableFilledCircleImage(diameter: minimalInputHeight, color: interfaceState.theme.chat.inputPanel.inputBackgroundColor)
|
|
|
|
self.searchLayoutClearImageNode.image = PresentationResourcesChat.chatInputTextFieldClearImage(interfaceState.theme)
|
|
|
|
self.audioRecordingTimeNode?.updateTheme(theme: interfaceState.theme)
|
|
self.audioRecordingCancelIndicator?.updateTheme(theme: interfaceState.theme)
|
|
|
|
for (_, button) in self.accessoryItemButtons {
|
|
button.updateThemeAndStrings(theme: interfaceState.theme, strings: interfaceState.strings)
|
|
}
|
|
} else {
|
|
if self.strings !== interfaceState.strings {
|
|
self.strings = interfaceState.strings
|
|
self.inputMenu.updateStrings(interfaceState.strings)
|
|
|
|
for (_, button) in self.accessoryItemButtons {
|
|
button.updateThemeAndStrings(theme: interfaceState.theme, strings: interfaceState.strings)
|
|
}
|
|
}
|
|
|
|
if wasEditingMedia != isEditingMedia {
|
|
if isEditingMedia {
|
|
self.attachmentButton.setImage(PresentationResourcesChat.chatInputPanelEditAttachmentButtonImage(interfaceState.theme), for: [])
|
|
} else {
|
|
self.attachmentButton.setImage(PresentationResourcesChat.chatInputPanelAttachmentButtonImage(interfaceState.theme), for: [])
|
|
}
|
|
}
|
|
}
|
|
|
|
let dismissedButtonMessageUpdated = interfaceState.interfaceState.messageActionsState.dismissedButtonKeyboardMessageId != previousState?.interfaceState.messageActionsState.dismissedButtonKeyboardMessageId
|
|
let replyMessageUpdated = interfaceState.interfaceState.replyMessageSubject != previousState?.interfaceState.replyMessageSubject
|
|
|
|
var peerUpdated = false
|
|
if let peer = interfaceState.renderedPeer?.peer, previousState?.renderedPeer?.peer == nil || !peer.isEqual(previousState!.renderedPeer!.peer!) {
|
|
peerUpdated = true
|
|
}
|
|
|
|
if peerUpdated || previousState?.interfaceState.silentPosting != interfaceState.interfaceState.silentPosting || themeUpdated || !self.initializedPlaceholder || previousState?.keyboardButtonsMessage?.id != interfaceState.keyboardButtonsMessage?.id || previousState?.keyboardButtonsMessage?.visibleReplyMarkupPlaceholder != interfaceState.keyboardButtonsMessage?.visibleReplyMarkupPlaceholder || dismissedButtonMessageUpdated || replyMessageUpdated || (previousState?.interfaceState.editMessage == nil) != (interfaceState.interfaceState.editMessage == nil) || previousState?.forumTopicData != interfaceState.forumTopicData || previousState?.replyMessage?.id != interfaceState.replyMessage?.id {
|
|
self.initializedPlaceholder = true
|
|
|
|
var placeholder: String = ""
|
|
|
|
if let peer = interfaceState.renderedPeer?.peer {
|
|
if let channel = peer as? TelegramChannel, case .broadcast = channel.info {
|
|
if interfaceState.interfaceState.silentPosting {
|
|
placeholder = interfaceState.strings.Conversation_InputTextSilentBroadcastPlaceholder
|
|
} else {
|
|
placeholder = interfaceState.strings.Conversation_InputTextBroadcastPlaceholder
|
|
}
|
|
} else {
|
|
if sendingTextDisabled {
|
|
placeholder = interfaceState.strings.Chat_PlaceholderTextNotAllowed
|
|
} else {
|
|
if let channel = peer as? TelegramChannel, case .group = channel.info, channel.hasPermission(.canBeAnonymous) {
|
|
placeholder = interfaceState.strings.Conversation_InputTextAnonymousPlaceholder
|
|
} else if case let .replyThread(replyThreadMessage) = interfaceState.chatLocation, !replyThreadMessage.isForumPost, replyThreadMessage.peerId != self.context?.account.peerId {
|
|
if replyThreadMessage.isChannelPost {
|
|
placeholder = interfaceState.strings.Conversation_InputTextPlaceholderComment
|
|
} else {
|
|
placeholder = interfaceState.strings.Conversation_InputTextPlaceholderReply
|
|
}
|
|
} else if let channel = peer as? TelegramChannel, channel.isForum, let forumTopicData = interfaceState.forumTopicData {
|
|
if let replyMessage = interfaceState.replyMessage, let threadInfo = replyMessage.associatedThreadInfo {
|
|
placeholder = interfaceState.strings.Chat_InputPlaceholderReplyInTopic(threadInfo.title).string
|
|
} else {
|
|
placeholder = interfaceState.strings.Chat_InputPlaceholderMessageInTopic(forumTopicData.title).string
|
|
}
|
|
} else {
|
|
placeholder = interfaceState.strings.Conversation_InputTextPlaceholder
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if case let .customChatContents(customChatContents) = interfaceState.subject {
|
|
switch customChatContents.kind {
|
|
case .hashTagSearch:
|
|
placeholder = ""
|
|
case let .quickReplyMessageInput(_, shortcutType):
|
|
switch shortcutType {
|
|
case .generic:
|
|
placeholder = interfaceState.strings.Chat_Placeholder_QuickReply
|
|
case .greeting:
|
|
placeholder = interfaceState.strings.Chat_Placeholder_GreetingMessage
|
|
case .away:
|
|
placeholder = interfaceState.strings.Chat_Placeholder_AwayMessage
|
|
}
|
|
case .businessLinkSetup:
|
|
placeholder = interfaceState.strings.Chat_Placeholder_BusinessLinkPreset
|
|
}
|
|
}
|
|
|
|
if let keyboardButtonsMessage = interfaceState.keyboardButtonsMessage, interfaceState.interfaceState.messageActionsState.dismissedButtonKeyboardMessageId != keyboardButtonsMessage.id {
|
|
if keyboardButtonsMessage.requestsSetupReply && keyboardButtonsMessage.id != interfaceState.interfaceState.replyMessageSubject?.messageId {
|
|
} else {
|
|
if let placeholderValue = interfaceState.keyboardButtonsMessage?.visibleReplyMarkupPlaceholder, !placeholderValue.isEmpty {
|
|
placeholder = placeholderValue
|
|
}
|
|
}
|
|
}
|
|
|
|
updatedPlaceholder = placeholder
|
|
|
|
self.actionButtons.sendButtonLongPressEnabled = !isScheduledMessages
|
|
}
|
|
|
|
var sendButtonHasApplyIcon = interfaceState.interfaceState.editMessage != nil
|
|
if let interfaceState = self.presentationInterfaceState {
|
|
if case let .customChatContents(customChatContents) = interfaceState.subject {
|
|
switch customChatContents.kind {
|
|
case .hashTagSearch:
|
|
break
|
|
case .quickReplyMessageInput:
|
|
break
|
|
case .businessLinkSetup:
|
|
sendButtonHasApplyIcon = true
|
|
}
|
|
}
|
|
}
|
|
|
|
if updateSendButtonIcon {
|
|
if !self.actionButtons.animatingSendButton {
|
|
let imageNode = self.actionButtons.sendButton.imageNode
|
|
|
|
if transition.isAnimated && !self.actionButtons.sendContainerNode.alpha.isZero && self.actionButtons.sendButton.layer.animation(forKey: "opacity") == nil, let previousImage = imageNode.image {
|
|
let tempView = UIImageView(image: previousImage)
|
|
self.actionButtons.sendButton.view.addSubview(tempView)
|
|
tempView.frame = imageNode.frame
|
|
tempView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak tempView] _ in
|
|
tempView?.removeFromSuperview()
|
|
})
|
|
tempView.layer.animateScale(from: 1.0, to: 0.2, duration: 0.2, removeOnCompletion: false)
|
|
|
|
imageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
imageNode.layer.animateScale(from: 0.2, to: 1.0, duration: 0.2)
|
|
}
|
|
self.actionButtons.sendButtonHasApplyIcon = sendButtonHasApplyIcon
|
|
if self.actionButtons.sendButtonHasApplyIcon {
|
|
self.actionButtons.sendButton.setImage(PresentationResourcesChat.chatInputPanelApplyIconImage(interfaceState.theme), for: [])
|
|
} else {
|
|
if isScheduledMessages {
|
|
self.actionButtons.sendButton.setImage(PresentationResourcesChat.chatInputPanelScheduleButtonImage(interfaceState.theme), for: [])
|
|
} else {
|
|
self.actionButtons.sendButton.setImage(PresentationResourcesChat.chatInputPanelSendIconImage(interfaceState.theme), for: [])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var textFieldMinHeight: CGFloat = 33.0
|
|
if let presentationInterfaceState = self.presentationInterfaceState {
|
|
textFieldMinHeight = calclulateTextFieldMinHeight(presentationInterfaceState, metrics: metrics)
|
|
}
|
|
let minimalHeight: CGFloat = 14.0 + textFieldMinHeight
|
|
let minimalInputHeight: CGFloat = 2.0 + textFieldMinHeight
|
|
|
|
var animatedTransition = true
|
|
if case .immediate = transition {
|
|
animatedTransition = false
|
|
}
|
|
|
|
var updateAccessoryButtons = false
|
|
if self.presentationInterfaceState?.inputTextPanelState.accessoryItems.count == self.accessoryItemButtons.count {
|
|
for i in 0 ..< interfaceState.inputTextPanelState.accessoryItems.count {
|
|
if interfaceState.inputTextPanelState.accessoryItems[i] != self.accessoryItemButtons[i].0 {
|
|
updateAccessoryButtons = true
|
|
break
|
|
}
|
|
}
|
|
} else {
|
|
updateAccessoryButtons = true
|
|
}
|
|
|
|
var removeAccessoryButtons: [AccessoryItemIconButtonNode]?
|
|
if updateAccessoryButtons {
|
|
var updatedButtons: [(ChatTextInputAccessoryItem, AccessoryItemIconButtonNode)] = []
|
|
for item in interfaceState.inputTextPanelState.accessoryItems {
|
|
var itemAndButton: (ChatTextInputAccessoryItem, AccessoryItemIconButtonNode)?
|
|
for i in 0 ..< self.accessoryItemButtons.count {
|
|
if self.accessoryItemButtons[i].0.key == item.key {
|
|
itemAndButton = self.accessoryItemButtons[i]
|
|
itemAndButton?.0 = item
|
|
self.accessoryItemButtons.remove(at: i)
|
|
break
|
|
}
|
|
}
|
|
if itemAndButton == nil {
|
|
let button = AccessoryItemIconButtonNode(item: item, theme: interfaceState.theme, strings: interfaceState.strings)
|
|
button.addTarget(self, action: #selector(self.accessoryItemButtonPressed(_:)), forControlEvents: .touchUpInside)
|
|
itemAndButton = (item, button)
|
|
}
|
|
updatedButtons.append(itemAndButton!)
|
|
}
|
|
for (_, button) in self.accessoryItemButtons {
|
|
if animatedTransition {
|
|
if removeAccessoryButtons == nil {
|
|
removeAccessoryButtons = []
|
|
}
|
|
removeAccessoryButtons!.append(button)
|
|
} else {
|
|
button.removeFromSupernode()
|
|
}
|
|
}
|
|
self.accessoryItemButtons = updatedButtons
|
|
}
|
|
|
|
let leftMenuInset: CGFloat
|
|
let menuButtonHeight: CGFloat = 33.0
|
|
let menuCollapsedButtonWidth: CGFloat = isSendAsButton ? menuButtonHeight : 38.0
|
|
let menuButtonWidth = menuTextSize.width + 47.0
|
|
if hasMenuButton {
|
|
let menuButtonSpacing: CGFloat = 10.0
|
|
if menuButtonExpanded {
|
|
leftMenuInset = menuButtonWidth + menuButtonSpacing
|
|
} else {
|
|
leftMenuInset = menuCollapsedButtonWidth + menuButtonSpacing
|
|
}
|
|
} else {
|
|
leftMenuInset = 0.0
|
|
}
|
|
self.leftMenuInset = leftMenuInset
|
|
|
|
var rightSlowModeInset: CGFloat = 0.0
|
|
var slowModeButtonSize: CGSize = .zero
|
|
if let presentationInterfaceState = self.presentationInterfaceState, (presentationInterfaceState.boostsToUnrestrict ?? 0) > 0 {
|
|
slowModeButtonSize = self.slowModeButton.update(size: CGSize(width: width, height: 44.0), interfaceState: presentationInterfaceState)
|
|
rightSlowModeInset = max(0.0, slowModeButtonSize.width - 33.0)
|
|
}
|
|
self.rightSlowModeInset = rightSlowModeInset
|
|
|
|
if buttonTitleUpdated && !transition.isAnimated {
|
|
transition = .animated(duration: 0.3, curve: .easeInOut)
|
|
}
|
|
|
|
var leftInset = leftInset
|
|
|
|
var textInputBackgroundWidthOffset: CGFloat = 0.0
|
|
var attachmentButtonX: CGFloat = hideOffset.x + leftInset + leftMenuInset + 2.0 - UIScreenPixel
|
|
if !displayMediaButton {
|
|
attachmentButtonX = -40.0
|
|
let inputFieldAdditionalWidth = 40.0 - 4.0
|
|
leftInset -= inputFieldAdditionalWidth
|
|
textInputBackgroundWidthOffset += inputFieldAdditionalWidth
|
|
}
|
|
|
|
let baseWidth = width - leftInset - leftMenuInset - rightInset - rightSlowModeInset
|
|
let (accessoryButtonsWidth, textFieldHeight) = self.calculateTextFieldMetrics(width: baseWidth, maxHeight: maxHeight, metrics: metrics)
|
|
var panelHeight = self.panelHeight(textFieldHeight: textFieldHeight, metrics: metrics)
|
|
if displayBotStartButton {
|
|
panelHeight += 27.0
|
|
}
|
|
|
|
let menuButtonOriginY: CGFloat
|
|
if displayBotStartButton {
|
|
menuButtonOriginY = floorToScreenPixels((minimalHeight - menuButtonHeight) / 2.0)
|
|
} else {
|
|
menuButtonOriginY = panelHeight - minimalHeight + floorToScreenPixels((minimalHeight - menuButtonHeight) / 2.0)
|
|
}
|
|
|
|
let menuButtonFrame = CGRect(x: leftInset + 10.0, y: menuButtonOriginY, width: menuButtonExpanded ? menuButtonWidth : menuCollapsedButtonWidth, height: menuButtonHeight)
|
|
transition.updateFrameAsPositionAndBounds(node: self.menuButton, frame: menuButtonFrame)
|
|
transition.updateFrame(node: self.menuButtonBackgroundNode, frame: CGRect(origin: CGPoint(), size: menuButtonFrame.size))
|
|
transition.updateFrame(node: self.menuButtonClippingNode, frame: CGRect(origin: CGPoint(x: 19.0, y: 0.0), size: CGSize(width: menuButtonWidth - 19.0, height: menuButtonFrame.height)))
|
|
var menuButtonTitleTransition = transition
|
|
if buttonTitleUpdated {
|
|
menuButtonTitleTransition = .immediate
|
|
}
|
|
menuButtonTitleTransition.updateFrame(node: self.menuButtonTextNode, frame: CGRect(origin: CGPoint(x: 16.0, y: 7.0 - UIScreenPixel), size: menuTextSize))
|
|
transition.updateAlpha(node: self.menuButtonTextNode, alpha: menuButtonExpanded ? 1.0 : 0.0)
|
|
transition.updateFrame(node: self.menuButtonIconNode, frame: CGRect(x: isSendAsButton ? 1.0 + UIScreenPixel : (4.0 + UIScreenPixel), y: 1.0 + UIScreenPixel, width: 30.0, height: 30.0))
|
|
|
|
transition.updateFrame(node: self.sendAsAvatarButtonNode, frame: menuButtonFrame)
|
|
transition.updateFrame(node: self.sendAsAvatarContainerNode, frame: CGRect(origin: CGPoint(), size: menuButtonFrame.size))
|
|
transition.updateFrame(node: self.sendAsAvatarReferenceNode, frame: CGRect(origin: CGPoint(), size: menuButtonFrame.size))
|
|
transition.updateFrame(node: self.sendAsAvatarNode, frame: CGRect(origin: CGPoint(), size: menuButtonFrame.size))
|
|
|
|
let showMenuButton = hasMenuButton && interfaceState.interfaceState.mediaDraftState == nil
|
|
if isSendAsButton {
|
|
if interfaceState.showSendAsPeers {
|
|
transition.updateTransformScale(node: self.menuButton, scale: 1.0)
|
|
transition.updateAlpha(node: self.menuButton, alpha: 1.0)
|
|
|
|
transition.updateTransformScale(node: self.sendAsAvatarButtonNode, scale: 0.001)
|
|
transition.updateAlpha(node: self.sendAsAvatarButtonNode, alpha: 0.0)
|
|
} else {
|
|
transition.updateTransformScale(node: self.menuButton, scale: 0.001)
|
|
transition.updateAlpha(node: self.menuButton, alpha: 0.0)
|
|
|
|
transition.updateTransformScale(node: self.sendAsAvatarButtonNode, scale: showMenuButton ? 1.0 : 0.001)
|
|
transition.updateAlpha(node: self.sendAsAvatarButtonNode, alpha: showMenuButton ? 1.0 : 0.0)
|
|
}
|
|
} else {
|
|
transition.updateTransformScale(node: self.menuButton, scale: showMenuButton ? 1.0 : 0.001)
|
|
transition.updateAlpha(node: self.menuButton, alpha: showMenuButton ? 1.0 : 0.0)
|
|
|
|
transition.updateTransformScale(node: self.sendAsAvatarButtonNode, scale: 0.001)
|
|
transition.updateAlpha(node: self.sendAsAvatarButtonNode, alpha: 0.0)
|
|
}
|
|
self.menuButton.isUserInteractionEnabled = hasMenuButton
|
|
self.sendAsAvatarButtonNode.isUserInteractionEnabled = hasMenuButton && isSendAsButton
|
|
|
|
self.actionButtons.micButton.updateMode(mode: interfaceState.interfaceState.mediaRecordingMode, animated: transition.isAnimated)
|
|
|
|
var hideMicButton = false
|
|
var audioRecordingItemsAlpha: CGFloat = 1
|
|
if mediaRecordingState != nil || (interfaceState.interfaceState.mediaDraftState != nil && self.finishedTransitionToPreview != true) {
|
|
if interfaceState.interfaceState.mediaDraftState != nil {
|
|
self.finishedTransitionToPreview = false
|
|
}
|
|
|
|
audioRecordingItemsAlpha = 0
|
|
|
|
let audioRecordingInfoContainerNode: ASDisplayNode
|
|
if let currentAudioRecordingInfoContainerNode = self.audioRecordingInfoContainerNode {
|
|
audioRecordingInfoContainerNode = currentAudioRecordingInfoContainerNode
|
|
} else {
|
|
audioRecordingInfoContainerNode = ASDisplayNode()
|
|
self.audioRecordingInfoContainerNode = audioRecordingInfoContainerNode
|
|
self.clippingNode.insertSubnode(audioRecordingInfoContainerNode, at: 0)
|
|
}
|
|
|
|
var animateTimeSlideIn = false
|
|
let audioRecordingTimeNode: ChatTextInputAudioRecordingTimeNode
|
|
if let currentAudioRecordingTimeNode = self.audioRecordingTimeNode {
|
|
audioRecordingTimeNode = currentAudioRecordingTimeNode
|
|
} else {
|
|
audioRecordingTimeNode = ChatTextInputAudioRecordingTimeNode(theme: interfaceState.theme)
|
|
self.audioRecordingTimeNode = audioRecordingTimeNode
|
|
audioRecordingInfoContainerNode.addSubnode(audioRecordingTimeNode)
|
|
|
|
if transition.isAnimated && mediaRecordingState != nil {
|
|
animateTimeSlideIn = true
|
|
}
|
|
}
|
|
|
|
var animateCancelSlideIn = false
|
|
let audioRecordingCancelIndicator: ChatTextInputAudioRecordingCancelIndicator
|
|
if let currentAudioRecordingCancelIndicator = self.audioRecordingCancelIndicator {
|
|
audioRecordingCancelIndicator = currentAudioRecordingCancelIndicator
|
|
} else {
|
|
animateCancelSlideIn = transition.isAnimated && mediaRecordingState != nil
|
|
|
|
audioRecordingCancelIndicator = ChatTextInputAudioRecordingCancelIndicator(theme: interfaceState.theme, strings: interfaceState.strings, cancel: { [weak self] in
|
|
self?.viewOnce = false
|
|
self?.interfaceInteraction?.finishMediaRecording(.dismiss)
|
|
self?.tooltipController?.dismiss()
|
|
})
|
|
self.audioRecordingCancelIndicator = audioRecordingCancelIndicator
|
|
self.clippingNode.insertSubnode(audioRecordingCancelIndicator, at: 0)
|
|
}
|
|
|
|
let isLocked = mediaRecordingState?.isLocked ?? (interfaceState.interfaceState.mediaDraftState != nil)
|
|
var hideInfo = false
|
|
|
|
if let mediaRecordingState = mediaRecordingState {
|
|
switch mediaRecordingState {
|
|
case let .audio(recorder, isLocked):
|
|
let hadAudioRecorder = self.actionButtons.micButton.audioRecorder != nil
|
|
if !hadAudioRecorder, isLocked {
|
|
self.actionButtons.micButton.lock()
|
|
}
|
|
self.actionButtons.micButton.audioRecorder = recorder
|
|
audioRecordingTimeNode.audioRecorder = recorder
|
|
case let .video(status, _):
|
|
let hadVideoRecorder = self.actionButtons.micButton.videoRecordingStatus != nil
|
|
if !hadVideoRecorder, isLocked {
|
|
self.actionButtons.micButton.lock()
|
|
}
|
|
switch status {
|
|
case let .recording(recordingStatus):
|
|
audioRecordingTimeNode.videoRecordingStatus = recordingStatus
|
|
self.actionButtons.micButton.videoRecordingStatus = recordingStatus
|
|
case .editing:
|
|
audioRecordingTimeNode.videoRecordingStatus = nil
|
|
self.actionButtons.micButton.videoRecordingStatus = nil
|
|
hideMicButton = true
|
|
hideInfo = true
|
|
}
|
|
case .waitingForPreview:
|
|
Queue.mainQueue().after(0.3, {
|
|
self.actionButtons.micButton.audioRecorder = nil
|
|
})
|
|
}
|
|
}
|
|
|
|
transition.updateAlpha(layer: self.textInputBackgroundNode.layer, alpha: 0.0)
|
|
if let textInputNode = self.textInputNode {
|
|
transition.updateAlpha(node: textInputNode, alpha: 0.0)
|
|
}
|
|
for (_, button) in self.accessoryItemButtons {
|
|
transition.updateAlpha(layer: button.layer, alpha: 0.0)
|
|
}
|
|
|
|
let cancelTransformThreshold: CGFloat = 8.0
|
|
|
|
let indicatorTranslation = max(0.0, self.actionButtons.micButton.cancelTranslation - cancelTransformThreshold)
|
|
|
|
let audioRecordingCancelIndicatorFrame = CGRect(
|
|
origin: CGPoint(
|
|
x: leftInset + floor((baseWidth - audioRecordingCancelIndicator.bounds.size.width - indicatorTranslation) / 2.0),
|
|
y: panelHeight - minimalHeight + floor((minimalHeight - audioRecordingCancelIndicator.bounds.size.height) / 2.0)),
|
|
size: audioRecordingCancelIndicator.bounds.size)
|
|
audioRecordingCancelIndicator.frame = audioRecordingCancelIndicatorFrame
|
|
if self.actionButtons.micButton.cancelTranslation > cancelTransformThreshold {
|
|
let progress: CGFloat = max(0.0, min(1.0, (audioRecordingCancelIndicatorFrame.minX - 100.0) / 10.0))
|
|
audioRecordingCancelIndicator.alpha = progress
|
|
} else {
|
|
audioRecordingCancelIndicator.alpha = 1
|
|
}
|
|
|
|
if animateCancelSlideIn {
|
|
let position = audioRecordingCancelIndicator.layer.position
|
|
audioRecordingCancelIndicator.layer.animatePosition(from: CGPoint(x: width + audioRecordingCancelIndicator.bounds.size.width, y: position.y), to: position, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
|
|
}
|
|
|
|
audioRecordingCancelIndicator.updateIsDisplayingCancel(isLocked, animated: !animateCancelSlideIn && mediaRecordingState != nil)
|
|
|
|
if isLocked || self.actionButtons.micButton.cancelTranslation > cancelTransformThreshold {
|
|
var deltaOffset: CGFloat = 0.0
|
|
if audioRecordingCancelIndicator.layer.animation(forKey: "slide_juggle") != nil, let presentationLayer = audioRecordingCancelIndicator.layer.presentation() {
|
|
let translation = CGPoint(x: presentationLayer.transform.m41, y: presentationLayer.transform.m42)
|
|
deltaOffset = translation.x
|
|
}
|
|
audioRecordingCancelIndicator.layer.removeAnimation(forKey: "slide_juggle")
|
|
if !deltaOffset.isZero {
|
|
audioRecordingCancelIndicator.layer.animatePosition(from: CGPoint(x: deltaOffset, y: 0.0), to: CGPoint(), duration: 0.3, additive: true)
|
|
}
|
|
} else if audioRecordingCancelIndicator.layer.animation(forKey: "slide_juggle") == nil, baseWidth > 320 {
|
|
let slideJuggleAnimation = CABasicAnimation(keyPath: "transform")
|
|
slideJuggleAnimation.toValue = CATransform3DMakeTranslation(6, 0, 0)
|
|
slideJuggleAnimation.duration = 1
|
|
slideJuggleAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
|
|
slideJuggleAnimation.autoreverses = true
|
|
slideJuggleAnimation.repeatCount = Float.infinity
|
|
audioRecordingCancelIndicator.layer.add(slideJuggleAnimation, forKey: "slide_juggle")
|
|
}
|
|
|
|
let audioRecordingTimeSize = audioRecordingTimeNode.measure(CGSize(width: 200.0, height: 100.0))
|
|
|
|
audioRecordingInfoContainerNode.frame = CGRect(
|
|
origin: CGPoint(
|
|
x: min(leftInset, width - audioRecordingTimeSize.width - 8.0 - 28.0),
|
|
y: 0.0
|
|
),
|
|
size: CGSize(width: baseWidth, height: panelHeight)
|
|
)
|
|
|
|
audioRecordingTimeNode.frame = CGRect(origin: CGPoint(x: 40.0, y: panelHeight - minimalHeight + floor((minimalHeight - audioRecordingTimeSize.height) / 2.0)), size: audioRecordingTimeSize)
|
|
if animateTimeSlideIn {
|
|
let position = audioRecordingTimeNode.layer.position
|
|
audioRecordingTimeNode.layer.animatePosition(from: CGPoint(x: position.x - 10.0, y: position.y), to: position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
|
|
audioRecordingTimeNode.layer.animateAlpha(from: 0, to: 1, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
|
|
}
|
|
|
|
let dotFrame = CGRect(origin: CGPoint(x: leftInset + 2.0 - UIScreenPixel, y: audioRecordingTimeNode.frame.midY - 20), size: CGSize(width: 40.0, height: 40))
|
|
|
|
var animateDotAppearing = false
|
|
let audioRecordingDotNode: AnimationNode
|
|
if let currentAudioRecordingDotNode = self.audioRecordingDotNode, !currentAudioRecordingDotNode.didPlay {
|
|
audioRecordingDotNode = currentAudioRecordingDotNode
|
|
} else {
|
|
self.audioRecordingDotNode?.removeFromSupernode()
|
|
audioRecordingDotNode = AnimationNode(animation: "BinRed")
|
|
|
|
self.audioRecordingDotNode = audioRecordingDotNode
|
|
self.audioRecordingDotNodeDismissed = false
|
|
self.clippingNode.insertSubnode(audioRecordingDotNode, belowSubnode: self.menuButton)
|
|
audioRecordingDotNode.frame = dotFrame
|
|
|
|
self.animatingBinNode?.removeFromSupernode()
|
|
self.animatingBinNode = nil
|
|
}
|
|
|
|
var resumingRecording = false
|
|
animateDotAppearing = transition.isAnimated && !hideInfo
|
|
if let mediaRecordingState = mediaRecordingState {
|
|
if case .waitingForPreview = mediaRecordingState {
|
|
self.recordingPaused = true
|
|
animateDotAppearing = false
|
|
} else {
|
|
if self.recordingPaused {
|
|
self.recordingPaused = false
|
|
resumingRecording = true
|
|
|
|
if (audioRecordingDotNode.layer.animationKeys() ?? []).isEmpty {
|
|
animateDotAppearing = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
audioRecordingDotNode.bounds = CGRect(origin: .zero, size: dotFrame.size)
|
|
audioRecordingDotNode.position = dotFrame.center
|
|
|
|
if animateDotAppearing {
|
|
Queue.mainQueue().justDispatch {
|
|
audioRecordingDotNode.layer.animateScale(from: 0.3, to: 1, duration: 0.15, delay: 0, removeOnCompletion: false)
|
|
|
|
let animateDot = { [weak audioRecordingDotNode] in
|
|
if let audioRecordingDotNode, audioRecordingDotNode.layer.animation(forKey: "recording") == nil {
|
|
audioRecordingDotNode.layer.animateAlpha(from: CGFloat(audioRecordingDotNode.layer.presentation()?.opacity ?? 0), to: 1, duration: 0.15, delay: 0, completion: { [weak audioRecordingDotNode] finished in
|
|
if finished {
|
|
let animation = CAKeyframeAnimation(keyPath: "opacity")
|
|
animation.values = [1.0 as NSNumber, 1.0 as NSNumber, 0.0 as NSNumber]
|
|
animation.keyTimes = [0.0 as NSNumber, 0.4546 as NSNumber, 0.9091 as NSNumber, 1 as NSNumber]
|
|
animation.duration = 0.5
|
|
animation.autoreverses = true
|
|
animation.repeatCount = Float.infinity
|
|
|
|
audioRecordingDotNode?.layer.add(animation, forKey: "recording")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
if resumingRecording {
|
|
animateDot()
|
|
} else {
|
|
audioRecordingTimeNode.started = {
|
|
animateDot()
|
|
}
|
|
}
|
|
}
|
|
self.attachmentButton.layer.animateAlpha(from: CGFloat(self.attachmentButton.layer.presentation()?.opacity ?? 1), to: 0, duration: 0.15, delay: 0, removeOnCompletion: false)
|
|
self.attachmentButton.layer.animateScale(from: 1, to: 0.3, duration: 0.15, delay: 0, removeOnCompletion: false)
|
|
}
|
|
|
|
if hideInfo {
|
|
audioRecordingDotNode.layer.removeAllAnimations()
|
|
audioRecordingDotNode.layer.animateAlpha(from: CGFloat(audioRecordingDotNode.layer.presentation()?.opacity ?? 1), to: 0, duration: 0.15, delay: 0, removeOnCompletion: false)
|
|
audioRecordingTimeNode.layer.animateAlpha(from: CGFloat(audioRecordingTimeNode.layer.presentation()?.opacity ?? 1), to: 0, duration: 0.15, delay: 0, removeOnCompletion: false)
|
|
audioRecordingCancelIndicator.layer.animateAlpha(from: CGFloat(audioRecordingCancelIndicator.layer.presentation()?.opacity ?? 1), to: 0, duration: 0.15, delay: 0, removeOnCompletion: false)
|
|
}
|
|
} else {
|
|
self.finishedTransitionToPreview = nil
|
|
|
|
var update = self.actionButtons.micButton.audioRecorder != nil || self.actionButtons.micButton.videoRecordingStatus != nil
|
|
self.actionButtons.micButton.audioRecorder = nil
|
|
self.actionButtons.micButton.videoRecordingStatus = nil
|
|
transition.updateAlpha(layer: self.textInputBackgroundNode.layer, alpha: 1.0)
|
|
if let textInputNode = self.textInputNode {
|
|
transition.updateAlpha(node: textInputNode, alpha: 1.0)
|
|
}
|
|
for (_, button) in self.accessoryItemButtons {
|
|
transition.updateAlpha(layer: button.layer, alpha: 1.0)
|
|
}
|
|
|
|
if let audioRecordingInfoContainerNode = self.audioRecordingInfoContainerNode {
|
|
self.audioRecordingInfoContainerNode = nil
|
|
transition.updateAlpha(node: audioRecordingInfoContainerNode, alpha: 0) { [weak audioRecordingInfoContainerNode] _ in
|
|
audioRecordingInfoContainerNode?.removeFromSupernode()
|
|
}
|
|
}
|
|
|
|
if let audioRecordingDotNode = self.audioRecordingDotNode {
|
|
let dismissDotNode = { [weak audioRecordingDotNode, weak self] in
|
|
guard let audioRecordingDotNode = audioRecordingDotNode, audioRecordingDotNode === self?.audioRecordingDotNode else { return }
|
|
|
|
self?.audioRecordingDotNode = nil
|
|
|
|
audioRecordingDotNode.layer.animateScale(from: 1.0, to: 0.3, duration: 0.15, delay: 0.0, removeOnCompletion: false)
|
|
audioRecordingDotNode.layer.animateAlpha(from: CGFloat(audioRecordingDotNode.layer.presentation()?.opacity ?? 1), to: 0.0, duration: 0.15, delay: 0.0, removeOnCompletion: false) { [weak audioRecordingDotNode] _ in
|
|
audioRecordingDotNode?.removeFromSupernode()
|
|
}
|
|
|
|
self?.attachmentButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: 0.0, removeOnCompletion: false)
|
|
self?.attachmentButton.layer.animateScale(from: 0.3, to: 1.0, duration: 0.15, delay: 0.0, removeOnCompletion: false)
|
|
}
|
|
|
|
if update && !self.audioRecordingDotNodeDismissed {
|
|
audioRecordingDotNode.layer.removeAllAnimations()
|
|
}
|
|
|
|
if self.isMediaDeleted {
|
|
if self.prevInputPanelNode is ChatRecordingPreviewInputPanelNode {
|
|
self.audioRecordingDotNode?.removeFromSupernode()
|
|
self.audioRecordingDotNode = nil
|
|
} else {
|
|
if !self.audioRecordingDotNodeDismissed {
|
|
audioRecordingDotNode.layer.removeAllAnimations()
|
|
}
|
|
audioRecordingDotNode.completion = dismissDotNode
|
|
audioRecordingDotNode.play()
|
|
update = true
|
|
}
|
|
} else {
|
|
dismissDotNode()
|
|
}
|
|
|
|
if update && !self.audioRecordingDotNodeDismissed {
|
|
self.audioRecordingDotNode?.layer.animatePosition(from: CGPoint(), to: CGPoint(x: leftMenuInset, y: 0.0), duration: 0.15, removeOnCompletion: false, additive: true)
|
|
self.audioRecordingDotNodeDismissed = true
|
|
}
|
|
}
|
|
|
|
if let audioRecordingTimeNode = self.audioRecordingTimeNode {
|
|
self.audioRecordingTimeNode = nil
|
|
|
|
let timePosition = audioRecordingTimeNode.position
|
|
transition.updatePosition(node: audioRecordingTimeNode, position: CGPoint(x: timePosition.x - audioRecordingTimeNode.bounds.width / 2.0, y: timePosition.y))
|
|
transition.updateTransformScale(node: audioRecordingTimeNode, scale: 0.1)
|
|
}
|
|
|
|
if let audioRecordingCancelIndicator = self.audioRecordingCancelIndicator {
|
|
self.audioRecordingCancelIndicator = nil
|
|
if transition.isAnimated {
|
|
audioRecordingCancelIndicator.layer.animateAlpha(from: audioRecordingCancelIndicator.alpha, to: 0.0, duration: 0.25, completion: { [weak audioRecordingCancelIndicator] _ in
|
|
audioRecordingCancelIndicator?.removeFromSupernode()
|
|
})
|
|
} else {
|
|
audioRecordingCancelIndicator.removeFromSupernode()
|
|
}
|
|
}
|
|
}
|
|
|
|
leftInset += leftMenuInset
|
|
|
|
transition.updateFrame(layer: self.attachmentButton.layer, frame: CGRect(origin: CGPoint(x: attachmentButtonX, y: hideOffset.y + panelHeight - minimalHeight), size: CGSize(width: 40.0, height: minimalHeight)))
|
|
transition.updateFrame(node: self.attachmentButtonDisabledNode, frame: self.attachmentButton.frame)
|
|
|
|
var composeButtonsOffset: CGFloat = 0.0
|
|
if self.extendedSearchLayout {
|
|
composeButtonsOffset = 44.0
|
|
textInputBackgroundWidthOffset = 36.0
|
|
}
|
|
|
|
self.updateCounterTextNode(transition: transition)
|
|
|
|
let actionButtonsFrame = CGRect(origin: CGPoint(x: hideOffset.x + width - rightInset - 43.0 - UIScreenPixel + composeButtonsOffset, y: hideOffset.y + panelHeight - minimalHeight), size: CGSize(width: 44.0, height: minimalHeight))
|
|
transition.updateFrame(node: self.actionButtons, frame: actionButtonsFrame)
|
|
if let (rect, containerSize) = self.absoluteRect {
|
|
self.actionButtons.updateAbsoluteRect(CGRect(x: rect.origin.x + actionButtonsFrame.origin.x, y: rect.origin.y + actionButtonsFrame.origin.y, width: actionButtonsFrame.width, height: actionButtonsFrame.height), within: containerSize, transition: transition)
|
|
}
|
|
|
|
if let presentationInterfaceState = self.presentationInterfaceState {
|
|
self.actionButtons.updateLayout(size: CGSize(width: 44.0, height: minimalHeight), isMediaInputExpanded: isMediaInputExpanded, currentMessageEffectId: presentationInterfaceState.interfaceState.sendMessageEffect, transition: transition, interfaceState: presentationInterfaceState)
|
|
}
|
|
|
|
let slowModeButtonFrame = CGRect(origin: CGPoint(x: hideOffset.x + width - rightInset - 5.0 - slowModeButtonSize.width + composeButtonsOffset, y: hideOffset.y + panelHeight - minimalHeight + 6.0), size: slowModeButtonSize)
|
|
transition.updateFrame(node: self.slowModeButton, frame: slowModeButtonFrame)
|
|
|
|
if let _ = interfaceState.inputTextPanelState.mediaRecordingState {
|
|
let text: String = interfaceState.strings.VoiceOver_MessageContextSend
|
|
let mediaRecordingAccessibilityArea: AccessibilityAreaNode
|
|
var added = false
|
|
if let current = self.mediaRecordingAccessibilityArea {
|
|
mediaRecordingAccessibilityArea = current
|
|
} else {
|
|
added = true
|
|
mediaRecordingAccessibilityArea = AccessibilityAreaNode()
|
|
mediaRecordingAccessibilityArea.accessibilityLabel = text
|
|
mediaRecordingAccessibilityArea.accessibilityTraits = [.button, .startsMediaSession]
|
|
self.mediaRecordingAccessibilityArea = mediaRecordingAccessibilityArea
|
|
mediaRecordingAccessibilityArea.activate = { [weak self] in
|
|
if let self {
|
|
self.interfaceInteraction?.finishMediaRecording(.send(viewOnce: self.viewOnce))
|
|
}
|
|
return true
|
|
}
|
|
self.clippingNode.insertSubnode(mediaRecordingAccessibilityArea, aboveSubnode: self.actionButtons)
|
|
}
|
|
self.actionButtons.isAccessibilityElement = false
|
|
let size: CGFloat = 120.0
|
|
mediaRecordingAccessibilityArea.frame = CGRect(origin: CGPoint(x: actionButtonsFrame.midX - size / 2.0, y: actionButtonsFrame.midY - size / 2.0), size: CGSize(width: size, height: size))
|
|
if added {
|
|
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.4, execute: {
|
|
[weak mediaRecordingAccessibilityArea] in
|
|
UIAccessibility.post(notification: UIAccessibility.Notification.layoutChanged, argument: mediaRecordingAccessibilityArea?.view)
|
|
})
|
|
}
|
|
} else {
|
|
self.actionButtons.isAccessibilityElement = true
|
|
if let mediaRecordingAccessibilityArea = self.mediaRecordingAccessibilityArea {
|
|
self.mediaRecordingAccessibilityArea = nil
|
|
mediaRecordingAccessibilityArea.removeFromSupernode()
|
|
}
|
|
}
|
|
|
|
let searchLayoutClearButtonSize = CGSize(width: 44.0, height: minimalHeight)
|
|
var textFieldInsets = self.textFieldInsets(metrics: metrics)
|
|
if additionalSideInsets.right > 0.0 {
|
|
textFieldInsets.right += additionalSideInsets.right / 3.0
|
|
}
|
|
self.actionButtons.micButton.isHidden = additionalSideInsets.right > 0.0
|
|
|
|
transition.updateFrame(layer: self.searchLayoutClearButton.layer, frame: CGRect(origin: CGPoint(x: width - rightInset - textFieldInsets.left - textFieldInsets.right + textInputBackgroundWidthOffset + 3.0, y: panelHeight - minimalHeight), size: searchLayoutClearButtonSize))
|
|
if let image = self.searchLayoutClearImageNode.image {
|
|
self.searchLayoutClearImageNode.frame = CGRect(origin: CGPoint(x: floor((searchLayoutClearButtonSize.width - image.size.width) / 2.0), y: floor((searchLayoutClearButtonSize.height - image.size.height) / 2.0)), size: image.size)
|
|
}
|
|
|
|
var textInputViewRealInsets = UIEdgeInsets()
|
|
if let presentationInterfaceState = self.presentationInterfaceState {
|
|
textInputViewRealInsets = calculateTextFieldRealInsets(presentationInterfaceState: presentationInterfaceState, accessoryButtonsWidth: accessoryButtonsWidth)
|
|
}
|
|
|
|
let textInputFrame = CGRect(x: hideOffset.x + leftInset + textFieldInsets.left, y: hideOffset.y + textFieldInsets.top, width: baseWidth - textFieldInsets.left - textFieldInsets.right, height: panelHeight - textFieldInsets.top - textFieldInsets.bottom)
|
|
transition.updateFrame(node: self.textInputContainer, frame: textInputFrame)
|
|
transition.updateFrame(node: self.textInputContainerBackgroundNode, frame: CGRect(origin: CGPoint(), size: textInputFrame.size))
|
|
transition.updateAlpha(node: self.textInputContainer, alpha: audioRecordingItemsAlpha)
|
|
|
|
if let textInputNode = self.textInputNode {
|
|
textInputNode.textContainerInset = textInputViewRealInsets
|
|
let textFieldFrame = CGRect(origin: CGPoint(x: self.textInputViewInternalInsets.left, y: self.textInputViewInternalInsets.top), size: CGSize(width: textInputFrame.size.width - (self.textInputViewInternalInsets.left + self.textInputViewInternalInsets.right), height: textInputFrame.size.height - self.textInputViewInternalInsets.top - textInputViewInternalInsets.bottom))
|
|
let shouldUpdateLayout = textFieldFrame.size != textInputNode.frame.size
|
|
//transition.updateFrame(node: textInputNode, frame: textFieldFrame)
|
|
textInputNode.frame = textFieldFrame
|
|
textInputNode.updateLayout(size: textFieldFrame.size)
|
|
self.updateInputField(textInputFrame: textFieldFrame, transition: ComponentTransition(transition))
|
|
if shouldUpdateLayout {
|
|
textInputNode.layout()
|
|
}
|
|
}
|
|
|
|
if interfaceState.slowmodeState == nil || isScheduledMessages, let contextPlaceholder = interfaceState.inputTextPanelState.contextPlaceholder {
|
|
let placeholderLayout = TextNode.asyncLayout(self.contextPlaceholderNode)
|
|
let (placeholderSize, placeholderApply) = placeholderLayout(TextNodeLayoutArguments(attributedString: contextPlaceholder, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - leftInset - rightInset - textFieldInsets.left - textFieldInsets.right - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right - accessoryButtonsWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
|
let contextPlaceholderNode = placeholderApply()
|
|
if let currentContextPlaceholderNode = self.contextPlaceholderNode, currentContextPlaceholderNode !== contextPlaceholderNode {
|
|
self.contextPlaceholderNode = nil
|
|
currentContextPlaceholderNode.removeFromSupernode()
|
|
}
|
|
|
|
if self.contextPlaceholderNode !== contextPlaceholderNode {
|
|
contextPlaceholderNode.displaysAsynchronously = false
|
|
contextPlaceholderNode.isUserInteractionEnabled = false
|
|
self.contextPlaceholderNode = contextPlaceholderNode
|
|
self.clippingNode.insertSubnode(contextPlaceholderNode, aboveSubnode: self.textPlaceholderNode)
|
|
}
|
|
|
|
let _ = placeholderApply()
|
|
|
|
let placeholderTransition: ContainedViewLayoutTransition
|
|
if placeholderSize.size.width == contextPlaceholderNode.frame.width {
|
|
placeholderTransition = transition
|
|
} else {
|
|
placeholderTransition = .immediate
|
|
}
|
|
placeholderTransition.updateFrame(node: contextPlaceholderNode, frame: CGRect(origin: CGPoint(x: hideOffset.x + leftInset + textFieldInsets.left + self.textInputViewInternalInsets.left, y: hideOffset.y + textFieldInsets.top + self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel), size: placeholderSize.size))
|
|
contextPlaceholderNode.alpha = audioRecordingItemsAlpha
|
|
} else if let contextPlaceholderNode = self.contextPlaceholderNode {
|
|
self.contextPlaceholderNode = nil
|
|
contextPlaceholderNode.removeFromSupernode()
|
|
self.textPlaceholderNode.alpha = 1.0
|
|
}
|
|
|
|
if let slowmodeState = interfaceState.slowmodeState, !isScheduledMessages && rightSlowModeInset.isZero {
|
|
let slowmodePlaceholderNode: ChatTextInputSlowmodePlaceholderNode
|
|
if let current = self.slowmodePlaceholderNode {
|
|
slowmodePlaceholderNode = current
|
|
} else {
|
|
slowmodePlaceholderNode = ChatTextInputSlowmodePlaceholderNode(theme: interfaceState.theme)
|
|
self.slowmodePlaceholderNode = slowmodePlaceholderNode
|
|
self.clippingNode.insertSubnode(slowmodePlaceholderNode, aboveSubnode: self.textPlaceholderNode)
|
|
}
|
|
let placeholderFrame = CGRect(origin: CGPoint(x: leftInset + textFieldInsets.left + self.textInputViewInternalInsets.left, y: textFieldInsets.top + self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel), size: CGSize(width: width - leftInset - rightInset - textFieldInsets.left - textFieldInsets.right - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right - accessoryButtonsWidth, height: 30.0))
|
|
slowmodePlaceholderNode.updateState(slowmodeState)
|
|
slowmodePlaceholderNode.frame = placeholderFrame
|
|
slowmodePlaceholderNode.alpha = audioRecordingItemsAlpha
|
|
slowmodePlaceholderNode.updateLayout(size: placeholderFrame.size)
|
|
} else if let slowmodePlaceholderNode = self.slowmodePlaceholderNode {
|
|
self.slowmodePlaceholderNode = nil
|
|
slowmodePlaceholderNode.removeFromSupernode()
|
|
}
|
|
|
|
if (interfaceState.slowmodeState != nil && rightSlowModeInset.isZero && !isScheduledMessages && interfaceState.editMessageState == nil) || interfaceState.inputTextPanelState.contextPlaceholder != nil {
|
|
self.textPlaceholderNode.isHidden = true
|
|
self.slowmodePlaceholderNode?.isHidden = inputHasText
|
|
} else {
|
|
self.textPlaceholderNode.isHidden = inputHasText
|
|
self.slowmodePlaceholderNode?.isHidden = true
|
|
}
|
|
|
|
var nextButtonTopRight = CGPoint(x: hideOffset.x + width - rightInset - textFieldInsets.right - accessoryButtonInset - rightSlowModeInset, y: hideOffset.y + panelHeight - textFieldInsets.bottom - minimalInputHeight)
|
|
for (item, button) in self.accessoryItemButtons.reversed() {
|
|
let buttonSize = CGSize(width: button.buttonWidth, height: minimalInputHeight)
|
|
button.updateLayout(item: item, size: buttonSize)
|
|
let buttonFrame = CGRect(origin: CGPoint(x: nextButtonTopRight.x - buttonSize.width, y: nextButtonTopRight.y + floor((minimalInputHeight - buttonSize.height) / 2.0)), size: buttonSize)
|
|
if button.supernode == nil {
|
|
self.clippingNode.addSubnode(button)
|
|
button.frame = buttonFrame.offsetBy(dx: -additionalOffset, dy: 0.0)
|
|
transition.updateFrame(layer: button.layer, frame: buttonFrame)
|
|
if animatedTransition {
|
|
button.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
|
button.layer.animateScale(from: 0.2, to: 1.0, duration: 0.25)
|
|
}
|
|
} else {
|
|
transition.updateFrame(layer: button.layer, frame: buttonFrame)
|
|
}
|
|
nextButtonTopRight.x -= buttonSize.width
|
|
nextButtonTopRight.x -= accessoryButtonSpacing
|
|
}
|
|
|
|
let textInputBackgroundFrame = CGRect(x: hideOffset.x + leftInset + textFieldInsets.left, y: hideOffset.y + textFieldInsets.top, width: baseWidth - textFieldInsets.left - textFieldInsets.right, height: panelHeight - textFieldInsets.top - textFieldInsets.bottom)
|
|
self.currentTextInputBackgroundWidthOffset = textInputBackgroundWidthOffset
|
|
transition.updateFrame(layer: self.textInputBackgroundNode.layer, frame: textInputBackgroundFrame)
|
|
transition.updateAlpha(node: self.textInputBackgroundNode, alpha: audioRecordingItemsAlpha)
|
|
|
|
let textPlaceholderSize: CGSize
|
|
let textPlaceholderMaxWidth: CGFloat = max(1.0, (nextButtonTopRight.x - textInputBackgroundFrame.minX) - 12.0)
|
|
|
|
if (updatedPlaceholder != nil && self.currentPlaceholder != updatedPlaceholder) || themeUpdated {
|
|
let currentPlaceholder = updatedPlaceholder ?? self.currentPlaceholder ?? ""
|
|
self.currentPlaceholder = currentPlaceholder
|
|
let baseFontSize = max(minInputFontSize, interfaceState.fontSize.baseDisplaySize)
|
|
self.textPlaceholderNode.attributedText = NSAttributedString(string: currentPlaceholder, font: Font.regular(baseFontSize), textColor: interfaceState.theme.chat.inputPanel.inputPlaceholderColor)
|
|
self.textInputNode?.textView.accessibilityHint = currentPlaceholder
|
|
let placeholderSize = self.textPlaceholderNode.updateLayout(CGSize(width: textPlaceholderMaxWidth, height: CGFloat.greatestFiniteMagnitude))
|
|
if transition.isAnimated, let snapshotLayer = self.textPlaceholderNode.layer.snapshotContentTree() {
|
|
self.textPlaceholderNode.supernode?.layer.insertSublayer(snapshotLayer, above: self.textPlaceholderNode.layer)
|
|
snapshotLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.22, removeOnCompletion: false, completion: { [weak snapshotLayer] _ in
|
|
snapshotLayer?.removeFromSuperlayer()
|
|
})
|
|
self.textPlaceholderNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18)
|
|
}
|
|
textPlaceholderSize = placeholderSize
|
|
} else {
|
|
textPlaceholderSize = self.textPlaceholderNode.bounds.size
|
|
}
|
|
|
|
let textPlaceholderFrame: CGRect
|
|
if sendingTextDisabled {
|
|
textPlaceholderFrame = CGRect(origin: CGPoint(x: textInputBackgroundFrame.minX + floor((textInputBackgroundFrame.width - textPlaceholderSize.width) / 2.0), y: textFieldInsets.top + self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel), size: textPlaceholderSize)
|
|
|
|
let textLockIconNode: ASImageNode
|
|
var textLockIconTransition = transition
|
|
if let current = self.textLockIconNode {
|
|
textLockIconNode = current
|
|
} else {
|
|
textLockIconTransition = .immediate
|
|
textLockIconNode = ASImageNode()
|
|
self.textLockIconNode = textLockIconNode
|
|
self.textPlaceholderNode.addSubnode(textLockIconNode)
|
|
|
|
textLockIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/TextLockIcon"), color: interfaceState.theme.chat.inputPanel.inputPlaceholderColor)
|
|
}
|
|
|
|
if let image = textLockIconNode.image {
|
|
textLockIconTransition.updateFrame(node: textLockIconNode, frame: CGRect(origin: CGPoint(x: -image.size.width - 4.0, y: floor((textPlaceholderFrame.height - image.size.height) / 2.0)), size: image.size))
|
|
}
|
|
} else {
|
|
textPlaceholderFrame = CGRect(origin: CGPoint(x: hideOffset.x + leftInset + textFieldInsets.left + self.textInputViewInternalInsets.left, y: hideOffset.y + textFieldInsets.top + self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel), size: textPlaceholderSize)
|
|
|
|
if let textLockIconNode = self.textLockIconNode {
|
|
self.textLockIconNode = nil
|
|
textLockIconNode.removeFromSupernode()
|
|
}
|
|
}
|
|
transition.updateFrame(node: self.textPlaceholderNode, frame: textPlaceholderFrame)
|
|
|
|
let textPlaceholderAlpha: CGFloat = audioRecordingItemsAlpha
|
|
transition.updateAlpha(node: self.textPlaceholderNode, alpha: textPlaceholderAlpha)
|
|
|
|
if let removeAccessoryButtons = removeAccessoryButtons {
|
|
for button in removeAccessoryButtons {
|
|
let buttonFrame = CGRect(origin: CGPoint(x: button.frame.origin.x + additionalOffset, y: panelHeight - textFieldInsets.bottom - minimalInputHeight), size: button.frame.size)
|
|
transition.updateFrame(layer: button.layer, frame: buttonFrame)
|
|
button.layer.animateScale(from: 1.0, to: 0.2, duration: 0.25, removeOnCompletion: false)
|
|
button.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak button] _ in
|
|
button?.removeFromSupernode()
|
|
})
|
|
}
|
|
}
|
|
|
|
if inputHasText || self.extendedSearchLayout {
|
|
hideMicButton = true
|
|
}
|
|
|
|
let mediaInputDisabled: Bool
|
|
if !interfaceState.voiceMessagesAvailable {
|
|
mediaInputDisabled = true
|
|
} else if interfaceState.hasActiveGroupCall {
|
|
mediaInputDisabled = true
|
|
} else if let channel = interfaceState.renderedPeer?.peer as? TelegramChannel, channel.hasBannedPermission(.banSendVoice, ignoreDefault: canBypassRestrictions) != nil, channel.hasBannedPermission(.banSendInstantVideos, ignoreDefault: canBypassRestrictions) != nil {
|
|
mediaInputDisabled = true
|
|
} else if let group = interfaceState.renderedPeer?.peer as? TelegramGroup, group.hasBannedPermission(.banSendVoice), group.hasBannedPermission(.banSendInstantVideos) {
|
|
mediaInputDisabled = true
|
|
} else {
|
|
mediaInputDisabled = false
|
|
}
|
|
|
|
var mediaInputIsActive = false
|
|
if case .media = interfaceState.inputMode {
|
|
mediaInputIsActive = true
|
|
}
|
|
|
|
self.actionButtons.micButton.fadeDisabled = mediaInputDisabled || mediaInputIsActive
|
|
|
|
self.updateActionButtons(hasText: inputHasText, hideMicButton: hideMicButton, animated: transition.isAnimated)
|
|
|
|
var viewOnceIsVisible = false
|
|
if let recordingState = interfaceState.inputTextPanelState.mediaRecordingState {
|
|
if case let .audio(_, isLocked) = recordingState {
|
|
viewOnceIsVisible = isLocked
|
|
} else if case let .video(_, isLocked) = recordingState {
|
|
viewOnceIsVisible = isLocked
|
|
}
|
|
}
|
|
|
|
if let prevInputPanelNode = self.prevInputPanelNode {
|
|
prevInputPanelNode.frame = CGRect(origin: .zero, size: prevInputPanelNode.frame.size)
|
|
}
|
|
if let prevPreviewInputPanelNode = self.prevInputPanelNode as? ChatRecordingPreviewInputPanelNode {
|
|
self.prevInputPanelNode = nil
|
|
|
|
if !prevPreviewInputPanelNode.viewOnceButton.isHidden {
|
|
self.viewOnce = prevPreviewInputPanelNode.viewOnce
|
|
self.viewOnceButton.update(isSelected: prevPreviewInputPanelNode.viewOnce, animated: false)
|
|
self.viewOnceButton.layer.animatePosition(from: prevPreviewInputPanelNode.viewOnceButton.position, to: self.viewOnceButton.position, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in
|
|
})
|
|
}
|
|
|
|
let animateOutPreviewButton: (ASDisplayNode) -> Void = { button in
|
|
if button.alpha > 0.0 {
|
|
if let snapshotView = button.view.snapshotContentTree() {
|
|
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
|
|
snapshotView.removeFromSuperview()
|
|
})
|
|
snapshotView.layer.animateScale(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
|
self.viewForOverlayContent?.addSubview(snapshotView)
|
|
}
|
|
}
|
|
}
|
|
|
|
animateOutPreviewButton(prevPreviewInputPanelNode.viewOnceButton)
|
|
animateOutPreviewButton(prevPreviewInputPanelNode.recordMoreButton)
|
|
|
|
prevPreviewInputPanelNode.gestureRecognizer?.isEnabled = false
|
|
prevPreviewInputPanelNode.isUserInteractionEnabled = false
|
|
|
|
if self.isMediaDeleted {
|
|
func animatePosition(for previewLayer: CALayer) {
|
|
previewLayer.animatePosition(
|
|
from: previewLayer.position,
|
|
to: CGPoint(x: leftMenuInset.isZero ? previewLayer.position.x - 20 : leftMenuInset + previewLayer.frame.width / 2.0, y: previewLayer.position.y),
|
|
duration: 0.15
|
|
)
|
|
}
|
|
|
|
animatePosition(for: prevPreviewInputPanelNode.waveformBackgroundNode.layer)
|
|
animatePosition(for: prevPreviewInputPanelNode.waveformScrubberNode.layer)
|
|
animatePosition(for: prevPreviewInputPanelNode.durationLabel.layer)
|
|
animatePosition(for: prevPreviewInputPanelNode.playButton.layer)
|
|
if let view = prevPreviewInputPanelNode.scrubber.view {
|
|
animatePosition(for: view.layer)
|
|
}
|
|
}
|
|
|
|
func animateAlpha(for previewLayer: CALayer) {
|
|
previewLayer.animateAlpha(
|
|
from: 1.0,
|
|
to: 0.0,
|
|
duration: 0.15,
|
|
removeOnCompletion: false
|
|
)
|
|
}
|
|
animateAlpha(for: prevPreviewInputPanelNode.waveformBackgroundNode.layer)
|
|
animateAlpha(for: prevPreviewInputPanelNode.waveformScrubberNode.layer)
|
|
animateAlpha(for: prevPreviewInputPanelNode.durationLabel.layer)
|
|
animateAlpha(for: prevPreviewInputPanelNode.playButton.layer)
|
|
if let view = prevPreviewInputPanelNode.scrubber.view {
|
|
animateAlpha(for: view.layer)
|
|
}
|
|
|
|
let binNode = prevPreviewInputPanelNode.binNode
|
|
self.animatingBinNode = binNode
|
|
let dismissBin = { [weak self, weak prevPreviewInputPanelNode, weak binNode] in
|
|
if binNode?.supernode != nil {
|
|
prevPreviewInputPanelNode?.deleteButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, delay: 0.0, removeOnCompletion: false) { [weak prevPreviewInputPanelNode] _ in
|
|
if prevPreviewInputPanelNode?.supernode === self {
|
|
prevPreviewInputPanelNode?.removeFromSupernode()
|
|
}
|
|
}
|
|
prevPreviewInputPanelNode?.deleteButton.layer.animateScale(from: 1.0, to: 0.3, duration: 0.15, delay: 0.0, removeOnCompletion: false)
|
|
|
|
if isRecording {
|
|
self?.attachmentButton.layer.animateAlpha(from: 0.0, to: 0, duration: 0.01, delay: 0.0, removeOnCompletion: false)
|
|
self?.attachmentButton.layer.animateScale(from: 1, to: 0.3, duration: 0.01, delay: 0.0, removeOnCompletion: false)
|
|
} else {
|
|
self?.attachmentButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: 0.0, removeOnCompletion: false)
|
|
self?.attachmentButton.layer.animateScale(from: 0.3, to: 1.0, duration: 0.15, delay: 0.0, removeOnCompletion: false)
|
|
}
|
|
} else if prevPreviewInputPanelNode?.supernode === self {
|
|
prevPreviewInputPanelNode?.removeFromSupernode()
|
|
}
|
|
}
|
|
|
|
if self.isMediaDeleted {
|
|
Queue.mainQueue().after(0.5, {
|
|
self.isMediaDeleted = false
|
|
})
|
|
}
|
|
|
|
if self.isMediaDeleted && !isRecording {
|
|
self.attachmentButton.layer.animateAlpha(from: 0.0, to: 0, duration: 0.01, delay: 0.0, removeOnCompletion: false)
|
|
binNode.completion = dismissBin
|
|
binNode.play()
|
|
} else {
|
|
dismissBin()
|
|
}
|
|
|
|
prevPreviewInputPanelNode.deleteButton.layer.animatePosition(from: CGPoint(), to: CGPoint(x: leftMenuInset, y: 0.0), duration: 0.15, removeOnCompletion: false, additive: true)
|
|
|
|
prevPreviewInputPanelNode.sendButton.layer.animateScale(from: 1.0, to: 0.3, duration: 0.15, removeOnCompletion: false)
|
|
prevPreviewInputPanelNode.sendButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
|
|
|
self.actionButtons.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: 0, removeOnCompletion: false)
|
|
self.actionButtons.layer.animateScale(from: 0.3, to: 1.0, duration: 0.15, delay: 0, removeOnCompletion: false)
|
|
|
|
if hasMenuButton {
|
|
if isSendAsButton {
|
|
|
|
} else {
|
|
self.menuButton.alpha = 1.0
|
|
self.menuButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: 0, removeOnCompletion: false)
|
|
self.menuButton.transform = CATransform3DIdentity
|
|
self.menuButton.layer.animateScale(from: 0.3, to: 1.0, duration: 0.15, delay: 0, removeOnCompletion: false)
|
|
}
|
|
}
|
|
}
|
|
|
|
var clippingDelta: CGFloat = 0.0
|
|
if case let .media(_, _, focused) = interfaceState.inputMode, focused {
|
|
clippingDelta = -panelHeight
|
|
}
|
|
transition.updateFrame(node: self.clippingNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: panelHeight)))
|
|
transition.updateSublayerTransformOffset(layer: self.clippingNode.layer, offset: CGPoint(x: 0.0, y: clippingDelta))
|
|
|
|
let viewOnceSize = self.viewOnceButton.update(theme: interfaceState.theme)
|
|
let viewOnceButtonFrame = CGRect(origin: CGPoint(x: width - rightInset - 44.0 - UIScreenPixel, y: -152.0), size: viewOnceSize)
|
|
self.viewOnceButton.bounds = CGRect(origin: .zero, size: viewOnceButtonFrame.size)
|
|
transition.updatePosition(node: self.viewOnceButton, position: viewOnceButtonFrame.center)
|
|
|
|
if self.viewOnceButton.alpha.isZero && viewOnceIsVisible {
|
|
self.viewOnceButton.update(isSelected: self.viewOnce, animated: false)
|
|
}
|
|
transition.updateAlpha(node: self.viewOnceButton, alpha: viewOnceIsVisible ? 1.0 : 0.0)
|
|
transition.updateTransformScale(node: self.viewOnceButton, scale: viewOnceIsVisible ? 1.0 : 0.01)
|
|
if let user = interfaceState.renderedPeer?.peer as? TelegramUser, user.id != interfaceState.accountPeerId && user.botInfo == nil {
|
|
self.viewOnceButton.isHidden = false
|
|
} else {
|
|
self.viewOnceButton.isHidden = true
|
|
}
|
|
|
|
return panelHeight
|
|
}
|
|
|
|
@objc private func slowModeButtonPressed() {
|
|
self.interfaceInteraction?.openBoostToUnrestrict()
|
|
}
|
|
|
|
@objc private func viewOncePressed() {
|
|
guard let context = self.context, let interfaceState = self.presentationInterfaceState else {
|
|
return
|
|
}
|
|
self.viewOnce = !self.viewOnce
|
|
|
|
self.viewOnceButton.update(isSelected: self.viewOnce, animated: true)
|
|
|
|
self.tooltipController?.dismiss()
|
|
if self.viewOnce {
|
|
self.displayViewOnceTooltip(text: interfaceState.strings.Chat_PlayVoiceMessageOnceTooltip)
|
|
|
|
let _ = ApplicationSpecificNotice.incrementVoiceMessagesPlayOnceSuggestion(accountManager: context.sharedContext.accountManager, count: 3).startStandalone()
|
|
}
|
|
}
|
|
|
|
private func displayViewOnceTooltip(text: String) {
|
|
guard let context = self.context, let parentController = self.interfaceInteraction?.chatController() else {
|
|
return
|
|
}
|
|
|
|
let absoluteFrame = self.viewOnceButton.view.convert(self.viewOnceButton.bounds, to: parentController.view)
|
|
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX - 20.0, y: absoluteFrame.midY), size: CGSize())
|
|
|
|
let tooltipController = TooltipScreen(
|
|
account: context.account,
|
|
sharedContext: context.sharedContext,
|
|
text: .plain(text: text),
|
|
balancedTextLayout: true,
|
|
constrainWidth: 240.0,
|
|
style: .customBlur(UIColor(rgb: 0x18181a), 0.0),
|
|
arrowStyle: .small,
|
|
icon: .animation(name: "anim_autoremove_on", delay: 0.1, tintColor: nil),
|
|
location: .point(location, .right),
|
|
displayDuration: .default,
|
|
inset: 8.0,
|
|
cornerRadius: 8.0,
|
|
shouldDismissOnTouch: { _, _ in
|
|
return .ignore
|
|
}
|
|
)
|
|
self.tooltipController = tooltipController
|
|
|
|
parentController.present(tooltipController, in: .current)
|
|
}
|
|
|
|
override func canHandleTransition(from prevInputPanelNode: ChatInputPanelNode?) -> Bool {
|
|
return prevInputPanelNode is ChatRecordingPreviewInputPanelNode
|
|
}
|
|
|
|
func chatInputTextNodeDidUpdateText() {
|
|
if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState, let context = self.context {
|
|
let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize)
|
|
refreshChatTextInputAttributes(context: context, textView: textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: (self.context?.animatedEmojiStickersValue.keys).flatMap(Set.init) ?? Set(), emojiViewProvider: self.emojiViewProvider, makeCollapsedQuoteAttachment: { text, attributes in
|
|
return ChatInputTextCollapsedQuoteAttachmentImpl(text: text, attributes: attributes)
|
|
})
|
|
refreshChatTextInputTypingAttributes(textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize)
|
|
|
|
self.updateSpoiler()
|
|
|
|
let inputTextState = self.inputTextState
|
|
|
|
self.interfaceInteraction?.updateTextInputStateAndMode({ _, inputMode in return (inputTextState, inputMode) })
|
|
self.interfaceInteraction?.updateInputLanguage({ _ in return textInputNode.textInputMode?.primaryLanguage })
|
|
self.updateTextNodeText(animated: true)
|
|
|
|
self.updateCounterTextNode(transition: .immediate)
|
|
}
|
|
}
|
|
|
|
@objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) {
|
|
self.chatInputTextNodeDidUpdateText()
|
|
}
|
|
|
|
private func updateSpoiler() {
|
|
guard let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState else {
|
|
return
|
|
}
|
|
|
|
let textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor
|
|
|
|
var rects: [CGRect] = []
|
|
var customEmojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute, CGFloat)] = []
|
|
|
|
let fontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize)
|
|
|
|
if let attributedText = textInputNode.attributedText {
|
|
let beginning = textInputNode.textView.beginningOfDocument
|
|
attributedText.enumerateAttributes(in: NSMakeRange(0, attributedText.length), options: [], using: { attributes, range, _ in
|
|
if let _ = attributes[ChatTextInputAttributes.spoiler] {
|
|
func addSpoiler(startIndex: Int, endIndex: Int) {
|
|
if let start = textInputNode.textView.position(from: beginning, offset: startIndex), let end = textInputNode.textView.position(from: start, offset: endIndex - startIndex), let textRange = textInputNode.textView.textRange(from: start, to: end) {
|
|
let textRects = textInputNode.textView.selectionRects(for: textRange)
|
|
for textRect in textRects {
|
|
if textRect.rect.width > 1.0 && textRect.rect.size.height > 1.0 {
|
|
rects.append(textRect.rect.insetBy(dx: 1.0, dy: 1.0).offsetBy(dx: 0.0, dy: 1.0))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var startIndex: Int?
|
|
var currentIndex: Int?
|
|
|
|
let nsString = (attributedText.string as NSString)
|
|
nsString.enumerateSubstrings(in: range, options: .byComposedCharacterSequences) { substring, range, _, _ in
|
|
if let substring = substring, substring.rangeOfCharacter(from: .whitespacesAndNewlines) != nil {
|
|
if let currentStartIndex = startIndex {
|
|
startIndex = nil
|
|
let endIndex = range.location
|
|
addSpoiler(startIndex: currentStartIndex, endIndex: endIndex)
|
|
}
|
|
} else if startIndex == nil {
|
|
startIndex = range.location
|
|
}
|
|
currentIndex = range.location + range.length
|
|
}
|
|
|
|
if let currentStartIndex = startIndex, let currentIndex = currentIndex {
|
|
startIndex = nil
|
|
let endIndex = currentIndex
|
|
addSpoiler(startIndex: currentStartIndex, endIndex: endIndex)
|
|
}
|
|
}
|
|
|
|
if let value = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute {
|
|
if let start = textInputNode.textView.position(from: beginning, offset: range.location), let end = textInputNode.textView.position(from: start, offset: range.length), let textRange = textInputNode.textView.textRange(from: start, to: end) {
|
|
let textRects = textInputNode.textView.selectionRects(for: textRange)
|
|
for textRect in textRects {
|
|
var emojiFontSize = fontSize
|
|
if let font = attributes[.font] as? UIFont {
|
|
emojiFontSize = font.pointSize
|
|
}
|
|
customEmojiRects.append((textRect.rect, value, emojiFontSize))
|
|
break
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
if !rects.isEmpty {
|
|
let dustNode: InvisibleInkDustNode
|
|
if let current = self.dustNode {
|
|
dustNode = current
|
|
} else {
|
|
dustNode = InvisibleInkDustNode(textNode: nil, enableAnimations: self.context?.sharedContext.energyUsageSettings.fullTranslucency ?? true)
|
|
dustNode.alpha = self.spoilersRevealed ? 0.0 : 1.0
|
|
dustNode.isUserInteractionEnabled = false
|
|
textInputNode.textView.addSubview(dustNode.view)
|
|
self.dustNode = dustNode
|
|
}
|
|
dustNode.frame = CGRect(origin: CGPoint(), size: textInputNode.textView.contentSize)
|
|
dustNode.update(size: textInputNode.textView.contentSize, color: textColor, textColor: textColor, rects: rects, wordRects: rects)
|
|
} else if let dustNode = self.dustNode {
|
|
dustNode.removeFromSupernode()
|
|
self.dustNode = nil
|
|
}
|
|
|
|
if !customEmojiRects.isEmpty {
|
|
let customEmojiContainerView: CustomEmojiContainerView
|
|
if let current = self.customEmojiContainerView {
|
|
customEmojiContainerView = current
|
|
} else {
|
|
customEmojiContainerView = CustomEmojiContainerView(emojiViewProvider: { [weak self] emoji in
|
|
guard let strongSelf = self, let emojiViewProvider = strongSelf.emojiViewProvider else {
|
|
return nil
|
|
}
|
|
return emojiViewProvider(emoji)
|
|
})
|
|
customEmojiContainerView.isUserInteractionEnabled = false
|
|
textInputNode.textView.addSubview(customEmojiContainerView)
|
|
self.customEmojiContainerView = customEmojiContainerView
|
|
}
|
|
|
|
customEmojiContainerView.update(fontSize: fontSize, textColor: textColor, emojiRects: customEmojiRects)
|
|
} else if let customEmojiContainerView = self.customEmojiContainerView {
|
|
customEmojiContainerView.removeFromSuperview()
|
|
self.customEmojiContainerView = nil
|
|
}
|
|
}
|
|
|
|
private func updateSpoilersRevealed(animated: Bool = true) {
|
|
guard let textInputNode = self.textInputNode else {
|
|
return
|
|
}
|
|
|
|
let selectionRange = textInputNode.textView.selectedRange
|
|
|
|
var revealed = false
|
|
if let attributedText = textInputNode.attributedText {
|
|
attributedText.enumerateAttributes(in: NSMakeRange(0, attributedText.length), options: [], using: { attributes, range, _ in
|
|
if let _ = attributes[ChatTextInputAttributes.spoiler] {
|
|
if let _ = selectionRange.intersection(range) {
|
|
revealed = true
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
guard self.spoilersRevealed != revealed else {
|
|
return
|
|
}
|
|
self.spoilersRevealed = revealed
|
|
|
|
if revealed {
|
|
self.updateInternalSpoilersRevealed(true, animated: animated)
|
|
} else {
|
|
Queue.mainQueue().after(1.5, {
|
|
self.updateInternalSpoilersRevealed(false, animated: true)
|
|
})
|
|
}
|
|
}
|
|
|
|
private func updateInternalSpoilersRevealed(_ revealed: Bool, animated: Bool) {
|
|
guard self.spoilersRevealed == revealed, let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState, let context = self.context else {
|
|
return
|
|
}
|
|
|
|
let textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor
|
|
let accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor
|
|
let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize)
|
|
|
|
textInputNode.textView.isScrollEnabled = false
|
|
|
|
refreshChatTextInputAttributes(context: context, textView: textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: (self.context?.animatedEmojiStickersValue.keys).flatMap(Set.init) ?? Set(), emojiViewProvider: self.emojiViewProvider, makeCollapsedQuoteAttachment: { text, attributes in
|
|
return ChatInputTextCollapsedQuoteAttachmentImpl(text: text, attributes: attributes)
|
|
})
|
|
|
|
textInputNode.attributedText = textAttributedStringForStateText(context: context, stateText: self.inputTextState.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed, availableEmojis: (self.context?.animatedEmojiStickersValue.keys).flatMap(Set.init) ?? Set(), emojiViewProvider: self.emojiViewProvider, makeCollapsedQuoteAttachment: { text, attributes in
|
|
return ChatInputTextCollapsedQuoteAttachmentImpl(text: text, attributes: attributes)
|
|
})
|
|
|
|
if textInputNode.textView.subviews.count > 1, animated {
|
|
let containerView = textInputNode.textView.subviews[1]
|
|
if let canvasView = containerView.subviews.first {
|
|
if let snapshotView = canvasView.snapshotView(afterScreenUpdates: false) {
|
|
snapshotView.frame = canvasView.frame.offsetBy(dx: 0.0, dy: -textInputNode.textView.contentOffset.y)
|
|
textInputNode.view.insertSubview(snapshotView, at: 0)
|
|
canvasView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
|
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView, weak textInputNode] _ in
|
|
textInputNode?.textView.isScrollEnabled = false
|
|
snapshotView?.removeFromSuperview()
|
|
Queue.mainQueue().after(0.1) {
|
|
textInputNode?.textView.isScrollEnabled = true
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
Queue.mainQueue().after(0.1) {
|
|
textInputNode.textView.isScrollEnabled = true
|
|
}
|
|
|
|
if animated {
|
|
if revealed {
|
|
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear)
|
|
if let dustNode = self.dustNode {
|
|
transition.updateAlpha(node: dustNode, alpha: 0.0)
|
|
}
|
|
} else {
|
|
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear)
|
|
if let dustNode = self.dustNode {
|
|
transition.updateAlpha(node: dustNode, alpha: 1.0)
|
|
}
|
|
}
|
|
} else if let dustNode = self.dustNode {
|
|
dustNode.alpha = revealed ? 0.0 : 1.0
|
|
}
|
|
}
|
|
|
|
private struct EmojiSuggestionPosition: Equatable {
|
|
var range: NSRange
|
|
var value: String
|
|
}
|
|
|
|
private final class CurrentEmojiSuggestion {
|
|
var localPosition: CGPoint
|
|
var position: EmojiSuggestionPosition
|
|
let disposable: MetaDisposable
|
|
var value: [TelegramMediaFile]?
|
|
|
|
init(localPosition: CGPoint, position: EmojiSuggestionPosition, disposable: MetaDisposable, value: [TelegramMediaFile]?) {
|
|
self.localPosition = localPosition
|
|
self.position = position
|
|
self.disposable = disposable
|
|
self.value = value
|
|
}
|
|
}
|
|
|
|
private var currentEmojiSuggestion: CurrentEmojiSuggestion?
|
|
private var currentEmojiSuggestionView: ComponentHostView<Empty>?
|
|
|
|
private var dismissedEmojiSuggestionPosition: EmojiSuggestionPosition?
|
|
|
|
private func updateInputField(textInputFrame: CGRect, transition: ComponentTransition) {
|
|
guard let textInputNode = self.textInputNode, let context = self.context else {
|
|
return
|
|
}
|
|
|
|
var hasTracking = false
|
|
var hasTrackingView = false
|
|
if textInputNode.selectedRange.length == 0, textInputNode.selectedRange.location > 0, let attributedText = textInputNode.textView.attributedText {
|
|
let selectedSubstring = attributedText.attributedSubstring(from: NSRange(location: 0, length: textInputNode.selectedRange.location))
|
|
if let lastCharacter = selectedSubstring.string.last, String(lastCharacter).isSingleEmoji {
|
|
let queryLength = (String(lastCharacter) as NSString).length
|
|
if selectedSubstring.attribute(ChatTextInputAttributes.customEmoji, at: selectedSubstring.length - queryLength, effectiveRange: nil) == nil {
|
|
let beginning = textInputNode.textView.beginningOfDocument
|
|
|
|
let characterRange = NSRange(location: selectedSubstring.length - queryLength, length: queryLength)
|
|
|
|
let start = textInputNode.textView.position(from: beginning, offset: selectedSubstring.length - queryLength)
|
|
let end = textInputNode.textView.position(from: beginning, offset: selectedSubstring.length)
|
|
|
|
if let start = start, let end = end, let textRange = textInputNode.textView.textRange(from: start, to: end) {
|
|
let selectionRects = textInputNode.textView.selectionRects(for: textRange)
|
|
let emojiSuggestionPosition = EmojiSuggestionPosition(range: characterRange, value: String(lastCharacter))
|
|
|
|
hasTracking = true
|
|
|
|
if let trackingRect = selectionRects.first?.rect {
|
|
let trackingPosition = CGPoint(x: trackingRect.midX, y: trackingRect.minY)
|
|
|
|
if self.dismissedEmojiSuggestionPosition == emojiSuggestionPosition {
|
|
} else {
|
|
hasTrackingView = true
|
|
|
|
var beginRequest = false
|
|
let suggestionContext: CurrentEmojiSuggestion
|
|
if let current = self.currentEmojiSuggestion, current.position.value == emojiSuggestionPosition.value {
|
|
suggestionContext = current
|
|
} else {
|
|
beginRequest = true
|
|
suggestionContext = CurrentEmojiSuggestion(localPosition: trackingPosition, position: emojiSuggestionPosition, disposable: MetaDisposable(), value: nil)
|
|
|
|
self.currentEmojiSuggestion?.disposable.dispose()
|
|
self.currentEmojiSuggestion = suggestionContext
|
|
}
|
|
suggestionContext.localPosition = trackingPosition
|
|
suggestionContext.position = emojiSuggestionPosition
|
|
self.dismissedEmojiSuggestionPosition = nil
|
|
|
|
if beginRequest {
|
|
suggestionContext.disposable.set((EmojiSuggestionsComponent.suggestionData(context: context, isSavedMessages: self.presentationInterfaceState?.chatLocation.peerId == self.context?.account.peerId, query: String(lastCharacter))
|
|
|> deliverOnMainQueue).startStrict(next: { [weak self, weak suggestionContext] result in
|
|
guard let strongSelf = self, let suggestionContext = suggestionContext, strongSelf.currentEmojiSuggestion === suggestionContext else {
|
|
return
|
|
}
|
|
|
|
suggestionContext.value = result
|
|
|
|
if let textInputNode = strongSelf.textInputNode {
|
|
strongSelf.updateInputField(textInputFrame: textInputNode.frame, transition: .immediate)
|
|
}
|
|
}).strict())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if !hasTracking {
|
|
self.dismissedEmojiSuggestionPosition = nil
|
|
}
|
|
|
|
if let currentEmojiSuggestion = self.currentEmojiSuggestion, let value = currentEmojiSuggestion.value, value.isEmpty {
|
|
hasTrackingView = false
|
|
}
|
|
if !textInputNode.textView.isFirstResponder {
|
|
hasTrackingView = false
|
|
}
|
|
|
|
if !hasTrackingView {
|
|
if let currentEmojiSuggestion = self.currentEmojiSuggestion {
|
|
self.currentEmojiSuggestion = nil
|
|
currentEmojiSuggestion.disposable.dispose()
|
|
}
|
|
|
|
if let currentEmojiSuggestionView = self.currentEmojiSuggestionView {
|
|
self.currentEmojiSuggestionView = nil
|
|
|
|
currentEmojiSuggestionView.alpha = 0.0
|
|
currentEmojiSuggestionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak currentEmojiSuggestionView] _ in
|
|
currentEmojiSuggestionView?.removeFromSuperview()
|
|
})
|
|
}
|
|
}
|
|
|
|
if let context = self.context, let theme = self.theme, let viewForOverlayContent = self.viewForOverlayContent, let presentationContext = self.presentationContext, let currentEmojiSuggestion = self.currentEmojiSuggestion, let value = currentEmojiSuggestion.value {
|
|
let currentEmojiSuggestionView: ComponentHostView<Empty>
|
|
if let current = self.currentEmojiSuggestionView {
|
|
currentEmojiSuggestionView = current
|
|
} else {
|
|
currentEmojiSuggestionView = ComponentHostView<Empty>()
|
|
self.currentEmojiSuggestionView = currentEmojiSuggestionView
|
|
viewForOverlayContent.addSubview(currentEmojiSuggestionView)
|
|
|
|
currentEmojiSuggestionView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
|
|
|
self.installEmojiSuggestionPreviewGesture(hostView: currentEmojiSuggestionView)
|
|
}
|
|
|
|
let globalPosition = textInputNode.textView.convert(currentEmojiSuggestion.localPosition, to: self.view)
|
|
|
|
let sideInset: CGFloat = 16.0
|
|
|
|
let viewSize = currentEmojiSuggestionView.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(EmojiSuggestionsComponent(
|
|
context: context,
|
|
userLocation: .other,
|
|
theme: EmojiSuggestionsComponent.Theme(theme: theme),
|
|
animationCache: presentationContext.animationCache,
|
|
animationRenderer: presentationContext.animationRenderer,
|
|
files: value,
|
|
action: { [weak self] file in
|
|
guard let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction, let currentEmojiSuggestion = strongSelf.currentEmojiSuggestion else {
|
|
return
|
|
}
|
|
|
|
AudioServicesPlaySystemSound(0x450)
|
|
|
|
interfaceInteraction.updateTextInputStateAndMode { textInputState, inputMode in
|
|
let inputText = NSMutableAttributedString(attributedString: textInputState.inputText)
|
|
|
|
var text: String?
|
|
var emojiAttribute: ChatTextInputTextCustomEmojiAttribute?
|
|
loop: for attribute in file.attributes {
|
|
switch attribute {
|
|
case let .CustomEmoji(_, _, displayText, _):
|
|
text = displayText
|
|
emojiAttribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: file.fileId.id, file: file)
|
|
break loop
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
if let emojiAttribute = emojiAttribute, let text = text {
|
|
let replacementText = NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: emojiAttribute])
|
|
|
|
let range = currentEmojiSuggestion.position.range
|
|
let previousText = inputText.attributedSubstring(from: range)
|
|
inputText.replaceCharacters(in: range, with: replacementText)
|
|
|
|
var replacedUpperBound = range.lowerBound
|
|
while true {
|
|
if inputText.attributedSubstring(from: NSRange(location: 0, length: replacedUpperBound)).string.hasSuffix(previousText.string) {
|
|
let replaceRange = NSRange(location: replacedUpperBound - previousText.length, length: previousText.length)
|
|
if replaceRange.location < 0 {
|
|
break
|
|
}
|
|
let adjacentString = inputText.attributedSubstring(from: replaceRange)
|
|
if adjacentString.string != previousText.string || adjacentString.attribute(ChatTextInputAttributes.customEmoji, at: 0, effectiveRange: nil) != nil {
|
|
break
|
|
}
|
|
inputText.replaceCharacters(in: replaceRange, with: NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: emojiAttribute.interactivelySelectedFromPackId, fileId: emojiAttribute.fileId, file: emojiAttribute.file)]))
|
|
replacedUpperBound = replaceRange.lowerBound
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
let selectionPosition = range.lowerBound + (replacementText.string as NSString).length
|
|
|
|
return (ChatTextInputState(inputText: inputText, selectionRange: selectionPosition ..< selectionPosition), inputMode)
|
|
}
|
|
|
|
return (textInputState, inputMode)
|
|
}
|
|
|
|
if let textInputNode = strongSelf.textInputNode {
|
|
strongSelf.dismissedEmojiSuggestionPosition = currentEmojiSuggestion.position
|
|
strongSelf.updateInputField(textInputFrame: textInputNode.frame, transition: .immediate)
|
|
}
|
|
}
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: self.bounds.width - sideInset * 2.0, height: 100.0)
|
|
)
|
|
|
|
let viewFrame = CGRect(origin: CGPoint(x: min(self.bounds.width - sideInset - viewSize.width, max(sideInset, floor(globalPosition.x - viewSize.width / 2.0))), y: globalPosition.y - 2.0 - viewSize.height), size: viewSize)
|
|
currentEmojiSuggestionView.frame = viewFrame
|
|
if let componentView = currentEmojiSuggestionView.componentView as? EmojiSuggestionsComponent.View {
|
|
componentView.adjustBackground(relativePositionX: floor(globalPosition.x - viewFrame.minX))
|
|
}
|
|
}
|
|
}
|
|
|
|
private func updateCounterTextNode(transition: ContainedViewLayoutTransition) {
|
|
var inputTextMaxLength: Int32?
|
|
if let presentationInterfaceState = self.presentationInterfaceState {
|
|
if let editMessage = presentationInterfaceState.interfaceState.editMessage, let inputTextMaxLengthValue = editMessage.inputTextMaxLength {
|
|
inputTextMaxLength = inputTextMaxLengthValue
|
|
} else if case let .customChatContents(customChatContents) = presentationInterfaceState.subject, case .businessLinkSetup = customChatContents.kind {
|
|
inputTextMaxLength = 4096
|
|
}
|
|
}
|
|
|
|
if let presentationInterfaceState = self.presentationInterfaceState, let textInputNode = self.textInputNode, let inputTextMaxLength {
|
|
let textCount = Int32(textInputNode.textView.text.count)
|
|
let counterColor: UIColor = textCount > inputTextMaxLength ? presentationInterfaceState.theme.chat.inputPanel.panelControlDestructiveColor : presentationInterfaceState.theme.chat.inputPanel.panelControlColor
|
|
|
|
let remainingCount = max(-999, inputTextMaxLength - textCount)
|
|
let counterText = remainingCount >= 5 ? "" : "\(remainingCount)"
|
|
self.counterTextNode.attributedText = NSAttributedString(string: counterText, font: counterFont, textColor: counterColor)
|
|
} else {
|
|
self.counterTextNode.attributedText = NSAttributedString(string: "", font: counterFont, textColor: .black)
|
|
}
|
|
|
|
if let (width, leftInset, rightInset, _, _, maxHeight, metrics, _, _) = self.validLayout {
|
|
var composeButtonsOffset: CGFloat = 0.0
|
|
if self.extendedSearchLayout {
|
|
composeButtonsOffset = 44.0
|
|
}
|
|
|
|
let (_, textFieldHeight) = self.calculateTextFieldMetrics(width: width - leftInset - rightInset - self.leftMenuInset - self.rightSlowModeInset + self.currentTextInputBackgroundWidthOffset, maxHeight: maxHeight, metrics: metrics)
|
|
let panelHeight = self.panelHeight(textFieldHeight: textFieldHeight, metrics: metrics)
|
|
var textFieldMinHeight: CGFloat = 33.0
|
|
if let presentationInterfaceState = self.presentationInterfaceState {
|
|
textFieldMinHeight = calclulateTextFieldMinHeight(presentationInterfaceState, metrics: metrics)
|
|
}
|
|
let minimalHeight: CGFloat = 14.0 + textFieldMinHeight
|
|
|
|
let counterSize = self.counterTextNode.updateLayout(CGSize(width: 44.0, height: 44.0))
|
|
let actionButtonsOriginX = width - rightInset - 43.0 - UIScreenPixel + composeButtonsOffset
|
|
let counterFrame = CGRect(origin: CGPoint(x: actionButtonsOriginX, y: panelHeight - minimalHeight - counterSize.height + 3.0), size: CGSize(width: width - actionButtonsOriginX - rightInset, height: counterSize.height))
|
|
transition.updateFrame(node: self.counterTextNode, frame: counterFrame)
|
|
}
|
|
}
|
|
|
|
private func installEmojiSuggestionPreviewGesture(hostView: UIView) {
|
|
let peekRecognizer = PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point in
|
|
guard let self else {
|
|
return nil
|
|
}
|
|
return self.emojiSuggestionPeekContentAtPoint(point: point)
|
|
}, present: { [weak self] content, sourceView, sourceRect in
|
|
guard let strongSelf = self, let context = strongSelf.context else {
|
|
return nil
|
|
}
|
|
|
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
|
let controller = PeekController(presentationData: presentationData, content: content, sourceView: {
|
|
return (sourceView, sourceRect)
|
|
})
|
|
//strongSelf.peekController = controller
|
|
strongSelf.interfaceInteraction?.presentGlobalOverlayController(controller, nil)
|
|
return controller
|
|
}, updateContent: { [weak self] content in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
|
|
let _ = strongSelf
|
|
})
|
|
hostView.addGestureRecognizer(peekRecognizer)
|
|
}
|
|
|
|
private func emojiSuggestionPeekContentAtPoint(point: CGPoint) -> Signal<(UIView, CGRect, PeekControllerContent)?, NoError>? {
|
|
guard let presentationInterfaceState = self.presentationInterfaceState else {
|
|
return nil
|
|
}
|
|
guard let chatPeerId = presentationInterfaceState.renderedPeer?.peer?.id else {
|
|
return nil
|
|
}
|
|
guard let context = self.context else {
|
|
return nil
|
|
}
|
|
|
|
var maybeFile: TelegramMediaFile?
|
|
var maybeItemLayer: CALayer?
|
|
|
|
if let currentEmojiSuggestionView = self.currentEmojiSuggestionView?.componentView as? EmojiSuggestionsComponent.View {
|
|
if let (itemLayer, file) = currentEmojiSuggestionView.item(at: point) {
|
|
maybeFile = file
|
|
maybeItemLayer = itemLayer
|
|
}
|
|
}
|
|
|
|
guard let file = maybeFile else {
|
|
return nil
|
|
}
|
|
guard let itemLayer = maybeItemLayer else {
|
|
return nil
|
|
}
|
|
|
|
let _ = chatPeerId
|
|
let _ = file
|
|
let _ = itemLayer
|
|
|
|
var collectionId: ItemCollectionId?
|
|
for attribute in file.attributes {
|
|
if case let .CustomEmoji(_, _, _, packReference) = attribute {
|
|
switch packReference {
|
|
case let .id(id, _):
|
|
collectionId = ItemCollectionId(namespace: Namespaces.ItemCollection.CloudEmojiPacks, id: id)
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
var bubbleUpEmojiOrStickersets: [ItemCollectionId] = []
|
|
if let collectionId {
|
|
bubbleUpEmojiOrStickersets.append(collectionId)
|
|
}
|
|
|
|
let accountPeerId = context.account.peerId
|
|
|
|
let _ = bubbleUpEmojiOrStickersets
|
|
let _ = context
|
|
let _ = accountPeerId
|
|
|
|
return context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: accountPeerId))
|
|
|> map { peer -> Bool in
|
|
var hasPremium = false
|
|
if case let .user(user) = peer, user.isPremium {
|
|
hasPremium = true
|
|
}
|
|
return hasPremium
|
|
}
|
|
|> deliverOnMainQueue
|
|
|> map { [weak self, weak itemLayer] hasPremium -> (UIView, CGRect, PeekControllerContent)? in
|
|
guard let strongSelf = self, let itemLayer = itemLayer else {
|
|
return nil
|
|
}
|
|
|
|
let _ = strongSelf
|
|
let _ = itemLayer
|
|
|
|
var menuItems: [ContextMenuItem] = []
|
|
menuItems.removeAll()
|
|
|
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
|
let _ = presentationData
|
|
|
|
var isLocked = false
|
|
if !hasPremium {
|
|
isLocked = file.isPremiumEmoji
|
|
if isLocked && chatPeerId == context.account.peerId {
|
|
isLocked = false
|
|
}
|
|
}
|
|
|
|
if let interaction = strongSelf.interfaceInteraction {
|
|
let _ = interaction
|
|
|
|
let sendEmoji: (TelegramMediaFile) -> Void = { file in
|
|
guard let self else {
|
|
return
|
|
}
|
|
guard let controller = (self.interfaceInteraction?.chatController() as? ChatControllerImpl) else {
|
|
return
|
|
}
|
|
|
|
var text = "."
|
|
var emojiAttribute: ChatTextInputTextCustomEmojiAttribute?
|
|
loop: for attribute in file.attributes {
|
|
switch attribute {
|
|
case let .CustomEmoji(_, _, displayText, stickerPackReference):
|
|
text = displayText
|
|
|
|
var packId: ItemCollectionId?
|
|
if case let .id(id, _) = stickerPackReference {
|
|
packId = ItemCollectionId(namespace: Namespaces.ItemCollection.CloudEmojiPacks, id: id)
|
|
}
|
|
emojiAttribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: packId, fileId: file.fileId.id, file: file)
|
|
break loop
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
if let emojiAttribute {
|
|
controller.controllerInteraction?.sendEmoji(text, emojiAttribute, true)
|
|
}
|
|
}
|
|
let setStatus: (TelegramMediaFile) -> Void = { file in
|
|
guard let self, let context = self.context else {
|
|
return
|
|
}
|
|
guard let controller = (self.interfaceInteraction?.chatController() as? ChatControllerImpl) else {
|
|
return
|
|
}
|
|
|
|
let _ = context.engine.accountData.setEmojiStatus(file: file, expirationDate: nil).startStandalone()
|
|
|
|
var animateInAsReplacement = false
|
|
animateInAsReplacement = false
|
|
/*if let currentUndoOverlayController = strongSelf.currentUndoOverlayController {
|
|
currentUndoOverlayController.dismissWithCommitActionAndReplacementAnimation()
|
|
strongSelf.currentUndoOverlayController = nil
|
|
animateInAsReplacement = true
|
|
}*/
|
|
|
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
|
|
|
let undoController = UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, loop: true, title: nil, text: presentationData.strings.EmojiStatus_AppliedText, undoText: nil, customAction: nil), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { _ in return false })
|
|
//strongSelf.currentUndoOverlayController = controller
|
|
controller.controllerInteraction?.presentController(undoController, nil)
|
|
}
|
|
let copyEmoji: (TelegramMediaFile) -> Void = { file in
|
|
var text = "."
|
|
var emojiAttribute: ChatTextInputTextCustomEmojiAttribute?
|
|
loop: for attribute in file.attributes {
|
|
switch attribute {
|
|
case let .CustomEmoji(_, _, displayText, _):
|
|
text = displayText
|
|
|
|
emojiAttribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: file.fileId.id, file: file)
|
|
break loop
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
if let _ = emojiAttribute {
|
|
storeMessageTextInPasteboard(text, entities: [MessageTextEntity(range: 0 ..< (text as NSString).length, type: .CustomEmoji(stickerPack: nil, fileId: file.fileId.id))])
|
|
}
|
|
}
|
|
|
|
menuItems.append(.action(ContextMenuActionItem(text: presentationData.strings.EmojiPreview_SendEmoji, icon: { theme in
|
|
if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Download"), color: theme.actionSheet.primaryTextColor) {
|
|
return generateImage(image.size, rotatedContext: { size, context in
|
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
|
|
|
if let cgImage = image.cgImage {
|
|
context.draw(cgImage, in: CGRect(origin: CGPoint(), size: size))
|
|
}
|
|
})
|
|
} else {
|
|
return nil
|
|
}
|
|
}, action: { _, f in
|
|
sendEmoji(file)
|
|
f(.default)
|
|
})))
|
|
|
|
menuItems.append(.action(ContextMenuActionItem(text: presentationData.strings.EmojiPreview_SetAsStatus, icon: { theme in
|
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Smile"), color: theme.actionSheet.primaryTextColor)
|
|
}, action: { _, f in
|
|
f(.default)
|
|
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
|
|
if hasPremium {
|
|
setStatus(file)
|
|
} else {
|
|
var replaceImpl: ((ViewController) -> Void)?
|
|
let controller = PremiumDemoScreen(context: context, subject: .animatedEmoji, action: {
|
|
let controller = PremiumIntroScreen(context: context, source: .animatedEmoji)
|
|
replaceImpl?(controller)
|
|
})
|
|
replaceImpl = { [weak controller] c in
|
|
controller?.replace(with: c)
|
|
}
|
|
strongSelf.interfaceInteraction?.getNavigationController()?.pushViewController(controller)
|
|
}
|
|
})))
|
|
|
|
menuItems.append(.action(ContextMenuActionItem(text: presentationData.strings.EmojiPreview_CopyEmoji, icon: { theme in
|
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.actionSheet.primaryTextColor)
|
|
}, action: { _, f in
|
|
copyEmoji(file)
|
|
f(.default)
|
|
})))
|
|
}
|
|
|
|
if menuItems.isEmpty {
|
|
return nil
|
|
}
|
|
|
|
let content = StickerPreviewPeekContent(context: context, theme: presentationData.theme, strings: presentationData.strings, item: .pack(file), isLocked: isLocked, menu: menuItems, openPremiumIntro: { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
guard let interfaceInteraction = self.interfaceInteraction else {
|
|
return
|
|
}
|
|
|
|
let _ = self
|
|
let _ = interfaceInteraction
|
|
|
|
let controller = PremiumIntroScreen(context: context, source: .stickers)
|
|
//let _ = controller
|
|
|
|
interfaceInteraction.getNavigationController()?.pushViewController(controller)
|
|
})
|
|
let _ = content
|
|
//return nil
|
|
|
|
return (strongSelf.view, itemLayer.convert(itemLayer.bounds, to: strongSelf.view.layer), content)
|
|
}
|
|
}
|
|
|
|
private func updateTextNodeText(animated: Bool) {
|
|
var inputHasText = false
|
|
var hideMicButton = false
|
|
if let textInputNode = self.textInputNode, let attributedText = textInputNode.attributedText, attributedText.length != 0 {
|
|
inputHasText = true
|
|
hideMicButton = true
|
|
}
|
|
|
|
var isScheduledMessages = false
|
|
if case .scheduledMessages = self.presentationInterfaceState?.subject {
|
|
isScheduledMessages = true
|
|
}
|
|
|
|
if let interfaceState = self.presentationInterfaceState {
|
|
if (interfaceState.slowmodeState != nil && !isScheduledMessages && interfaceState.editMessageState == nil) || interfaceState.inputTextPanelState.contextPlaceholder != nil {
|
|
self.textPlaceholderNode.isHidden = true
|
|
self.slowmodePlaceholderNode?.isHidden = inputHasText
|
|
} else {
|
|
self.textPlaceholderNode.isHidden = inputHasText
|
|
self.slowmodePlaceholderNode?.isHidden = true
|
|
}
|
|
}
|
|
|
|
let _ = hideMicButton
|
|
// self.updateActionButtons(hasText: inputHasText, hideMicButton: hideMicButton, animated: animated)
|
|
self.updateTextHeight(animated: animated)
|
|
}
|
|
|
|
private func updateActionButtons(hasText: Bool, hideMicButton: Bool, animated: Bool) {
|
|
var hideMicButton = hideMicButton
|
|
|
|
var mediaInputIsActive = false
|
|
var keepSendButtonEnabled = self.keepSendButtonEnabled
|
|
if let presentationInterfaceState = self.presentationInterfaceState {
|
|
if let mediaRecordingState = presentationInterfaceState.inputTextPanelState.mediaRecordingState {
|
|
if case .video(.editing, false) = mediaRecordingState {
|
|
hideMicButton = true
|
|
}
|
|
}
|
|
if case .media = presentationInterfaceState.inputMode {
|
|
mediaInputIsActive = true
|
|
}
|
|
|
|
if case let .customChatContents(customChatContents) = presentationInterfaceState.subject {
|
|
switch customChatContents.kind {
|
|
case .hashTagSearch:
|
|
break
|
|
case .quickReplyMessageInput:
|
|
break
|
|
case .businessLinkSetup:
|
|
keepSendButtonEnabled = true
|
|
}
|
|
}
|
|
}
|
|
|
|
var animateWithBounce = false
|
|
if self.extendedSearchLayout {
|
|
hideMicButton = true
|
|
|
|
if !self.actionButtons.sendContainerNode.alpha.isZero {
|
|
self.actionButtons.sendContainerNode.alpha = 0.0
|
|
self.actionButtons.sendButtonRadialStatusNode?.alpha = 0.0
|
|
self.actionButtons.updateAccessibility()
|
|
if animated {
|
|
self.actionButtons.animatingSendButton = true
|
|
self.actionButtons.sendContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in
|
|
if let strongSelf = self {
|
|
strongSelf.actionButtons.animatingSendButton = false
|
|
strongSelf.applyUpdateSendButtonIcon()
|
|
}
|
|
})
|
|
self.actionButtons.sendContainerNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.2)
|
|
|
|
self.actionButtons.sendButtonRadialStatusNode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
|
self.actionButtons.sendButtonRadialStatusNode?.layer.animateScale(from: 1.0, to: 0.2, duration: 0.2)
|
|
}
|
|
}
|
|
if self.searchLayoutClearButton.alpha.isZero {
|
|
self.searchLayoutClearButton.alpha = 1.0
|
|
if animated {
|
|
self.searchLayoutClearButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
|
|
self.searchLayoutClearButton.layer.animateScale(from: 0.8, to: 1.0, duration: 0.2)
|
|
}
|
|
}
|
|
} else {
|
|
animateWithBounce = true
|
|
if !self.searchLayoutClearButton.alpha.isZero {
|
|
animateWithBounce = false
|
|
self.searchLayoutClearButton.alpha = 0.0
|
|
if animated {
|
|
self.searchLayoutClearButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
|
self.searchLayoutClearButton.layer.animateScale(from: 1.0, to: 0.8, duration: 0.2)
|
|
}
|
|
}
|
|
|
|
let hasSlowModeButton = self.rightSlowModeInset > 0.0
|
|
if hasSlowModeButton {
|
|
hideMicButton = true
|
|
if self.slowModeButton.alpha.isZero {
|
|
self.slowModeButton.alpha = 1.0
|
|
if animated {
|
|
self.slowModeButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
|
|
if animateWithBounce {
|
|
self.slowModeButton.layer.animateSpring(from: NSNumber(value: Float(0.1)), to: NSNumber(value: Float(1.0)), keyPath: "transform.scale", duration: 0.6)
|
|
} else {
|
|
self.slowModeButton.layer.animateScale(from: 0.2, to: 1.0, duration: 0.25)
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if !self.slowModeButton.alpha.isZero {
|
|
self.slowModeButton.alpha = 0.0
|
|
if animated {
|
|
self.slowModeButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
|
}
|
|
}
|
|
}
|
|
|
|
if (hasText || keepSendButtonEnabled && !mediaInputIsActive && !hasSlowModeButton || SGSimpleSettings.shared.hideRecordingButton) {
|
|
hideMicButton = true
|
|
|
|
if self.actionButtons.sendContainerNode.alpha.isZero && self.rightSlowModeInset.isZero {
|
|
self.actionButtons.sendContainerNode.alpha = 1.0
|
|
self.actionButtons.sendButtonRadialStatusNode?.alpha = 1.0
|
|
self.actionButtons.updateAccessibility()
|
|
if animated {
|
|
self.actionButtons.sendContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
|
|
self.actionButtons.sendButtonRadialStatusNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
|
|
if animateWithBounce {
|
|
self.actionButtons.sendContainerNode.layer.animateSpring(from: NSNumber(value: Float(0.1)), to: NSNumber(value: Float(1.0)), keyPath: "transform.scale", duration: 0.6)
|
|
self.actionButtons.sendButtonRadialStatusNode?.layer.animateSpring(from: NSNumber(value: Float(0.1)), to: NSNumber(value: Float(1.0)), keyPath: "transform.scale", duration: 0.6)
|
|
} else {
|
|
self.actionButtons.sendContainerNode.layer.animateScale(from: 0.2, to: 1.0, duration: 0.25)
|
|
self.actionButtons.sendButtonRadialStatusNode?.layer.animateScale(from: 0.2, to: 1.0, duration: 0.25)
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if !self.actionButtons.sendContainerNode.alpha.isZero {
|
|
self.actionButtons.sendContainerNode.alpha = 0.0
|
|
self.actionButtons.sendButtonRadialStatusNode?.alpha = 0.0
|
|
self.actionButtons.updateAccessibility()
|
|
if animated {
|
|
self.actionButtons.animatingSendButton = true
|
|
self.actionButtons.sendContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in
|
|
if let strongSelf = self {
|
|
strongSelf.actionButtons.animatingSendButton = false
|
|
strongSelf.applyUpdateSendButtonIcon()
|
|
}
|
|
})
|
|
self.actionButtons.sendButtonRadialStatusNode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let hideExpandMediaInput = hideMicButton
|
|
|
|
if mediaInputIsActive {
|
|
hideMicButton = true
|
|
}
|
|
|
|
if let interfaceState = self.presentationInterfaceState {
|
|
if case let .customChatContents(customChatContents) = interfaceState.subject {
|
|
switch customChatContents.kind {
|
|
case .hashTagSearch:
|
|
break
|
|
case .quickReplyMessageInput:
|
|
break
|
|
case .businessLinkSetup:
|
|
hideMicButton = true
|
|
}
|
|
}
|
|
}
|
|
|
|
if hideMicButton {
|
|
if !self.actionButtons.micButton.alpha.isZero {
|
|
self.actionButtons.micButton.alpha = 0.0
|
|
if animated {
|
|
self.actionButtons.micButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
|
}
|
|
}
|
|
} else {
|
|
let micAlpha: CGFloat = self.actionButtons.micButton.fadeDisabled ? 0.5 : 1.0
|
|
if !self.actionButtons.micButton.alpha.isEqual(to: micAlpha) {
|
|
self.actionButtons.micButton.alpha = micAlpha
|
|
if animated {
|
|
self.actionButtons.micButton.layer.animateAlpha(from: 0.0, to: micAlpha, duration: 0.1)
|
|
if animateWithBounce {
|
|
self.actionButtons.micButton.layer.animateSpring(from: NSNumber(value: Float(0.1)), to: NSNumber(value: Float(1.0)), keyPath: "transform.scale", duration: 0.6)
|
|
} else {
|
|
self.actionButtons.micButton.layer.animateScale(from: 0.2, to: 1.0, duration: 0.25)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if mediaInputIsActive && !hideExpandMediaInput {
|
|
if self.actionButtons.expandMediaInputButton.alpha.isZero {
|
|
self.actionButtons.expandMediaInputButton.alpha = 1.0
|
|
if animated {
|
|
self.actionButtons.expandMediaInputButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
|
|
if animateWithBounce {
|
|
self.actionButtons.expandMediaInputButton.layer.animateSpring(from: NSNumber(value: Float(0.1)), to: NSNumber(value: Float(1.0)), keyPath: "transform.scale", duration: 0.6)
|
|
} else {
|
|
self.actionButtons.expandMediaInputButton.layer.animateScale(from: 0.2, to: 1.0, duration: 0.25)
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if !self.actionButtons.expandMediaInputButton.alpha.isZero {
|
|
self.actionButtons.expandMediaInputButton.alpha = 0.0
|
|
if animated {
|
|
self.actionButtons.expandMediaInputButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
|
}
|
|
}
|
|
}
|
|
|
|
self.actionButtons.updateAccessibility()
|
|
}
|
|
|
|
private func updateTextHeight(animated: Bool) {
|
|
if let (width, leftInset, rightInset, _, additionalSideInsets, maxHeight, metrics, _, _) = self.validLayout {
|
|
let (_, textFieldHeight) = self.calculateTextFieldMetrics(width: width - leftInset - rightInset - additionalSideInsets.right - self.leftMenuInset - self.rightSlowModeInset + self.currentTextInputBackgroundWidthOffset, maxHeight: maxHeight, metrics: metrics)
|
|
let panelHeight = self.panelHeight(textFieldHeight: textFieldHeight, metrics: metrics)
|
|
if !self.bounds.size.height.isEqual(to: panelHeight) {
|
|
self.updateHeight(animated)
|
|
} else {
|
|
if let textInputNode = self.textInputNode {
|
|
self.updateInputField(textInputFrame: textInputNode.frame, transition: .immediate)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func updateIsProcessingInlineRequest(_ value: Bool) {
|
|
if value {
|
|
if self.searchActivityIndicator == nil, let currentState = self.presentationInterfaceState {
|
|
let searchActivityIndicator = ActivityIndicator(type: .custom(currentState.theme.list.itemAccentColor, 20.0, 1.5, true))
|
|
searchActivityIndicator.isUserInteractionEnabled = false
|
|
self.searchActivityIndicator = searchActivityIndicator
|
|
let indicatorSize = searchActivityIndicator.measure(CGSize(width: 100.0, height: 100.0))
|
|
let size = self.searchLayoutClearButton.bounds.size
|
|
searchActivityIndicator.frame = CGRect(origin: CGPoint(x: floor((size.width - indicatorSize.width) / 2.0) + 0.0, y: floor((size.height - indicatorSize.height) / 2.0) - 0.0), size: indicatorSize)
|
|
//self.searchLayoutClearImageNode.isHidden = true
|
|
self.searchLayoutClearButton.addSubnode(searchActivityIndicator)
|
|
}
|
|
} else if let searchActivityIndicator = self.searchActivityIndicator {
|
|
self.searchActivityIndicator = nil
|
|
//self.searchLayoutClearImageNode.isHidden = false
|
|
searchActivityIndicator.removeFromSupernode()
|
|
}
|
|
}
|
|
|
|
func chatInputTextNodeShouldReturn() -> Bool {
|
|
if self.actionButtons.sendButton.supernode != nil && !self.actionButtons.sendButton.isHidden && !self.actionButtons.sendContainerNode.alpha.isZero {
|
|
self.sendButtonPressed()
|
|
}
|
|
return false
|
|
}
|
|
|
|
@objc func editableTextNodeShouldReturn(_ editableTextNode: ASEditableTextNode) -> Bool {
|
|
return self.chatInputTextNodeShouldReturn()
|
|
}
|
|
|
|
private func applyUpdateSendButtonIcon() {
|
|
if let interfaceState = self.presentationInterfaceState {
|
|
var sendButtonHasApplyIcon = interfaceState.interfaceState.editMessage != nil
|
|
if case let .customChatContents(customChatContents) = interfaceState.subject {
|
|
switch customChatContents.kind {
|
|
case .hashTagSearch:
|
|
break
|
|
case .quickReplyMessageInput:
|
|
break
|
|
case .businessLinkSetup:
|
|
sendButtonHasApplyIcon = true
|
|
}
|
|
}
|
|
|
|
if sendButtonHasApplyIcon != self.actionButtons.sendButtonHasApplyIcon {
|
|
self.actionButtons.sendButtonHasApplyIcon = sendButtonHasApplyIcon
|
|
if self.actionButtons.sendButtonHasApplyIcon {
|
|
self.actionButtons.sendButton.setImage(PresentationResourcesChat.chatInputPanelApplyIconImage(interfaceState.theme), for: [])
|
|
} else {
|
|
if case .scheduledMessages = interfaceState.subject {
|
|
self.actionButtons.sendButton.setImage(PresentationResourcesChat.chatInputPanelScheduleIconImage(interfaceState.theme), for: [])
|
|
} else {
|
|
self.actionButtons.sendButton.setImage(PresentationResourcesChat.chatInputPanelSendIconImage(interfaceState.theme), for: [])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func chatInputTextNodeDidChangeSelection(dueToEditing: Bool) {
|
|
if !dueToEditing && !self.updatingInputState {
|
|
let inputTextState = self.inputTextState
|
|
self.interfaceInteraction?.updateTextInputStateAndMode({ _, inputMode in return (inputTextState, inputMode) })
|
|
}
|
|
|
|
if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState {
|
|
if case .format = self.inputMenu.state {
|
|
self.inputMenu.hide()
|
|
}
|
|
|
|
let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize)
|
|
refreshChatTextInputTypingAttributes(textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize)
|
|
|
|
self.updateSpoilersRevealed()
|
|
|
|
self.updateInputField(textInputFrame: textInputNode.frame, transition: .immediate)
|
|
}
|
|
}
|
|
|
|
@objc func editableTextNodeDidChangeSelection(_ editableTextNode: ASEditableTextNode, fromSelectedRange: NSRange, toSelectedRange: NSRange, dueToEditing: Bool) {
|
|
self.chatInputTextNodeDidChangeSelection(dueToEditing: dueToEditing)
|
|
}
|
|
|
|
func chatInputTextNodeDidBeginEditing() {
|
|
guard let interfaceInteraction = self.interfaceInteraction, let presentationInterfaceState = self.presentationInterfaceState else {
|
|
return
|
|
}
|
|
|
|
switch presentationInterfaceState.inputMode {
|
|
case .text:
|
|
break
|
|
case .media:
|
|
break
|
|
case .inputButtons, .none:
|
|
if self.textInputNode?.textView.inputView == nil {
|
|
interfaceInteraction.updateInputModeAndDismissedButtonKeyboardMessageId({ state in
|
|
return (.text, state.keyboardButtonsMessage?.id)
|
|
})
|
|
}
|
|
}
|
|
|
|
self.inputMenu.activate()
|
|
}
|
|
|
|
@objc func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) {
|
|
self.chatInputTextNodeDidBeginEditing()
|
|
}
|
|
|
|
var skipPresentationInterfaceStateUpdate = false
|
|
func chatInputTextNodeDidFinishEditing() {
|
|
guard let editableTextNode = self.textInputNode else {
|
|
return
|
|
}
|
|
|
|
self.storedInputLanguage = editableTextNode.textInputMode?.primaryLanguage
|
|
self.inputMenu.deactivate()
|
|
self.dismissedEmojiSuggestionPosition = nil
|
|
|
|
if let presentationInterfaceState = self.presentationInterfaceState, !self.skipPresentationInterfaceStateUpdate {
|
|
if let peer = presentationInterfaceState.renderedPeer?.peer as? TelegramUser, peer.botInfo != nil, let keyboardButtonsMessage = presentationInterfaceState.keyboardButtonsMessage, let keyboardMarkup = keyboardButtonsMessage.visibleButtonKeyboardMarkup, keyboardMarkup.flags.contains(.persistent) {
|
|
self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId { _ in
|
|
return (.inputButtons(persistent: true), nil)
|
|
}
|
|
} else {
|
|
switch presentationInterfaceState.inputMode {
|
|
case .text:
|
|
self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId { _ in
|
|
return (.none, nil)
|
|
}
|
|
case .media:
|
|
break
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) {
|
|
self.chatInputTextNodeDidFinishEditing()
|
|
}
|
|
|
|
func chatInputTextNodeBackspaceWhileEmpty() {
|
|
}
|
|
|
|
func editableTextNodeTarget(forAction action: Selector) -> ASEditableTextNodeTargetForAction? {
|
|
if action == makeSelectorFromString("_accessibilitySpeak:") {
|
|
if case .format = self.inputMenu.state {
|
|
return ASEditableTextNodeTargetForAction(target: nil)
|
|
} else if let textInputNode = self.textInputNode, textInputNode.selectedRange.length > 0 {
|
|
return ASEditableTextNodeTargetForAction(target: self)
|
|
} else {
|
|
return ASEditableTextNodeTargetForAction(target: nil)
|
|
}
|
|
} else if action == makeSelectorFromString("_accessibilitySpeakSpellOut:") {
|
|
if case .format = self.inputMenu.state {
|
|
return ASEditableTextNodeTargetForAction(target: nil)
|
|
} else if let textInputNode = self.textInputNode, textInputNode.selectedRange.length > 0 {
|
|
return nil
|
|
} else {
|
|
return ASEditableTextNodeTargetForAction(target: nil)
|
|
}
|
|
}
|
|
else if action == makeSelectorFromString("_accessibilitySpeakLanguageSelection:") || action == makeSelectorFromString("_accessibilityPauseSpeaking:") || action == makeSelectorFromString("_accessibilitySpeakSentence:") {
|
|
return ASEditableTextNodeTargetForAction(target: nil)
|
|
} else if action == makeSelectorFromString("_showTextStyleOptions:") {
|
|
if #available(iOS 16.0, *) {
|
|
return ASEditableTextNodeTargetForAction(target: nil)
|
|
} else {
|
|
if case .general = self.inputMenu.state {
|
|
if let textInputNode = self.textInputNode, textInputNode.attributedText == nil || textInputNode.attributedText!.length == 0 || textInputNode.selectedRange.length == 0 {
|
|
return ASEditableTextNodeTargetForAction(target: nil)
|
|
}
|
|
return ASEditableTextNodeTargetForAction(target: self)
|
|
} else {
|
|
return ASEditableTextNodeTargetForAction(target: nil)
|
|
}
|
|
}
|
|
} else if action == #selector(self.formatAttributesBold(_:)) || action == #selector(self.formatAttributesItalic(_:)) || action == #selector(self.formatAttributesMonospace(_:)) || action == #selector(self.formatAttributesLink(_:)) || action == #selector(self.formatAttributesStrikethrough(_:)) || action == #selector(self.formatAttributesUnderline(_:)) || action == #selector(self.formatAttributesSpoiler(_:)) || action == #selector(self.formatAttributesQuote(_:)) || action == #selector(self.formatAttributesCodeBlock(_:)) {
|
|
if case .format = self.inputMenu.state {
|
|
if action == #selector(self.formatAttributesSpoiler(_:)), let selectedRange = self.textInputNode?.selectedRange {
|
|
var intersectsMonospace = false
|
|
self.inputTextState.inputText.enumerateAttributes(in: selectedRange, options: [], using: { attributes, _, _ in
|
|
if let _ = attributes[ChatTextInputAttributes.monospace] {
|
|
intersectsMonospace = true
|
|
}
|
|
})
|
|
if !intersectsMonospace {
|
|
return ASEditableTextNodeTargetForAction(target: self)
|
|
} else {
|
|
return ASEditableTextNodeTargetForAction(target: nil)
|
|
}
|
|
} else if action == #selector(self.formatAttributesQuote(_:)), let selectedRange = self.textInputNode?.selectedRange {
|
|
let _ = selectedRange
|
|
return ASEditableTextNodeTargetForAction(target: self)
|
|
} else if action == #selector(self.formatAttributesCodeBlock(_:)), let selectedRange = self.textInputNode?.selectedRange {
|
|
let _ = selectedRange
|
|
return ASEditableTextNodeTargetForAction(target: self)
|
|
} else if action == #selector(self.formatAttributesMonospace(_:)), let selectedRange = self.textInputNode?.selectedRange {
|
|
var intersectsSpoiler = false
|
|
self.inputTextState.inputText.enumerateAttributes(in: selectedRange, options: [], using: { attributes, _, _ in
|
|
if let _ = attributes[ChatTextInputAttributes.spoiler] {
|
|
intersectsSpoiler = true
|
|
}
|
|
})
|
|
if !intersectsSpoiler {
|
|
return ASEditableTextNodeTargetForAction(target: self)
|
|
} else {
|
|
return ASEditableTextNodeTargetForAction(target: nil)
|
|
}
|
|
} else {
|
|
return ASEditableTextNodeTargetForAction(target: self)
|
|
}
|
|
} else {
|
|
return ASEditableTextNodeTargetForAction(target: nil)
|
|
}
|
|
}
|
|
if case .format = self.inputMenu.state {
|
|
return ASEditableTextNodeTargetForAction(target: nil)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var suggestedActionCounter: Int = 0
|
|
|
|
@available(iOS 13.0, *)
|
|
func chatInputTextNodeMenu(forTextRange textRange: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu {
|
|
guard let editableTextNode = self.textInputNode else {
|
|
return UIMenu(children: [])
|
|
}
|
|
|
|
var actions = suggestedActions
|
|
|
|
if #available(iOS 16.0, *) {
|
|
if let index = actions.firstIndex(where: { $0.description.contains("identifier = com.apple.menu.replace;") }), let subMenu = actions[index] as? UIMenu {
|
|
var filteredChildren = subMenu.children
|
|
if let subIndex = filteredChildren.firstIndex(where: { $0.description.contains("identifier = com.apple.menu.autofill;") }) {
|
|
filteredChildren.remove(at: subIndex)
|
|
}
|
|
actions[index] = UIMenu(title: subMenu.title, subtitle: subMenu.subtitle, image: subMenu.image, identifier: subMenu.identifier, options: subMenu.options, children: filteredChildren)
|
|
}
|
|
}
|
|
|
|
if editableTextNode.attributedText == nil || editableTextNode.attributedText!.length == 0 || editableTextNode.selectedRange.length == 0 {
|
|
} else {
|
|
var children: [UIAction] = []
|
|
|
|
var hasSpoilers = true
|
|
if self.presentationInterfaceState?.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat {
|
|
hasSpoilers = false
|
|
}
|
|
|
|
if hasSpoilers {
|
|
children.append(UIAction(title: self.strings?.TextFormat_Quote ?? "Quote", image: nil) { [weak self] (action) in
|
|
if let strongSelf = self {
|
|
strongSelf.formatAttributesQuote(strongSelf)
|
|
}
|
|
})
|
|
}
|
|
|
|
if hasSpoilers {
|
|
children.append(UIAction(title: self.strings?.TextFormat_Spoiler ?? "Spoiler", image: nil) { [weak self] (action) in
|
|
if let strongSelf = self {
|
|
strongSelf.formatAttributesSpoiler(strongSelf)
|
|
}
|
|
})
|
|
}
|
|
|
|
children.append(contentsOf: [
|
|
UIAction(title: self.strings?.TextFormat_Bold ?? "Bold", image: nil) { [weak self] (action) in
|
|
if let strongSelf = self {
|
|
strongSelf.formatAttributesBold(strongSelf)
|
|
}
|
|
},
|
|
UIAction(title: self.strings?.TextFormat_Italic ?? "Italic", image: nil) { [weak self] (action) in
|
|
if let strongSelf = self {
|
|
strongSelf.formatAttributesItalic(strongSelf)
|
|
}
|
|
},
|
|
UIAction(title: self.strings?.TextFormat_Monospace ?? "Monospace", image: nil) { [weak self] (action) in
|
|
if let strongSelf = self {
|
|
strongSelf.formatAttributesMonospace(strongSelf)
|
|
}
|
|
},
|
|
UIAction(title: self.strings?.TextFormat_Link ?? "Link", image: nil) { [weak self] (action) in
|
|
if let strongSelf = self {
|
|
strongSelf.formatAttributesLink(strongSelf)
|
|
}
|
|
},
|
|
UIAction(title: self.strings?.TextFormat_Strikethrough ?? "Strikethrough", image: nil) { [weak self] (action) in
|
|
if let strongSelf = self {
|
|
strongSelf.formatAttributesStrikethrough(strongSelf)
|
|
}
|
|
},
|
|
UIAction(title: self.strings?.TextFormat_Underline ?? "Underline", image: nil) { [weak self] (action) in
|
|
if let strongSelf = self {
|
|
strongSelf.formatAttributesUnderline(strongSelf)
|
|
}
|
|
}
|
|
] as [UIAction])
|
|
|
|
children.append(UIAction(title: self.strings?.TextFormat_Code ?? "Code", image: nil) { [weak self] (action) in
|
|
if let strongSelf = self {
|
|
strongSelf.formatAttributesCodeBlock(strongSelf)
|
|
}
|
|
})
|
|
|
|
let formatMenu = UIMenu(title: self.strings?.TextFormat_Format ?? "Format", image: nil, children: children)
|
|
actions.insert(formatMenu, at: 1)
|
|
}
|
|
return UIMenu(children: actions)
|
|
}
|
|
|
|
@available(iOS 16.0, *)
|
|
func editableTextNodeMenu(_ editableTextNode: ASEditableTextNode, forTextRange textRange: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu {
|
|
return chatInputTextNodeMenu(forTextRange: textRange, suggestedActions: suggestedActions)
|
|
}
|
|
|
|
private var currentSpeechHolder: SpeechSynthesizerHolder?
|
|
@objc func _accessibilitySpeak(_ sender: Any) {
|
|
var text = ""
|
|
self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
|
|
text = current.inputText.attributedSubstring(from: NSMakeRange(current.selectionRange.lowerBound, current.selectionRange.count)).string
|
|
return (current, inputMode)
|
|
}
|
|
if let context = self.context {
|
|
if let speechHolder = speakText(context: context, text: text) {
|
|
speechHolder.completion = { [weak self, weak speechHolder] in
|
|
if let strongSelf = self, strongSelf.currentSpeechHolder == speechHolder {
|
|
strongSelf.currentSpeechHolder = nil
|
|
}
|
|
}
|
|
self.currentSpeechHolder = speechHolder
|
|
}
|
|
}
|
|
if #available(iOS 13.0, *) {
|
|
UIMenuController.shared.hideMenu()
|
|
} else {
|
|
UIMenuController.shared.isMenuVisible = false
|
|
UIMenuController.shared.update()
|
|
}
|
|
}
|
|
|
|
@objc func _showTextStyleOptions(_ sender: Any) {
|
|
if let textInputNode = self.textInputNode {
|
|
self.inputMenu.format(view: textInputNode.view, rect: textInputNode.selectionRect.offsetBy(dx: 0.0, dy: -textInputNode.textView.contentOffset.y).insetBy(dx: 0.0, dy: -1.0))
|
|
}
|
|
}
|
|
|
|
@objc func formatAttributesBold(_ sender: Any) {
|
|
self.inputMenu.back()
|
|
self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
|
|
return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.bold, value: nil), inputMode)
|
|
}
|
|
}
|
|
|
|
@objc func formatAttributesItalic(_ sender: Any) {
|
|
self.inputMenu.back()
|
|
self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
|
|
return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.italic, value: nil), inputMode)
|
|
}
|
|
}
|
|
|
|
@objc func formatAttributesMonospace(_ sender: Any) {
|
|
self.inputMenu.back()
|
|
self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
|
|
return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.monospace, value: nil), inputMode)
|
|
}
|
|
}
|
|
|
|
@objc func formatAttributesLink(_ sender: Any) {
|
|
self.inputMenu.back()
|
|
self.interfaceInteraction?.openLinkEditing()
|
|
}
|
|
|
|
@objc func formatAttributesStrikethrough(_ sender: Any) {
|
|
self.inputMenu.back()
|
|
self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
|
|
return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.strikethrough, value: nil), inputMode)
|
|
}
|
|
}
|
|
|
|
@objc func formatAttributesUnderline(_ sender: Any) {
|
|
self.inputMenu.back()
|
|
self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
|
|
return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.underline, value: nil), inputMode)
|
|
}
|
|
}
|
|
|
|
@objc func formatAttributesQuote(_ sender: Any) {
|
|
self.inputMenu.back()
|
|
|
|
self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
|
|
return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .quote, isCollapsed: false)), inputMode)
|
|
}
|
|
}
|
|
|
|
@objc func formatAttributesCodeBlock(_ sender: Any) {
|
|
self.inputMenu.back()
|
|
|
|
self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
|
|
return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .code(language: nil), isCollapsed: false)), inputMode)
|
|
}
|
|
}
|
|
|
|
@objc func formatAttributesSpoiler(_ sender: Any) {
|
|
self.inputMenu.back()
|
|
|
|
var animated = false
|
|
if let attributedText = self.textInputNode?.attributedText {
|
|
attributedText.enumerateAttributes(in: NSMakeRange(0, attributedText.length), options: [], using: { attributes, _, _ in
|
|
if let _ = attributes[ChatTextInputAttributes.spoiler] {
|
|
animated = true
|
|
}
|
|
})
|
|
}
|
|
|
|
self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
|
|
return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.spoiler, value: nil), inputMode)
|
|
}
|
|
|
|
self.updateSpoilersRevealed(animated: animated)
|
|
}
|
|
|
|
func chatInputTextNode(shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
|
|
guard let editableTextNode = self.textInputNode, let context = self.context else {
|
|
return false
|
|
}
|
|
|
|
self.updateActivity()
|
|
|
|
// MARK: Swiftgram
|
|
if self.sendWithReturnKey && text == "\n" {
|
|
self.sendButtonPressed()
|
|
return false
|
|
}
|
|
|
|
var cleanText = text
|
|
let removeSequences: [String] = ["\u{202d}", "\u{202c}"]
|
|
for sequence in removeSequences {
|
|
inner: while true {
|
|
if let range = cleanText.range(of: sequence) {
|
|
cleanText.removeSubrange(range)
|
|
} else {
|
|
break inner
|
|
}
|
|
}
|
|
}
|
|
|
|
if cleanText != text {
|
|
let string = NSMutableAttributedString(attributedString: editableTextNode.attributedText ?? NSAttributedString())
|
|
var textColor: UIColor = .black
|
|
var accentTextColor: UIColor = .blue
|
|
var baseFontSize: CGFloat = 17.0
|
|
if let presentationInterfaceState = self.presentationInterfaceState {
|
|
textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor
|
|
accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor
|
|
baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize)
|
|
}
|
|
let cleanReplacementString = textAttributedStringForStateText(context: context, stateText: NSAttributedString(string: cleanText), fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed, availableEmojis: (self.context?.animatedEmojiStickersValue.keys).flatMap(Set.init) ?? Set(), emojiViewProvider: self.emojiViewProvider, makeCollapsedQuoteAttachment: { text, attributes in
|
|
return ChatInputTextCollapsedQuoteAttachmentImpl(text: text, attributes: attributes)
|
|
})
|
|
string.replaceCharacters(in: range, with: cleanReplacementString)
|
|
self.textInputNode?.attributedText = string
|
|
self.textInputNode?.selectedRange = NSMakeRange(range.lowerBound + cleanReplacementString.length, 0)
|
|
self.updateTextNodeText(animated: true)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
@objc func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
|
|
return self.chatInputTextNode(shouldChangeTextIn: range, replacementText: text)
|
|
}
|
|
|
|
func chatInputTextNodeShouldCopy() -> Bool {
|
|
self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
|
|
storeInputTextInPasteboard(current.inputText.attributedSubstring(from: NSMakeRange(current.selectionRange.lowerBound, current.selectionRange.count)))
|
|
return (current, inputMode)
|
|
}
|
|
return false
|
|
}
|
|
|
|
@objc func editableTextNodeShouldCopy(_ editableTextNode: ASEditableTextNode) -> Bool {
|
|
return self.chatInputTextNodeShouldCopy()
|
|
}
|
|
|
|
public func chatInputTextNodeShouldRespondToAction(action: Selector) -> Bool {
|
|
return true
|
|
}
|
|
|
|
public func chatInputTextNodeTargetForAction(action: Selector) -> ChatInputTextNode.TargetForAction? {
|
|
if let target = self.editableTextNodeTarget(forAction: action) {
|
|
return ChatInputTextNode.TargetForAction(target: target.target)
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func chatInputTextNodeShouldPaste() -> Bool {
|
|
let pasteboard = UIPasteboard.general
|
|
|
|
var attributedString: NSAttributedString?
|
|
if let data = pasteboard.data(forPasteboardType: "private.telegramtext"), let value = chatInputStateStringFromAppSpecificString(data: data) {
|
|
attributedString = value
|
|
} else if let data = pasteboard.data(forPasteboardType: kUTTypeRTF as String) {
|
|
attributedString = chatInputStateStringFromRTF(data, type: NSAttributedString.DocumentType.rtf)
|
|
} else if let data = pasteboard.data(forPasteboardType: "com.apple.flat-rtfd") {
|
|
attributedString = chatInputStateStringFromRTF(data, type: NSAttributedString.DocumentType.rtfd)
|
|
}
|
|
|
|
if let attributedString = attributedString {
|
|
self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
|
|
if let inputText = current.inputText.mutableCopy() as? NSMutableAttributedString {
|
|
inputText.replaceCharacters(in: NSMakeRange(current.selectionRange.lowerBound, current.selectionRange.count), with: attributedString)
|
|
let updatedRange = current.selectionRange.lowerBound + attributedString.length
|
|
return (ChatTextInputState(inputText: inputText, selectionRange: updatedRange ..< updatedRange), inputMode)
|
|
} else {
|
|
return (ChatTextInputState(inputText: attributedString), inputMode)
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
var images: [UIImage] = []
|
|
if let data = pasteboard.data(forPasteboardType: "com.compuserve.gif") {
|
|
self.paste(.gif(data))
|
|
return false
|
|
} else if let data = pasteboard.data(forPasteboardType: "public.mpeg-4") {
|
|
self.paste(.video(data))
|
|
return false
|
|
} else if let data = pasteboard.data(forPasteboardType: "public.heics") {
|
|
self.paste(.animatedSticker(data))
|
|
return false
|
|
} else {
|
|
var isPNG = false
|
|
var isMemoji = false
|
|
for item in pasteboard.items {
|
|
if let image = item["com.apple.png-sticker"] as? UIImage {
|
|
images.append(image)
|
|
isPNG = true
|
|
isMemoji = true
|
|
} else if let image = item[kUTTypePNG as String] as? UIImage {
|
|
images.append(image)
|
|
isPNG = true
|
|
} else if let image = item["com.apple.uikit.image"] as? UIImage {
|
|
images.append(image)
|
|
isPNG = true
|
|
} else if let image = item[kUTTypeJPEG as String] as? UIImage {
|
|
images.append(image)
|
|
} else if let image = item[kUTTypeGIF as String] as? UIImage {
|
|
images.append(image)
|
|
}
|
|
}
|
|
|
|
if isPNG && images.count == 1, let image = images.first {
|
|
let maxSide = max(image.size.width, image.size.height)
|
|
if maxSide.isZero {
|
|
return false
|
|
}
|
|
let aspectRatio = min(image.size.width, image.size.height) / maxSide
|
|
if isMemoji || (imageHasTransparency(image) && aspectRatio > 0.2) {
|
|
self.paste(.sticker(image, isMemoji))
|
|
return true
|
|
}
|
|
}
|
|
|
|
if !images.isEmpty {
|
|
self.paste(.images(images))
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
@objc func editableTextNodeShouldPaste(_ editableTextNode: ASEditableTextNode) -> Bool {
|
|
return self.chatInputTextNodeShouldPaste()
|
|
}
|
|
|
|
@objc func sendButtonPressed() {
|
|
if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState, let editMessage = presentationInterfaceState.interfaceState.editMessage, let inputTextMaxLength = editMessage.inputTextMaxLength {
|
|
let textCount = Int32(textInputNode.textView.text.count)
|
|
let remainingCount = inputTextMaxLength - textCount
|
|
|
|
if remainingCount < 0 {
|
|
textInputNode.layer.addShakeAnimation()
|
|
self.hapticFeedback.error()
|
|
return
|
|
}
|
|
}
|
|
|
|
self.sendMessage()
|
|
}
|
|
|
|
@objc func sendAsAvatarButtonPressed() {
|
|
self.interfaceInteraction?.openSendAsPeer(self.sendAsAvatarReferenceNode, nil)
|
|
}
|
|
|
|
@objc func menuButtonPressed() {
|
|
self.hapticFeedback.impact(.light)
|
|
guard let presentationInterfaceState = self.presentationInterfaceState else {
|
|
return
|
|
}
|
|
|
|
if let sendAsPeers = presentationInterfaceState.sendAsPeers, !sendAsPeers.isEmpty {
|
|
self.interfaceInteraction?.updateShowSendAsPeers { value in
|
|
return !value
|
|
}
|
|
} else if case let .webView(title, url) = presentationInterfaceState.botMenuButton {
|
|
let willShow = !(self.presentationInterfaceState?.showWebView ?? false)
|
|
if willShow || "".isEmpty {
|
|
self.interfaceInteraction?.openWebView(title, url, false, .menu)
|
|
} else {
|
|
self.interfaceInteraction?.updateShowWebView { _ in
|
|
return false
|
|
}
|
|
}
|
|
} else {
|
|
self.interfaceInteraction?.updateShowCommands { value in
|
|
return !value
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc func attachmentButtonPressed() {
|
|
self.displayAttachmentMenu()
|
|
}
|
|
|
|
// MARK: Swiftgram
|
|
@objc func attachmentButtonLongPressed(_ gesture: UILongPressGestureRecognizer) {
|
|
guard gesture.state == .began else { return }
|
|
guard let _ = self.interfaceInteraction?.chatController() as? ChatControllerImpl else {
|
|
return
|
|
}
|
|
// controller.openStickerEditor()
|
|
}
|
|
|
|
@objc func searchLayoutClearButtonPressed() {
|
|
if let interfaceInteraction = self.interfaceInteraction {
|
|
interfaceInteraction.updateTextInputStateAndMode { textInputState, inputMode in
|
|
var mentionQueryRange: NSRange?
|
|
inner: for (_, type, queryRange) in textInputStateContextQueryRangeAndType(textInputState) {
|
|
if type == [.contextRequest] {
|
|
mentionQueryRange = queryRange
|
|
break inner
|
|
}
|
|
}
|
|
if let mentionQueryRange = mentionQueryRange, mentionQueryRange.length > 0 {
|
|
let inputText = NSMutableAttributedString(attributedString: textInputState.inputText)
|
|
|
|
let rangeLower = mentionQueryRange.lowerBound
|
|
let rangeUpper = mentionQueryRange.upperBound
|
|
|
|
inputText.replaceCharacters(in: NSRange(location: rangeLower, length: rangeUpper - rangeLower), with: "")
|
|
|
|
return (ChatTextInputState(inputText: inputText), inputMode)
|
|
} else {
|
|
return (ChatTextInputState(inputText: NSAttributedString(string: "")), inputMode)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc func textInputBackgroundViewTap(_ recognizer: UITapGestureRecognizer) {
|
|
if case .ended = recognizer.state {
|
|
self.ensureFocused()
|
|
}
|
|
}
|
|
|
|
var isFocused: Bool {
|
|
return self.textInputNode?.isFirstResponder() ?? false
|
|
}
|
|
|
|
func ensureUnfocused() {
|
|
self.textInputNode?.resignFirstResponder()
|
|
}
|
|
|
|
func ensureFocused() {
|
|
if self.sendingTextDisabled {
|
|
return
|
|
}
|
|
|
|
if self.textInputNode == nil {
|
|
self.loadTextInputNode()
|
|
}
|
|
|
|
if !self.switching {
|
|
self.textInputNode?.becomeFirstResponder()
|
|
}
|
|
}
|
|
|
|
private var switching = false
|
|
func ensureFocusedOnTap() {
|
|
if self.textInputNode == nil {
|
|
self.loadTextInputNode()
|
|
}
|
|
|
|
if !self.switching {
|
|
self.switching = true
|
|
self.textInputNode?.becomeFirstResponder()
|
|
|
|
self.switchToTextInputIfNeeded?()
|
|
self.switching = false
|
|
}
|
|
}
|
|
|
|
func backwardsDeleteText() {
|
|
guard let textInputNode = self.textInputNode else {
|
|
return
|
|
}
|
|
textInputNode.textView.deleteBackward()
|
|
}
|
|
|
|
@objc func expandButtonPressed() {
|
|
self.toggleExpandMediaInput?()
|
|
/*self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in
|
|
if case let .media(mode, expanded, focused) = state.inputMode {
|
|
if let _ = expanded {
|
|
return (.media(mode: mode, expanded: nil, focused: focused), state.interfaceState.messageActionsState.closedButtonKeyboardMessageId)
|
|
} else {
|
|
return (.media(mode: mode, expanded: .content, focused: focused), state.interfaceState.messageActionsState.closedButtonKeyboardMessageId)
|
|
}
|
|
} else {
|
|
return (state.inputMode, state.interfaceState.messageActionsState.closedButtonKeyboardMessageId)
|
|
}
|
|
})*/
|
|
}
|
|
|
|
@objc func accessoryItemButtonPressed(_ button: UIView) {
|
|
for (item, currentButton) in self.accessoryItemButtons {
|
|
if currentButton === button {
|
|
switch item {
|
|
case let .input(isEnabled, inputMode), let .botInput(isEnabled, inputMode):
|
|
switch inputMode {
|
|
case .keyboard:
|
|
self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in
|
|
return (.text, state.keyboardButtonsMessage?.id)
|
|
})
|
|
case .stickers, .emoji:
|
|
if isEnabled {
|
|
self.interfaceInteraction?.openStickers()
|
|
} else {
|
|
self.interfaceInteraction?.displayRestrictedInfo(.stickers, .tooltip)
|
|
}
|
|
case .bot:
|
|
self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in
|
|
return (.inputButtons(persistent: state.keyboardButtonsMessage?.visibleButtonKeyboardMarkup?.flags.contains(.persistent) ?? false), nil)
|
|
})
|
|
}
|
|
case .commands:
|
|
self.interfaceInteraction?.updateTextInputStateAndMode { _, inputMode in
|
|
return (ChatTextInputState(inputText: NSAttributedString(string: "/")), .text)
|
|
}
|
|
case .silentPost:
|
|
self.interfaceInteraction?.toggleSilentPost()
|
|
case .messageAutoremoveTimeout:
|
|
self.interfaceInteraction?.setupMessageAutoremoveTimeout()
|
|
case .scheduledMessages:
|
|
self.interfaceInteraction?.openScheduledMessages()
|
|
case .gift:
|
|
self.interfaceInteraction?.openPremiumGift()
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
if let audioRecordingCancelIndicator = self.audioRecordingCancelIndicator {
|
|
if let result = audioRecordingCancelIndicator.hitTest(point.offsetBy(dx: -audioRecordingCancelIndicator.frame.minX, dy: -audioRecordingCancelIndicator.frame.minY), with: event) {
|
|
return result
|
|
}
|
|
}
|
|
|
|
if self.bounds.contains(point), let textInputNode = self.textInputNode, let currentEmojiSuggestion = self.currentEmojiSuggestion, let currentEmojiSuggestionView = self.currentEmojiSuggestionView {
|
|
if let result = currentEmojiSuggestionView.hitTest(self.view.convert(point, to: currentEmojiSuggestionView), with: event) {
|
|
return result
|
|
}
|
|
self.dismissedEmojiSuggestionPosition = currentEmojiSuggestion.position
|
|
self.updateInputField(textInputFrame: textInputNode.frame, transition: .immediate)
|
|
}
|
|
|
|
let result = super.hitTest(point, with: event)
|
|
return result
|
|
}
|
|
|
|
func frameForAccessoryButton(_ item: ChatTextInputAccessoryItem) -> CGRect? {
|
|
for (buttonItem, buttonNode) in self.accessoryItemButtons {
|
|
if buttonItem == item {
|
|
return buttonNode.frame
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func frameForAttachmentButton() -> CGRect? {
|
|
if !self.attachmentButton.alpha.isZero {
|
|
return self.attachmentButton.frame.insetBy(dx: 0.0, dy: 6.0).offsetBy(dx: 2.0, dy: 0.0)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func frameForMenuButton() -> CGRect? {
|
|
if !self.menuButton.alpha.isZero {
|
|
return self.menuButton.frame
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func frameForInputActionButton() -> CGRect? {
|
|
if !self.actionButtons.alpha.isZero {
|
|
if self.actionButtons.micButton.alpha.isZero {
|
|
return self.actionButtons.frame.insetBy(dx: 0.0, dy: 6.0).offsetBy(dx: 4.0, dy: 0.0)
|
|
} else {
|
|
return self.actionButtons.frame.insetBy(dx: 0.0, dy: 6.0).offsetBy(dx: 2.0, dy: 0.0)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func frameForStickersButton() -> CGRect? {
|
|
for (item, button) in self.accessoryItemButtons {
|
|
if case let .input(_, inputMode) = item, case .stickers = inputMode {
|
|
return button.frame.insetBy(dx: 0.0, dy: 6.0)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func frameForEmojiButton() -> CGRect? {
|
|
for (item, button) in self.accessoryItemButtons {
|
|
if case let .input(_, inputMode) = item, case .emoji = inputMode {
|
|
return button.frame.insetBy(dx: 0.0, dy: 6.0)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func frameForGiftButton() -> CGRect? {
|
|
for (item, button) in self.accessoryItemButtons {
|
|
if case .gift = item {
|
|
return button.frame.insetBy(dx: 0.0, dy: 6.0)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func makeSnapshotForTransition() -> ChatMessageTransitionNodeImpl.Source.TextInput? {
|
|
guard let backgroundImage = self.transparentTextInputBackgroundImage else {
|
|
return nil
|
|
}
|
|
guard let textInputNode = self.textInputNode else {
|
|
return nil
|
|
}
|
|
|
|
let backgroundView = UIImageView(image: backgroundImage)
|
|
backgroundView.frame = self.textInputBackgroundNode.frame
|
|
|
|
let caretColor = textInputNode.textView.tintColor
|
|
textInputNode.textView.tintColor = .clear
|
|
|
|
guard let contentView = textInputNode.view.snapshotView(afterScreenUpdates: true) else {
|
|
textInputNode.textView.tintColor = caretColor
|
|
return nil
|
|
}
|
|
|
|
textInputNode.textView.tintColor = caretColor
|
|
|
|
contentView.frame = textInputNode.frame
|
|
|
|
return ChatMessageTransitionNodeImpl.Source.TextInput(
|
|
backgroundView: backgroundView,
|
|
contentView: contentView,
|
|
sourceRect: self.view.convert(self.bounds, to: nil),
|
|
scrollOffset: textInputNode.textView.contentOffset.y
|
|
)
|
|
}
|
|
|
|
func makeAttachmentMenuTransition(accessoryPanelNode: ASDisplayNode?) -> AttachmentController.InputPanelTransition {
|
|
return AttachmentController.InputPanelTransition(inputNode: self, accessoryPanelNode: accessoryPanelNode, menuButtonNode: self.menuButton, menuButtonBackgroundNode: self.menuButtonBackgroundNode, menuIconNode: self.menuButtonIconNode, menuTextNode: self.menuButtonTextNode, prepareForDismiss: { self.menuButtonIconNode.enqueueState(.app, animated: false) })
|
|
}
|
|
}
|
|
|
|
private enum MenuIconNodeState: Equatable {
|
|
case menu
|
|
case app
|
|
case close
|
|
}
|
|
|
|
private final class MenuIconNode: ManagedAnimationNode {
|
|
private let duration: Double = 0.33
|
|
fileprivate var iconState: MenuIconNodeState = .menu
|
|
|
|
init() {
|
|
super.init(size: CGSize(width: 30.0, height: 30.0))
|
|
|
|
self.trackTo(item: ManagedAnimationItem(source: .local("anim_menuclose"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01))
|
|
}
|
|
|
|
func enqueueState(_ state: MenuIconNodeState, animated: Bool) {
|
|
guard self.iconState != state else {
|
|
return
|
|
}
|
|
|
|
let previousState = self.iconState
|
|
self.iconState = state
|
|
|
|
switch previousState {
|
|
case .close:
|
|
switch state {
|
|
case .menu:
|
|
if animated {
|
|
self.trackTo(item: ManagedAnimationItem(source: .local("anim_closemenu"), frames: .range(startFrame: 0, endFrame: 20), duration: self.duration))
|
|
} else {
|
|
self.trackTo(item: ManagedAnimationItem(source: .local("anim_menuclose"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01))
|
|
}
|
|
case .app:
|
|
if animated {
|
|
self.trackTo(item: ManagedAnimationItem(source: .local("anim_webview"), frames: .range(startFrame: 0, endFrame: 22), duration: self.duration))
|
|
} else {
|
|
self.trackTo(item: ManagedAnimationItem(source: .local("anim_webview"), frames: .range(startFrame: 22, endFrame: 22), duration: 0.01))
|
|
}
|
|
case .close:
|
|
break
|
|
}
|
|
case .menu:
|
|
switch state {
|
|
case .close:
|
|
if animated {
|
|
self.trackTo(item: ManagedAnimationItem(source: .local("anim_menuclose"), frames: .range(startFrame: 0, endFrame: 20), duration: self.duration))
|
|
} else {
|
|
self.trackTo(item: ManagedAnimationItem(source: .local("anim_menuclose"), frames: .range(startFrame: 20, endFrame: 20), duration: 0.01))
|
|
}
|
|
case .app:
|
|
self.trackTo(item: ManagedAnimationItem(source: .local("anim_webview"), frames: .range(startFrame: 22, endFrame: 22), duration: 0.01))
|
|
case .menu:
|
|
break
|
|
}
|
|
case .app:
|
|
switch state {
|
|
case .close:
|
|
if animated {
|
|
self.trackTo(item: ManagedAnimationItem(source: .local("anim_webview"), frames: .range(startFrame: 22, endFrame: 0), duration: self.duration))
|
|
} else {
|
|
self.trackTo(item: ManagedAnimationItem(source: .local("anim_webview"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01))
|
|
}
|
|
case .menu:
|
|
self.trackTo(item: ManagedAnimationItem(source: .local("anim_menuclose"), frames: .range(startFrame: 0, endFrame: 20), duration: 0.01))
|
|
case .app:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func generateClearImage(color: UIColor) -> UIImage? {
|
|
return generateImage(CGSize(width: 17.0, height: 17.0), rotatedContext: { size, context in
|
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
|
context.setFillColor(color.cgColor)
|
|
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
|
context.setBlendMode(.copy)
|
|
context.setStrokeColor(UIColor.clear.cgColor)
|
|
context.setLineCap(.round)
|
|
context.setLineWidth(1.66)
|
|
context.move(to: CGPoint(x: 6.0, y: 6.0))
|
|
context.addLine(to: CGPoint(x: 11.0, y: 11.0))
|
|
context.strokePath()
|
|
context.move(to: CGPoint(x: size.width - 6.0, y: 6.0))
|
|
context.addLine(to: CGPoint(x: size.width - 11.0, y: 11.0))
|
|
context.strokePath()
|
|
})
|
|
}
|
|
|
|
|
|
private final class BoostSlowModeButton: HighlightTrackingButtonNode {
|
|
let containerNode: ASDisplayNode
|
|
let backgroundNode: ASImageNode
|
|
let textNode: ImmediateAnimatedCountLabelNode
|
|
let iconNode: ASImageNode
|
|
|
|
private var updateTimer: SwiftSignalKit.Timer?
|
|
|
|
var requestUpdate: () -> Void = {}
|
|
|
|
override init(pointerStyle: PointerStyle? = nil) {
|
|
self.containerNode = ASDisplayNode()
|
|
|
|
self.backgroundNode = ASImageNode()
|
|
self.backgroundNode.displaysAsynchronously = false
|
|
self.backgroundNode.clipsToBounds = true
|
|
self.backgroundNode.image = generateGradientImage(size: CGSize(width: 100.0, height: 2.0), scale: 1.0, colors: [UIColor(rgb: 0x9076ff), UIColor(rgb: 0xbc6de8)], locations: [0.0, 1.0], direction: .horizontal)
|
|
|
|
self.iconNode = ASImageNode()
|
|
self.iconNode.displaysAsynchronously = false
|
|
self.iconNode.image = generateClearImage(color: .white)
|
|
|
|
self.textNode = ImmediateAnimatedCountLabelNode()
|
|
self.textNode.alwaysOneDirection = true
|
|
self.textNode.isUserInteractionEnabled = false
|
|
|
|
super.init(pointerStyle: pointerStyle)
|
|
|
|
self.addSubnode(self.containerNode)
|
|
self.containerNode.addSubnode(self.backgroundNode)
|
|
self.containerNode.addSubnode(self.iconNode)
|
|
self.containerNode.addSubnode(self.textNode)
|
|
|
|
self.highligthedChanged = { [weak self] highlighted in
|
|
if let self {
|
|
if highlighted {
|
|
self.containerNode.layer.animateScale(from: 1.0, to: 0.75, duration: 0.4, removeOnCompletion: false)
|
|
} else if let presentationLayer = self.containerNode.layer.presentation() {
|
|
self.containerNode.layer.animateScale(from: CGFloat((presentationLayer.value(forKeyPath: "transform.scale.y") as? NSNumber)?.floatValue ?? 1.0), to: 1.0, duration: 0.25, removeOnCompletion: false)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func update(size: CGSize, interfaceState: ChatPresentationInterfaceState) -> CGSize {
|
|
var text = ""
|
|
if let slowmodeState = interfaceState.slowmodeState {
|
|
let relativeTimestamp: CGFloat
|
|
switch slowmodeState.variant {
|
|
case let .timestamp(validUntilTimestamp):
|
|
let timestamp = CGFloat(Date().timeIntervalSince1970)
|
|
relativeTimestamp = CGFloat(validUntilTimestamp) - timestamp
|
|
case .pendingMessages:
|
|
relativeTimestamp = CGFloat(slowmodeState.timeout)
|
|
}
|
|
|
|
self.updateTimer?.invalidate()
|
|
|
|
if relativeTimestamp >= 0.0 {
|
|
text = stringForDuration(Int32(relativeTimestamp))
|
|
|
|
self.updateTimer = SwiftSignalKit.Timer(timeout: 1.0 / 60.0, repeat: false, completion: { [weak self] in
|
|
self?.requestUpdate()
|
|
}, queue: .mainQueue())
|
|
self.updateTimer?.start()
|
|
}
|
|
} else {
|
|
self.updateTimer?.invalidate()
|
|
self.updateTimer = nil
|
|
}
|
|
|
|
let font = Font.with(size: 15.0, design: .round, weight: .semibold, traits: [.monospacedNumbers])
|
|
let textColor = UIColor.white
|
|
|
|
var segments: [AnimatedCountLabelNode.Segment] = []
|
|
var textCount = 0
|
|
|
|
for char in text {
|
|
if let intValue = Int(String(char)) {
|
|
segments.append(.number(intValue, NSAttributedString(string: String(char), font: font, textColor: textColor)))
|
|
} else {
|
|
segments.append(.text(textCount, NSAttributedString(string: String(char), font: font, textColor: textColor)))
|
|
textCount += 1
|
|
}
|
|
}
|
|
self.textNode.segments = segments
|
|
|
|
let textSize = self.textNode.updateLayout(size: CGSize(width: 200.0, height: 100.0), animated: true)
|
|
let totalSize = CGSize(width: textSize.width > 0.0 ? textSize.width + 38.0 : 33.0, height: 33.0)
|
|
|
|
self.containerNode.bounds = CGRect(origin: .zero, size: totalSize)
|
|
self.containerNode.position = CGPoint(x: totalSize.width / 2.0, y: totalSize.height / 2.0)
|
|
self.backgroundNode.frame = CGRect(origin: .zero, size: totalSize)
|
|
self.backgroundNode.cornerRadius = totalSize.height / 2.0
|
|
self.textNode.frame = CGRect(origin: CGPoint(x: 9.0, y: floorToScreenPixels((totalSize.height - textSize.height) / 2.0)), size: textSize)
|
|
if let icon = self.iconNode.image {
|
|
self.iconNode.frame = CGRect(origin: CGPoint(x: totalSize.width - icon.size.width - 7.0, y: floorToScreenPixels((totalSize.height - icon.size.height) / 2.0)), size: icon.size)
|
|
}
|
|
return totalSize
|
|
}
|
|
}
|