Ilya Laktyushin cf49acb4aa Various fixes
2024-04-29 17:45:32 +04:00

475 lines
20 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import LegacyComponents
import Display
import TelegramCore
import Postbox
import SwiftSignalKit
import AccountContext
import LegacyComponents
import ComponentFlow
import MessageInputPanelComponent
import TelegramPresentationData
import ContextUI
import TooltipUI
import LegacyMessageInputPanelInputView
import UndoUI
public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView {
private let context: AccountContext
private let chatLocation: ChatLocation
private let isScheduledMessages: Bool
private let isFile: Bool
private let present: (ViewController) -> Void
private let presentInGlobalOverlay: (ViewController) -> Void
private let makeEntityInputView: () -> LegacyMessageInputPanelInputView?
private let state = ComponentState()
private let inputPanelExternalState = MessageInputPanelComponent.ExternalState()
private let inputPanel = ComponentView<Empty>()
private var currentTimeout: Int32?
private var currentIsEditing = false
private var currentHeight: CGFloat?
private var currentIsVideo = false
private let hapticFeedback = HapticFeedback()
private var inputView: LegacyMessageInputPanelInputView?
private var isEmojiKeyboardActive = false
private var validLayout: (width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, keyboardHeight: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, metrics: LayoutMetrics)?
public init(
context: AccountContext,
chatLocation: ChatLocation,
isScheduledMessages: Bool,
isFile: Bool,
present: @escaping (ViewController) -> Void,
presentInGlobalOverlay: @escaping (ViewController) -> Void,
makeEntityInputView: @escaping () -> LegacyMessageInputPanelInputView?
) {
self.context = context
self.chatLocation = chatLocation
self.isScheduledMessages = isScheduledMessages
self.isFile = isFile
self.present = present
self.presentInGlobalOverlay = presentInGlobalOverlay
self.makeEntityInputView = makeEntityInputView
super.init()
self.state._updated = { [weak self] transition, _ in
if let self {
self.update(transition: transition.containedViewLayoutTransition)
}
}
}
public var sendPressed: ((NSAttributedString?) -> Void)?
public var focusUpdated: ((Bool) -> Void)?
public var heightUpdated: ((Bool) -> Void)?
public var timerUpdated: ((NSNumber?) -> Void)?
public func updateLayoutSize(_ size: CGSize, keyboardHeight: CGFloat, sideInset: CGFloat, animated: Bool) -> CGFloat {
return self.updateLayout(width: size.width, leftInset: sideInset, rightInset: sideInset, bottomInset: 0.0, keyboardHeight: keyboardHeight, additionalSideInsets: UIEdgeInsets(), maxHeight: size.height, isSecondary: false, transition: animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), isMediaInputExpanded: false)
}
public func caption() -> NSAttributedString {
if let view = self.inputPanel.view as? MessageInputPanelComponent.View, case let .text(caption) = view.getSendMessageInput() {
return caption
} else {
return NSAttributedString()
}
}
private var scheduledMessageInput: MessageInputPanelComponent.SendMessageInput?
public func setCaption(_ caption: NSAttributedString?) {
let sendMessageInput = MessageInputPanelComponent.SendMessageInput.text(caption ?? NSAttributedString())
if let view = self.inputPanel.view as? MessageInputPanelComponent.View {
view.setSendMessageInput(value: sendMessageInput, updateState: true)
} else {
self.scheduledMessageInput = sendMessageInput
}
}
public func animate(_ view: UIView, frame: CGRect) {
let transition = Transition.spring(duration: 0.4)
transition.setFrame(view: view, frame: frame)
}
public func setTimeout(_ timeout: Int32, isVideo: Bool) {
self.dismissTimeoutTooltip()
var timeout: Int32? = timeout
if timeout == 0 {
timeout = nil
}
self.currentTimeout = timeout
self.currentIsVideo = isVideo
}
public func activateInput() {
if let view = self.inputPanel.view as? MessageInputPanelComponent.View {
view.activateInput()
}
}
public func dismissInput() -> Bool {
if let view = self.inputPanel.view as? MessageInputPanelComponent.View {
if view.canDeactivateInput() {
self.isEmojiKeyboardActive = false
self.inputView = nil
view.deactivateInput(force: true)
return true
} else {
view.animateError()
return false
}
} else {
return true
}
}
public func onAnimateOut() {
self.dismissTimeoutTooltip()
}
public func baseHeight() -> CGFloat {
return 52.0
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func update(transition: ContainedViewLayoutTransition) {
if let (width, leftInset, rightInset, bottomInset, keyboardHeight, additionalSideInsets, maxHeight, isSecondary, metrics) = self.validLayout {
let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, keyboardHeight: keyboardHeight, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: transition, metrics: metrics, isMediaInputExpanded: false)
}
}
public func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, keyboardHeight: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat {
let previousLayout = self.validLayout
self.validLayout = (width, leftInset, rightInset, bottomInset, keyboardHeight, additionalSideInsets, maxHeight, isSecondary, metrics)
var transition = transition
if keyboardHeight.isZero, let previousKeyboardHeight = previousLayout?.keyboardHeight, previousKeyboardHeight > 0.0, !transition.isAnimated {
transition = .animated(duration: 0.4, curve: .spring)
}
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let theme = defaultDarkColorPresentationTheme
var timeoutValue: String?
var timeoutSelected = false
if self.isFile {
timeoutValue = nil
} else {
if let timeout = self.currentTimeout {
if timeout == viewOnceTimeout {
timeoutValue = "1"
} else {
timeoutValue = "\(timeout)"
}
timeoutSelected = true
} else {
timeoutValue = "1"
}
}
var maxInputPanelHeight = maxHeight
if keyboardHeight.isZero {
maxInputPanelHeight = 60.0
} else {
maxInputPanelHeight = maxHeight - keyboardHeight - 100.0
}
var resetInputContents: MessageInputPanelComponent.SendMessageInput?
if let scheduledMessageInput = self.scheduledMessageInput {
resetInputContents = scheduledMessageInput
self.scheduledMessageInput = nil
}
var hasTimer = self.chatLocation.peerId?.namespace == Namespaces.Peer.CloudUser && !self.isScheduledMessages
if self.chatLocation.peerId?.isRepliesOrSavedMessages(accountPeerId: self.context.account.peerId) == true {
hasTimer = false
}
self.inputPanel.parentState = self.state
let inputPanelSize = self.inputPanel.update(
transition: Transition(transition),
component: AnyComponent(
MessageInputPanelComponent(
externalState: self.inputPanelExternalState,
context: self.context,
theme: theme,
strings: presentationData.strings,
style: .media,
placeholder: .plain(presentationData.strings.MediaPicker_AddCaption),
maxLength: Int(self.context.userLimits.maxCaptionLength),
queryTypes: [.mention],
alwaysDarkWhenHasText: false,
resetInputContents: resetInputContents,
nextInputMode: { [weak self] _ in
if self?.isEmojiKeyboardActive == true {
return .text
} else {
return .emoji
}
},
areVoiceMessagesAvailable: false,
presentController: self.present,
presentInGlobalOverlay: self.presentInGlobalOverlay,
sendMessageAction: { [weak self] in
if let self {
self.sendPressed?(self.caption())
let _ = self.dismissInput()
}
},
sendMessageOptionsAction: nil,
sendStickerAction: { _ in },
setMediaRecordingActive: nil,
lockMediaRecording: nil,
stopAndPreviewMediaRecording: nil,
discardMediaRecordingPreview: nil,
attachmentAction: nil,
myReaction: nil,
likeAction: nil,
likeOptionsAction: nil,
inputModeAction: { [weak self] in
if let self {
self.toggleInputMode()
}
},
timeoutAction: hasTimer ? { [weak self] sourceView, gesture in
if let self {
self.presentTimeoutSetup(sourceView: sourceView, gesture: gesture)
}
} : nil,
forwardAction: nil,
moreAction: nil,
presentVoiceMessagesUnavailableTooltip: nil,
presentTextLengthLimitTooltip: nil,
presentTextFormattingTooltip: nil,
paste: { _ in },
audioRecorder: nil,
videoRecordingStatus: nil,
isRecordingLocked: false,
hasRecordedVideo: false,
recordedAudioPreview: nil,
hasRecordedVideoPreview: false,
wasRecordingDismissed: false,
timeoutValue: timeoutValue,
timeoutSelected: timeoutSelected,
displayGradient: false,
bottomInset: 0.0,
isFormattingLocked: false,
hideKeyboard: false,
customInputView: self.inputView,
forceIsEditing: false,
disabledPlaceholder: nil,
header: nil,
isChannel: false,
storyItem: nil,
chatLocation: self.chatLocation
)
),
environment: {},
containerSize: CGSize(width: width, height: maxInputPanelHeight)
)
if let view = self.inputPanel.view {
if view.superview == nil {
self.view.addSubview(view)
}
let inputPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: -8.0), size: inputPanelSize)
transition.updateFrame(view: view, frame: inputPanelFrame)
}
if self.currentIsEditing != self.inputPanelExternalState.isEditing {
self.currentIsEditing = self.inputPanelExternalState.isEditing
self.focusUpdated?(self.currentIsEditing)
}
if self.currentHeight != inputPanelSize.height {
self.currentHeight = inputPanelSize.height
self.heightUpdated?(transition.isAnimated)
}
return inputPanelSize.height - 8.0
}
private func toggleInputMode() {
self.isEmojiKeyboardActive = !self.isEmojiKeyboardActive
if self.isEmojiKeyboardActive {
let inputView = self.makeEntityInputView()
inputView?.insertText = { [weak self] text in
if let self {
self.inputPanelExternalState.insertText(text)
}
}
inputView?.deleteBackwards = { [weak self] in
if let self {
self.inputPanelExternalState.deleteBackward()
}
}
inputView?.switchToKeyboard = { [weak self] in
if let self {
self.isEmojiKeyboardActive = false
self.inputView = nil
self.update(transition: .immediate)
}
}
inputView?.presentController = { [weak self] c in
if let self {
if !(c is UndoOverlayController) {
self.isEmojiKeyboardActive = false
if let view = self.inputPanel.view as? MessageInputPanelComponent.View {
view.deactivateInput(force: true)
}
}
self.present(c)
}
}
self.inputView = inputView
self.update(transition: .immediate)
} else {
self.inputView = nil
self.update(transition: .immediate)
}
}
private func presentTimeoutSetup(sourceView: UIView, gesture: ContextGesture?) {
self.hapticFeedback.impact(.light)
var items: [ContextMenuItem] = []
let updateTimeout: (Int32?) -> Void = { [weak self] timeout in
if let self {
let previousTimeout = self.currentTimeout
self.currentTimeout = timeout
self.timerUpdated?(timeout as? NSNumber)
self.update(transition: .immediate)
if previousTimeout != timeout {
self.presentTimeoutTooltip(sourceView: sourceView, timeout: timeout)
}
}
}
let currentValue = self.currentTimeout
let presentationData = self.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme)
let title = presentationData.strings.MediaPicker_Timer_Description
let emptyAction: ((ContextMenuActionItem.Action) -> Void)? = nil
items.append(.action(ContextMenuActionItem(text: title, textLayout: .multiline, textFont: .small, icon: { _ in return nil }, action: emptyAction)))
items.append(.action(ContextMenuActionItem(text: presentationData.strings.MediaPicker_Timer_ViewOnce, icon: { theme in
return currentValue == viewOnceTimeout ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
}, action: { _, a in
a(.default)
updateTimeout(viewOnceTimeout)
})))
let values: [Int32] = [3, 10, 30]
for value in values {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.MediaPicker_Timer_Seconds(value), icon: { theme in
return currentValue == value ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
}, action: { _, a in
a(.default)
updateTimeout(value)
})))
}
items.append(.action(ContextMenuActionItem(text: presentationData.strings.MediaPicker_Timer_DoNotDelete, icon: { theme in
return currentValue == nil ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
}, action: { _, a in
a(.default)
updateTimeout(nil)
})))
let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
self.present(contextController)
}
private weak var tooltipController: TooltipScreen?
private func dismissTimeoutTooltip() {
if let tooltipController = self.tooltipController {
self.tooltipController = nil
tooltipController.dismiss()
}
}
private func presentTimeoutTooltip(sourceView: UIView, timeout: Int32?) {
guard let superview = self.view.superview?.superview else {
return
}
self.dismissTimeoutTooltip()
let parentFrame = superview.convert(superview.bounds, to: nil)
let absoluteFrame = sourceView.convert(sourceView.bounds, to: nil).offsetBy(dx: -parentFrame.minX, dy: 0.0)
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY - 2.0), size: CGSize())
let isVideo = self.currentIsVideo
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let text: String
let iconName: String
if timeout == viewOnceTimeout {
text = isVideo ? presentationData.strings.MediaPicker_Timer_Video_ViewOnceTooltip : presentationData.strings.MediaPicker_Timer_Photo_ViewOnceTooltip
iconName = "anim_autoremove_on"
} else if let timeout {
text = isVideo ? presentationData.strings.MediaPicker_Timer_Video_TimerTooltip("\(timeout)").string : presentationData.strings.MediaPicker_Timer_Photo_TimerTooltip("\(timeout)").string
iconName = "anim_autoremove_on"
} else {
text = isVideo ? presentationData.strings.MediaPicker_Timer_Video_KeepTooltip : presentationData.strings.MediaPicker_Timer_Photo_KeepTooltip
iconName = "anim_autoremove_off"
}
let tooltipController = TooltipScreen(
account: self.context.account,
sharedContext: self.context.sharedContext,
text: .plain(text: text),
balancedTextLayout: false,
style: .customBlur(UIColor(rgb: 0x18181a), 0.0),
arrowStyle: .small,
icon: .animation(name: iconName, delay: 0.1, tintColor: nil),
location: .point(location, .bottom),
displayDuration: .default,
inset: 8.0,
shouldDismissOnTouch: { _, _ in
return .ignore
}
)
self.tooltipController = tooltipController
self.present(tooltipController)
}
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event)
if let view = self.inputPanel.view, let panelResult = view.hitTest(self.view.convert(point, to: view), with: event) {
return panelResult
}
return result
}
}
private final class HeaderContextReferenceContentSource: ContextReferenceContentSource {
private let sourceView: UIView
var keepInPlace: Bool {
return true
}
init(sourceView: UIView) {
self.sourceView = sourceView
}
func transitionInfo() -> ContextControllerReferenceViewInfo? {
return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds, actionsPosition: .top)
}
}