mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
475 lines
20 KiB
Swift
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 = ComponentTransition.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: ComponentTransition(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)
|
|
}
|
|
}
|