1186 lines
52 KiB
Swift
1186 lines
52 KiB
Swift
//
|
|
// Updated_ChatInputView.swift
|
|
// Telegram
|
|
//
|
|
// Created by Mike Renoir on 11.10.2023.
|
|
// Copyright © 2023 Telegram. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import TGUIKit
|
|
import TGUIKit
|
|
import SwiftSignalKit
|
|
import TelegramCore
|
|
import InputView
|
|
import Postbox
|
|
import ColorPalette
|
|
import TelegramMedia
|
|
|
|
protocol ChatInputDelegate : AnyObject {
|
|
func inputChanged(height:CGFloat, animated:Bool);
|
|
}
|
|
|
|
final class InputMessageEffectView : Control {
|
|
|
|
|
|
class RadialGradientView: View {
|
|
|
|
override func draw(_ layer: CALayer, in context: CGContext) {
|
|
super.draw(layer, in: context)
|
|
|
|
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
|
|
|
let colors = [theme.colors.background.cgColor, theme.colors.background.withAlphaComponent(0).cgColor] as CFArray
|
|
|
|
guard let gradient = CGGradient(colorsSpace: colorSpace, colors: colors, locations: [0.0, 1.0]) else { return }
|
|
|
|
let center = CGPoint(x: bounds.midX, y: bounds.midY)
|
|
let radius = min(bounds.width, bounds.height) / 2
|
|
|
|
context.drawRadialGradient(gradient, startCenter: center, startRadius: 0, endCenter: center, endRadius: radius, options: .drawsBeforeStartLocation)
|
|
}
|
|
}
|
|
|
|
let view: InlineStickerView
|
|
private let gradient: RadialGradientView = RadialGradientView(frame: NSMakeRect(0, 0, 20, 20))
|
|
init(account: Account, file: TelegramMediaFile, size: NSSize) {
|
|
self.view = .init(account: account, file: file, size: size, playPolicy: .onceEnd)
|
|
super.init(frame: NSMakeSize(size.width, 20).bounds)
|
|
self.layer?.masksToBounds = false
|
|
addSubview(gradient)
|
|
addSubview(view)
|
|
scaleOnClick = true
|
|
}
|
|
|
|
required init(frame frameRect: NSRect) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func layout() {
|
|
super.layout()
|
|
gradient.center()
|
|
view.center()
|
|
}
|
|
}
|
|
|
|
class ChatInputView: View, Notifable {
|
|
|
|
private var standart:CGFloat = 50.0
|
|
private var bottomHeight:CGFloat = 0
|
|
static let bottomPadding:CGFloat = 10
|
|
|
|
|
|
private let sendActivityDisposable = MetaDisposable()
|
|
public let ready = Promise<Bool>()
|
|
weak var delegate:ChatInputDelegate?
|
|
var chatInteraction:ChatInteraction
|
|
let accessory:ChatInputAccessory
|
|
private let _ts:View
|
|
|
|
private let contentView:View
|
|
private let bottomView:NSScrollView = NSScrollView()
|
|
|
|
|
|
private var messageActionsPanelView:MessageActionsPanelView?
|
|
private var recordingPanelView:ChatInputRecordingView?
|
|
private var blockedActionView:TextButton?
|
|
private var blockText: View?
|
|
private var additionBlockedActionView: ImageButton?
|
|
private var chatDiscussionView: ChannelDiscussionInputView?
|
|
private var restrictedView:RestrictionWrappedView?
|
|
private var disallowText:Control?
|
|
private var messageEffect: InputMessageEffectView?
|
|
|
|
private let actionsView:ChatInputActionsView
|
|
|
|
|
|
|
|
let textView:UITextView!
|
|
let attachView:ChatInputAttachView!
|
|
|
|
private let rtfAttachmentsDisposable = MetaDisposable()
|
|
private let slowModeUntilDisposable = MetaDisposable()
|
|
private let accessoryDisposable:MetaDisposable = MetaDisposable()
|
|
|
|
|
|
private var replyMarkupModel:ReplyMarkupNode?
|
|
override var isFlipped: Bool {
|
|
return false
|
|
}
|
|
|
|
static let maxBottomHeight = ReplyMarkupNode.rowHeight * 3 + ReplyMarkupNode.buttonHeight / 2
|
|
|
|
|
|
|
|
private var botMenuView: ChatInputMenuView?
|
|
private var sendAsView: ChatInputSendAsView?
|
|
|
|
private let textInteractions: TextView_Interactions = .init()
|
|
|
|
init(frame frameRect: NSRect, chatInteraction:ChatInteraction) {
|
|
self.chatInteraction = chatInteraction
|
|
self.accessory = ChatInputAccessory(chatInteraction:chatInteraction)
|
|
self.contentView = View(frame: NSMakeRect(0, 0, NSWidth(frameRect), NSHeight(frameRect)))
|
|
self._ts = View(frame: NSMakeRect(0, 0, NSWidth(frameRect), .borderSize))
|
|
self.attachView = ChatInputAttachView(frame: NSMakeRect(0, 0, chatInteraction.mode.customChatLink != nil ? 20 : 60, contentView.frame.height), chatInteraction:chatInteraction)
|
|
self.attachView.isHidden = chatInteraction.mode.customChatLink != nil
|
|
self.actionsView = ChatInputActionsView(frame: NSMakeRect(contentView.frame.width - 100, 0, 100, contentView.frame.height), chatInteraction:chatInteraction);
|
|
self.textView = UITextView(frame: NSMakeRect(attachView.isHidden ? 0 : attachView.frame.width, 0, contentView.frame.width - actionsView.frame.width, contentView.frame.height), interactions: self.textInteractions)
|
|
|
|
super.init(frame: frameRect)
|
|
|
|
self.textView.context = chatInteraction.context
|
|
|
|
self.animates = true
|
|
|
|
_ts.backgroundColor = .border;
|
|
|
|
contentView.flip = false
|
|
|
|
contentView.addSubview(attachView)
|
|
|
|
bottomView.scrollerStyle = .overlay
|
|
|
|
|
|
contentView.addSubview(textView)
|
|
contentView.addSubview(actionsView)
|
|
|
|
self.addSubview(accessory)
|
|
self.addSubview(contentView)
|
|
self.addSubview(bottomView)
|
|
self.addSubview(_ts)
|
|
|
|
bottomView.documentView = View()
|
|
|
|
self.background = theme.colors.background
|
|
updateLocalizationAndTheme(theme: theme)
|
|
|
|
|
|
textInteractions.inputDidUpdate = { [weak self] state in
|
|
guard let `self` = self else {
|
|
return
|
|
}
|
|
self.set(state)
|
|
self.inputDidUpdateLayout(animated: true)
|
|
}
|
|
|
|
textInteractions.processEnter = { [weak self] event in
|
|
return self?.textViewEnterPressed(event) ?? true
|
|
}
|
|
textInteractions.processPaste = { [weak self] pasteboard in
|
|
return self?.processPaste(pasteboard) ?? false
|
|
}
|
|
textInteractions.processAttriburedCopy = { attributedString in
|
|
return globalLinkExecutor.copyAttributedString(attributedString)
|
|
}
|
|
}
|
|
|
|
func set(_ state: Updated_ChatTextInputState) {
|
|
self.chatInteraction.update({
|
|
$0.withUpdatedEffectiveInputState(state.textInputState())
|
|
})
|
|
}
|
|
|
|
private var markNextTextChangeToFalseActivity: Bool = false
|
|
|
|
public func textViewEnterPressed(_ event: NSEvent) -> Bool {
|
|
|
|
let interaction = self.chatInteraction
|
|
let context = interaction.context
|
|
|
|
if FastSettings.checkSendingAbility(for: event) {
|
|
let text = textView.string().trimmed
|
|
if text.length > interaction.maxInputCharacters {
|
|
if context.isPremium || context.premiumIsBlocked {
|
|
alert(for: context.window, info: strings().chatInputErrorMessageTooLongCountable(text.length - Int(interaction.maxInputCharacters)))
|
|
} else {
|
|
verifyAlert_button(for: context.window, information: strings().chatInputErrorMessageTooLongCountable(text.length - Int(interaction.maxInputCharacters)), ok: strings().alertOK, cancel: "", option: strings().premiumGetPremiumDouble, successHandler: { result in
|
|
switch result {
|
|
case .thrid:
|
|
showPremiumLimit(context: context, type: .caption(text.length))
|
|
default:
|
|
break
|
|
}
|
|
|
|
})
|
|
}
|
|
return true
|
|
}
|
|
if !text.isEmpty || !interaction.presentation.interfaceState.forwardMessageIds.isEmpty || interaction.presentation.state == .editing {
|
|
interaction.sendMessage(false, nil, interaction.presentation.messageEffect)
|
|
if interaction.peerIsAccountPeer {
|
|
interaction.context.account.updateLocalInputActivity(peerId: interaction.activitySpace, activity: .typingText, isPresent: false)
|
|
}
|
|
markNextTextChangeToFalseActivity = true
|
|
} else if text.isEmpty {
|
|
interaction.scrollToLatest(true)
|
|
}
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func height(for width: CGFloat) -> CGFloat {
|
|
let contentHeight:CGFloat = contentHeight(for: width)
|
|
var sumHeight:CGFloat = contentHeight + (accessory.isVisibility() ? accessory.size.height + 5 : 0)
|
|
if let markup = replyMarkupModel {
|
|
bottomHeight = min(
|
|
ChatInputView.maxBottomHeight,
|
|
markup.size.height + ChatInputView.bottomPadding
|
|
)
|
|
} else {
|
|
bottomHeight = 0
|
|
}
|
|
if chatInteraction.presentation.isKeyboardShown {
|
|
sumHeight += bottomHeight
|
|
}
|
|
return sumHeight
|
|
}
|
|
|
|
|
|
|
|
|
|
public override var responder:NSResponder? {
|
|
return textView.inputView
|
|
}
|
|
|
|
func updateInterface(with interaction:ChatInteraction) -> Void {
|
|
self.chatInteraction = interaction
|
|
actionsView.prepare(with: chatInteraction)
|
|
needUpdateChatState(with: chatState, false)
|
|
needUpdateReplyMarkup(with: interaction.presentation, false)
|
|
|
|
updateMessageEffect(interaction.presentation.interfaceState.messageEffect, animated: false)
|
|
|
|
updateAdditions(interaction.presentation, false)
|
|
|
|
chatInteraction.add(observer: self)
|
|
ready.set(accessory.nodeReady.get() |> map {_ in return true} |> take(1) )
|
|
|
|
|
|
|
|
updateLayout(size: frame.size, transition: .immediate)
|
|
|
|
self.updateInput(interaction.presentation, prevState: ChatPresentationInterfaceState(chatLocation: interaction.chatLocation, chatMode: interaction.mode), animated: false, initial: true)
|
|
|
|
}
|
|
|
|
private var textPlaceholder: String {
|
|
|
|
|
|
if case let .thread(_, mode) = chatInteraction.mode {
|
|
switch mode {
|
|
case .comments:
|
|
return strings().messagesPlaceholderComment
|
|
case .replies:
|
|
return strings().messagesPlaceholderReply
|
|
case .topic:
|
|
return strings().messagesPlaceholderSentMessage
|
|
case .savedMessages, .saved:
|
|
break
|
|
}
|
|
}
|
|
if case let .customChatContents(contents) = chatInteraction.mode {
|
|
switch contents.kind {
|
|
case .awayMessageInput:
|
|
return strings().chatInputBusinessAway
|
|
case .greetingMessageInput:
|
|
return strings().chatInputBusinessGreeting
|
|
case .quickReplyMessageInput:
|
|
return strings().chatInputBusinessQuickReply
|
|
case .searchHashtag:
|
|
return ""
|
|
}
|
|
}
|
|
if case .customLink = chatInteraction.mode {
|
|
return strings().chatInputBusinessLink
|
|
}
|
|
|
|
guard let peer = chatInteraction.presentation.peer else {
|
|
return strings().messagesPlaceholderSentMessage
|
|
}
|
|
|
|
if let _ = permissionText(from: peer, for: .banSendText, cachedData: chatInteraction.presentation.cachedData), chatInteraction.presentation.state == .normal {
|
|
return strings().channelPersmissionMessageBlock
|
|
}
|
|
|
|
|
|
|
|
if let cachedData = chatInteraction.presentation.cachedData as? CachedChannelData {
|
|
let viewForumAsMessages = cachedData.viewForumAsMessages.knownValue
|
|
if peer.isForum, viewForumAsMessages == true {
|
|
if let replyMessage = chatInteraction.presentation.interfaceState.replyMessage {
|
|
if let threadInfo = replyMessage.associatedThreadInfo {
|
|
return strings().messagePlaceholderReplyToTopic(threadInfo.title)
|
|
}
|
|
} else {
|
|
return strings().messagePlaceholderMessageInGeneral
|
|
}
|
|
}
|
|
}
|
|
|
|
if chatInteraction.presentation.interfaceState.editState != nil {
|
|
return strings().messagePlaceholderEdit
|
|
}
|
|
if chatInteraction.mode == .scheduled {
|
|
return strings().messagesPlaceholderScheduled
|
|
}
|
|
if let replyMarkup = chatInteraction.presentation.keyboardButtonsMessage?.replyMarkup {
|
|
if let placeholder = replyMarkup.placeholder {
|
|
return placeholder
|
|
}
|
|
}
|
|
if let peer = chatInteraction.presentation.peer {
|
|
if let peer = peer as? TelegramChannel {
|
|
if peer.hasPermission(.canBeAnonymous) {
|
|
return strings().messagesPlaceholderAnonymous
|
|
}
|
|
}
|
|
if peer.isChannel {
|
|
return FastSettings.isChannelMessagesMuted(peer.id) ? strings().messagesPlaceholderSilentBroadcast : strings().messagesPlaceholderBroadcast
|
|
}
|
|
}
|
|
if !chatInteraction.peerIsAccountPeer {
|
|
return strings().messagesPlaceholderAnonymous
|
|
}
|
|
return strings().messagesPlaceholderSentMessage
|
|
}
|
|
|
|
override func updateLocalizationAndTheme(theme: PresentationTheme) {
|
|
super.updateLocalizationAndTheme(theme: theme)
|
|
let theme = (theme as! TelegramPresentationTheme)
|
|
_ts.backgroundColor = theme.colors.border
|
|
backgroundColor = theme.colors.background
|
|
contentView.backgroundColor = theme.colors.background
|
|
actionsView.backgroundColor = theme.colors.background
|
|
chatDiscussionView?.updateLocalizationAndTheme(theme: theme)
|
|
bottomView.backgroundColor = theme.colors.background
|
|
bottomView.documentView?.background = theme.colors.background
|
|
self.needUpdateReplyMarkup(with: chatInteraction.presentation, false)
|
|
|
|
accessory.update(with: chatInteraction.presentation, context: chatInteraction.context, animated: false)
|
|
accessory.backgroundColor = theme.colors.background
|
|
accessory.container.backgroundColor = theme.colors.background
|
|
|
|
blockText?.backgroundColor = theme.colors.background
|
|
|
|
let myPeerColor = chatInteraction.context.myPeer?.nameColor
|
|
let colors: PeerNameColors.Colors
|
|
if let myPeerColor = myPeerColor {
|
|
colors = chatInteraction.context.peerNameColors.get(myPeerColor)
|
|
} else {
|
|
colors = .init(main: theme.colors.accent)
|
|
}
|
|
textView.inputTheme = theme.inputTheme.withUpdatedQuote(colors)
|
|
}
|
|
|
|
func notify(with value: Any, oldValue:Any, animated:Bool) {
|
|
|
|
let transition: ContainedViewLayoutTransition
|
|
if animated {
|
|
transition = .animated(duration: 0.2, curve: .easeOut)
|
|
} else {
|
|
transition = .immediate
|
|
}
|
|
|
|
|
|
updateLayout(size: frame.size, transition: transition)
|
|
|
|
self.actionsView.notify(with: value, oldValue: oldValue, animated: animated)
|
|
|
|
if let value = value as? ChatPresentationInterfaceState, let oldValue = oldValue as? ChatPresentationInterfaceState {
|
|
|
|
if value.effectiveInput != oldValue.effectiveInput || oldValue.state != value.state {
|
|
updateInput(value, prevState: oldValue, animated: animated)
|
|
}
|
|
updateAttachments(value,animated)
|
|
|
|
var urlPreviewChanged:Bool
|
|
if value.urlPreview?.0 != oldValue.urlPreview?.0 {
|
|
urlPreviewChanged = true
|
|
} else if let valuePreview = value.urlPreview?.1, let oldValuePreview = oldValue.urlPreview?.1 {
|
|
urlPreviewChanged = !valuePreview.isEqual(to: oldValuePreview)
|
|
} else if (value.urlPreview?.1 == nil) != (oldValue.urlPreview?.1 == nil) {
|
|
urlPreviewChanged = true
|
|
} else {
|
|
urlPreviewChanged = false
|
|
}
|
|
|
|
urlPreviewChanged = urlPreviewChanged || value.interfaceState.composeDisableUrlPreview != oldValue.interfaceState.composeDisableUrlPreview
|
|
|
|
|
|
if !isEqualMessageList(lhs: value.interfaceState.forwardMessages, rhs: oldValue.interfaceState.forwardMessages) || value.interfaceState.forwardMessageIds != oldValue.interfaceState.forwardMessageIds || value.interfaceState.replyMessageId != oldValue.interfaceState.replyMessageId || value.interfaceState.editState != oldValue.interfaceState.editState || urlPreviewChanged || value.interfaceState.hideSendersName != oldValue.interfaceState.hideSendersName || value.interfaceState.hideCaptions != oldValue.interfaceState.hideCaptions || value.interfaceState.linkBelowMessage != oldValue.interfaceState.linkBelowMessage || value.interfaceState.largeMedia != oldValue.interfaceState.largeMedia {
|
|
updateAdditions(value,animated)
|
|
}
|
|
|
|
if value.state != oldValue.state {
|
|
needUpdateChatState(with:value.state, animated)
|
|
}
|
|
|
|
var updateReplyMarkup = false
|
|
|
|
if let lhsMessage = value.keyboardButtonsMessage, let rhsMessage = oldValue.keyboardButtonsMessage {
|
|
if lhsMessage.id != rhsMessage.id || lhsMessage.stableVersion != rhsMessage.stableVersion {
|
|
updateReplyMarkup = true
|
|
}
|
|
} else if (value.keyboardButtonsMessage == nil) != (oldValue.keyboardButtonsMessage == nil) {
|
|
updateReplyMarkup = true
|
|
}
|
|
|
|
if !updateReplyMarkup {
|
|
updateReplyMarkup = value.isKeyboardShown != oldValue.isKeyboardShown
|
|
}
|
|
|
|
if updateReplyMarkup {
|
|
needUpdateReplyMarkup(with: value, animated)
|
|
inputDidUpdateLayout(animated: animated)
|
|
}
|
|
|
|
if value.interfaceState.messageEffect != oldValue.interfaceState.messageEffect {
|
|
self.updateMessageEffect(value.interfaceState.messageEffect, animated: animated)
|
|
}
|
|
|
|
self.updateLayout(size: self.frame.size, transition: animated ? .animated(duration: 0.2, curve: .easeOut) : .immediate)
|
|
|
|
}
|
|
}
|
|
|
|
private func updateMessageEffect(_ messageEffect: ChatInterfaceMessageEffect?, animated: Bool) {
|
|
let context = self.chatInteraction.context
|
|
if let messageEffect {
|
|
if self.messageEffect?.view.animateLayer.fileId != messageEffect.effect.effectSticker.fileId.id {
|
|
if let view = self.messageEffect {
|
|
performSubviewRemoval(view, animated: animated)
|
|
}
|
|
let current = InputMessageEffectView(account: chatInteraction.context.account, file: messageEffect.effect.effectSticker, size: NSMakeSize(16, 16))
|
|
current.userInteractionEnabled = true
|
|
current.setFrameOrigin(NSMakePoint(frame.width - current.frame.width - 10, 5))
|
|
|
|
|
|
let showMenu:(Control)->Void = { [weak self] control in
|
|
if let event = NSApp.currentEvent, let chatInteraction = self?.chatInteraction {
|
|
let sendMenu = chatInteraction.sendMessageMenu(true) |> deliverOnMainQueue
|
|
_ = sendMenu.startStandalone(next: { menu in
|
|
if let menu {
|
|
AppMenu.show(menu: menu, event: event, for: control)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
current.set(handler: { control in
|
|
showMenu(control)
|
|
}, for: .Down)
|
|
|
|
current.set(handler: { control in
|
|
showMenu(control)
|
|
}, for: .LongMouseDown)
|
|
|
|
|
|
self.messageEffect = current
|
|
addSubview(current, positioned: .below, relativeTo: _ts)
|
|
|
|
|
|
if let fromRect = messageEffect.fromRect {
|
|
let layer = InlineStickerItemLayer(account: context.account, inlinePacksContext: context.inlinePacksContext, emoji: .init(fileId: messageEffect.effect.effectSticker.fileId.id, file: messageEffect.effect.effectSticker, emoji: ""), size: current.frame.size)
|
|
|
|
let toRect = current.convert(current.frame.size.bounds, to: nil)
|
|
|
|
let from = fromRect.origin.offsetBy(dx: fromRect.width / 2, dy: fromRect.height / 2)
|
|
let to = toRect.origin.offsetBy(dx: toRect.width / 2, dy: toRect.height / 2)
|
|
|
|
let completed: (Bool)->Void = { [weak self] _ in
|
|
DispatchQueue.main.async {
|
|
if let container = self?.messageEffect {
|
|
NSHapticFeedbackManager.defaultPerformer.perform(.levelChange, performanceTime: .default)
|
|
container.isHidden = false
|
|
}
|
|
}
|
|
}
|
|
current.isHidden = true
|
|
parabollicReactionAnimation(layer, fromPoint: from, toPoint: to, window: context.window, completion: completed)
|
|
|
|
DispatchQueue.main.async { [weak self] in
|
|
self?.chatInteraction.update {
|
|
$0.updatedInterfaceState {
|
|
$0.withRemovedEffectRect()
|
|
}
|
|
}
|
|
}
|
|
|
|
let messageEffect = messageEffect.effect
|
|
let file = messageEffect.effectSticker
|
|
let signal: Signal<(LottieAnimation, String)?, NoError>
|
|
|
|
let animationSize = NSMakeSize(200, 200)
|
|
|
|
if let animation = messageEffect.effectAnimation {
|
|
signal = context.account.postbox.mediaBox.resourceData(animation.resource) |> filter { $0.complete } |> take(1) |> map { data in
|
|
if data.complete, let data = try? Data(contentsOf: URL(fileURLWithPath: data.path)) {
|
|
return (LottieAnimation(compressed: data, key: .init(key: .bundle("_prem_effect_\(animation.fileId.id)"), size: animationSize, backingScale: Int(System.backingScale), mirror: false), cachePurpose: .temporaryLZ4(.effect), playPolicy: .onceEnd), animation.stickerText ?? "")
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
} else {
|
|
if let effect = messageEffect.effectSticker.premiumEffect {
|
|
signal = context.account.postbox.mediaBox.resourceData(effect.resource) |> filter { $0.complete } |> take(1) |> map { data in
|
|
if data.complete, let data = try? Data(contentsOf: URL(fileURLWithPath: data.path)) {
|
|
return (LottieAnimation(compressed: data, key: .init(key: .bundle("_prem_effect_\(file.fileId.id)"), size: animationSize, backingScale: Int(System.backingScale), mirror: false), cachePurpose: .temporaryLZ4(.effect), playPolicy: .onceEnd), file.stickerText ?? "")
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
} else {
|
|
signal = .single(nil)
|
|
}
|
|
}
|
|
_ = (signal |> deliverOnMainQueue).startStandalone(next: { value in
|
|
|
|
if let animation = value?.0 {
|
|
let player = LottiePlayerView(frame: NSMakeRect(toRect.minX - animationSize.width / 2 - 50, toRect.minY - animationSize.height / 2 + 30, animationSize.width, animationSize.height))
|
|
|
|
animation.triggerOn = (LottiePlayerTriggerFrame.last, { [weak player] in
|
|
player?.removeFromSuperview()
|
|
}, {})
|
|
player.set(animation)
|
|
context.window.contentView?.addSubview(player)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
} else if let view = self.messageEffect {
|
|
performSubviewRemoval(view, animated: animated)
|
|
self.messageEffect = nil
|
|
|
|
let players = context.window.contentView?.subviews.compactMap {
|
|
$0 as? LottiePlayerView
|
|
}
|
|
|
|
if let players {
|
|
for view in players {
|
|
performSubviewRemoval(view, animated: animated, scale: true)
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
func needUpdateReplyMarkup(with state:ChatPresentationInterfaceState, _ animated:Bool) {
|
|
if let keyboardMessage = state.keyboardButtonsMessage, let attribute = keyboardMessage.replyMarkup, state.isKeyboardShown || attribute.flags.contains(.persistent) {
|
|
replyMarkupModel = ReplyMarkupNode(attribute.rows, attribute.flags, chatInteraction.processBotKeyboard(with: keyboardMessage), theme, bottomView.documentView as? View, true)
|
|
replyMarkupModel?.measureSize(frame.width - 30)
|
|
replyMarkupModel?.redraw()
|
|
replyMarkupModel?.layout()
|
|
bottomView.contentView.scroll(to: NSZeroPoint)
|
|
}
|
|
}
|
|
|
|
func isEqual(to other: Notifable) -> Bool {
|
|
if let other = other as? ChatInputView {
|
|
return other == self
|
|
}
|
|
return false
|
|
}
|
|
|
|
var chatState:ChatState {
|
|
return chatInteraction.presentation.state
|
|
}
|
|
|
|
func contentHeight(for width: CGFloat) -> CGFloat {
|
|
return chatState == .normal || chatState == .editing ? textViewSize(width).0.height : CGFloat(textView.min_height)
|
|
}
|
|
|
|
func needUpdateChatState(with state:ChatState, _ animated:Bool) -> Void {
|
|
CATransaction.begin()
|
|
if animated {
|
|
inputDidUpdateLayout(animated: animated)
|
|
}
|
|
|
|
recordingPanelView?.removeFromSuperview()
|
|
recordingPanelView = nil
|
|
blockedActionView?.removeFromSuperview()
|
|
blockedActionView = nil
|
|
additionBlockedActionView?.removeFromSuperview()
|
|
additionBlockedActionView = nil
|
|
chatDiscussionView?.removeFromSuperview()
|
|
chatDiscussionView = nil
|
|
restrictedView?.removeFromSuperview()
|
|
restrictedView = nil
|
|
messageActionsPanelView?.removeFromSuperview()
|
|
messageActionsPanelView = nil
|
|
|
|
blockText?.removeFromSuperview()
|
|
blockText = nil
|
|
|
|
textView.isHidden = false
|
|
|
|
let chatInteraction = self.chatInteraction
|
|
switch state {
|
|
case .normal, .editing:
|
|
self.contentView.isHidden = false
|
|
self.contentView.change(opacity: 1.0, animated: animated)
|
|
self.accessory.change(opacity: 1.0, animated: animated)
|
|
break
|
|
case .selecting:
|
|
self.messageActionsPanelView = MessageActionsPanelView(frame: bounds)
|
|
self.messageActionsPanelView?.prepare(with: chatInteraction)
|
|
if animated {
|
|
self.messageActionsPanelView?.layer?.animateAlpha(from: 0, to: 1, duration: 0.2)
|
|
}
|
|
self.addSubview(self.messageActionsPanelView!, positioned: .below, relativeTo: _ts)
|
|
self.contentView.isHidden = true
|
|
self.contentView.change(opacity: 0.0, animated: animated)
|
|
self.accessory.change(opacity: 0.0, animated: animated)
|
|
break
|
|
case let .block(string):
|
|
if !string.isEmpty {
|
|
let current = Control(frame: NSMakeRect(0, 0, frame.width, frame.height - 1))
|
|
current.backgroundColor = theme.colors.background
|
|
addSubview(current)
|
|
self.blockText = current
|
|
|
|
let context = chatInteraction.context
|
|
|
|
let textView = TextView()
|
|
textView.isSelectable = false
|
|
|
|
let parsed = parseMarkdownIntoAttributedString(string, attributes: MarkdownAttributes.init(body: MarkdownAttributeSet(font: .normal(.text), textColor: theme.colors.grayText), bold: MarkdownAttributeSet(font: .medium(.text), textColor: theme.colors.grayText), link: MarkdownAttributeSet(font: .medium(.text), textColor: theme.colors.link), linkAttribute: { link in
|
|
return (NSAttributedString.Key.link.rawValue, inAppLink.callback(link, { value in
|
|
if value == "premium" {
|
|
showModal(with: PremiumBoardingController(context: context), for: context.window)
|
|
}
|
|
}))
|
|
})).detectBold(with: .medium(.text))
|
|
let layout = TextViewLayout(parsed, alignment: .center)
|
|
layout.measure(width: frame.width - 40)
|
|
layout.interactions = globalLinkExecutor
|
|
textView.update(layout)
|
|
current.addSubview(textView)
|
|
textView.center()
|
|
} else if let view = blockText {
|
|
performSubviewRemoval(view, animated: animated)
|
|
blockText = nil
|
|
}
|
|
case let .action(text, action, addition):
|
|
self.messageActionsPanelView?.removeFromSuperview()
|
|
self.blockedActionView?.removeFromSuperview()
|
|
|
|
let blockedActionView = TextButton(frame: bounds)
|
|
blockedActionView.autoSizeToFit = false
|
|
blockedActionView.set(color: theme.colors.accent, for: .Normal)
|
|
blockedActionView.set(font: .normal(.title), for: .Normal)
|
|
|
|
blockedActionView.set(text: text, for: .Normal)
|
|
blockedActionView.set(background: theme.colors.grayBackground, for: .Highlight)
|
|
blockedActionView.sizeToFit(.zero, bounds.size, thatFit: true)
|
|
if animated {
|
|
blockedActionView.layer?.animateAlpha(from: 0, to: 1, duration: 0.2)
|
|
}
|
|
blockedActionView.set(handler: {_ in
|
|
action(chatInteraction)
|
|
}, for:.Click)
|
|
|
|
self.addSubview(blockedActionView, positioned: .below, relativeTo: _ts)
|
|
|
|
self.blockedActionView = blockedActionView
|
|
|
|
if let addition = addition {
|
|
additionBlockedActionView = ImageButton()
|
|
additionBlockedActionView?.animates = false
|
|
additionBlockedActionView?.set(image: addition.icon, for: .Normal)
|
|
additionBlockedActionView?.sizeToFit()
|
|
addSubview(additionBlockedActionView!, positioned: .above, relativeTo: self.blockedActionView)
|
|
|
|
additionBlockedActionView?.set(handler: { control in
|
|
addition.action(control)
|
|
}, for: .Click)
|
|
} else {
|
|
additionBlockedActionView?.removeFromSuperview()
|
|
additionBlockedActionView = nil
|
|
}
|
|
|
|
self.contentView.isHidden = true
|
|
self.contentView.change(opacity: 0.0, animated: animated)
|
|
self.accessory.change(opacity: 0.0, animated: animated)
|
|
case let .botStart(text, action):
|
|
self.messageActionsPanelView?.removeFromSuperview()
|
|
self.blockedActionView?.removeFromSuperview()
|
|
|
|
self.blockedActionView = TextButton(frame: bounds.insetBy(dx: 5, dy: 5))
|
|
self.blockedActionView?.autoSizeToFit = false
|
|
self.blockedActionView?.style = ControlStyle(font: .normal(.title),foregroundColor: theme.colors.underSelectedColor)
|
|
self.blockedActionView?.set(text: text, for: .Normal)
|
|
self.blockedActionView?.scaleOnClick = true
|
|
self.blockedActionView?.set(background: theme.colors.accent, for: .Normal)
|
|
self.blockedActionView?.set(background: theme.colors.accent.withAlphaComponent(0.8), for: .Highlight)
|
|
self.blockedActionView?.sizeToFit(.zero, bounds.insetBy(dx: 5, dy: 5).size, thatFit: true)
|
|
|
|
|
|
let shimmer = ShimmerEffectView()
|
|
shimmer.isStatic = true
|
|
self.blockedActionView?.addSubview(shimmer)
|
|
|
|
self.blockedActionView?.layer?.cornerRadius = 10
|
|
if animated {
|
|
self.blockedActionView?.layer?.animateAlpha(from: 0, to: 1, duration: 0.2)
|
|
}
|
|
self.blockedActionView?.set(handler: {_ in
|
|
action(chatInteraction)
|
|
}, for:.Click)
|
|
|
|
|
|
|
|
self.addSubview(self.blockedActionView!, positioned: .below, relativeTo: _ts)
|
|
|
|
self.contentView.isHidden = true
|
|
self.contentView.change(opacity: 0.0, animated: animated)
|
|
self.accessory.change(opacity: 0.0, animated: animated)
|
|
case let .channelWithDiscussion(discussionGroupId, leftAction, rightAction):
|
|
self.messageActionsPanelView?.removeFromSuperview()
|
|
self.chatDiscussionView = ChannelDiscussionInputView(frame: bounds)
|
|
self.chatDiscussionView?.update(with: chatInteraction, discussionGroupId: discussionGroupId, leftAction: leftAction, rightAction: rightAction)
|
|
|
|
self.addSubview(self.chatDiscussionView!, positioned: .below, relativeTo: _ts)
|
|
self.contentView.isHidden = true
|
|
self.contentView.change(opacity: 0.0, animated: animated)
|
|
self.accessory.change(opacity: 0.0, animated: animated)
|
|
case let .recording(recorder):
|
|
textView.isHidden = true
|
|
recordingPanelView = ChatInputRecordingView(frame: NSMakeRect(0,0,frame.width,standart), chatInteraction:chatInteraction, recorder:recorder)
|
|
addSubview(recordingPanelView!, positioned: .below, relativeTo: _ts)
|
|
if animated {
|
|
self.recordingPanelView?.layer?.animateAlpha(from: 0, to: 1, duration: 0.2)
|
|
}
|
|
case let.restricted( text):
|
|
self.messageActionsPanelView?.removeFromSuperview()
|
|
self.restrictedView = RestrictionWrappedView(text)
|
|
if animated {
|
|
self.restrictedView?.layer?.animateAlpha(from: 0, to: 1, duration: 0.2)
|
|
}
|
|
self.addSubview(self.restrictedView!, positioned: .below, relativeTo: _ts)
|
|
self.contentView.isHidden = true
|
|
self.contentView.change(opacity: 0.0, animated: animated)
|
|
self.accessory.change(opacity: 0.0, animated: animated)
|
|
}
|
|
|
|
if let peer = chatInteraction.presentation.peer, let text = permissionText(from: peer, for: .banSendText, cachedData: chatInteraction.presentation.cachedData), state == .normal {
|
|
let context = chatInteraction.context
|
|
let current: Control
|
|
if let view = self.disallowText {
|
|
current = view
|
|
} else {
|
|
current = Control(frame: textView.frame)
|
|
self.contentView.addSubview(current)
|
|
self.disallowText = current
|
|
}
|
|
current.removeAllHandlers()
|
|
current.set(handler: { _ in
|
|
showModalText(for: context.window, text: text)
|
|
}, for: .Click)
|
|
current.set(cursor: .arrow, for: .Normal)
|
|
current.set(cursor: .arrow, for: .Highlight)
|
|
current.set(cursor: .arrow, for: .Hover)
|
|
|
|
} else if let view = self.disallowText {
|
|
performSubviewRemoval(view, animated: animated)
|
|
self.disallowText = nil
|
|
}
|
|
|
|
CATransaction.commit()
|
|
}
|
|
|
|
func updateInput(_ state:ChatPresentationInterfaceState, prevState: ChatPresentationInterfaceState, animated:Bool = true, initial: Bool = false) -> Void {
|
|
|
|
if let peer = state.peer, let _ = permissionText(from: peer, for: .banSendText, cachedData: state.cachedData), state.state == .normal {
|
|
textView.inputView.isEditable = false
|
|
textView.isHidden = false
|
|
} else {
|
|
switch state.state {
|
|
case .normal, .editing:
|
|
textView.inputView.isEditable = true
|
|
textView.isHidden = false
|
|
case let .block(string):
|
|
textView.isHidden = !string.isEmpty
|
|
default:
|
|
textView.inputView.isEditable = false
|
|
}
|
|
}
|
|
|
|
let input = state.effectiveInput
|
|
|
|
self.textView.interactions.inputIsEnabled = self.isEnabled()
|
|
self.textView.set(input)
|
|
self.textView.placeholder = textPlaceholder
|
|
|
|
if prevState.effectiveInput.inputText.isEmpty {
|
|
self.textView.scrollToCursor()
|
|
}
|
|
|
|
if state.effectiveInput != prevState.effectiveInput {
|
|
if state.effectiveInput.inputText.count != prevState.effectiveInput.inputText.count {
|
|
self.textView.scrollToCursor()
|
|
}
|
|
}
|
|
|
|
if chatInteraction.context.peerId != chatInteraction.peerId, let peer = chatInteraction.presentation.peer, !peer.isChannel && !markNextTextChangeToFalseActivity {
|
|
sendActivityDisposable.set((Signal<Bool, NoError>.single(!state.effectiveInput.inputText.isEmpty) |> then(Signal<Bool, NoError>.single(false) |> delay(4.0, queue: Queue.mainQueue()))).start(next: { [weak self] isPresent in
|
|
if let chatInteraction = self?.chatInteraction, let peer = chatInteraction.presentation.peer, !peer.isChannel && chatInteraction.presentation.state != .editing {
|
|
if self?.chatInteraction.peerIsAccountPeer == true {
|
|
chatInteraction.context.account.updateLocalInputActivity(peerId: .init(peerId: peer.id, category: chatInteraction.mode.activityCategory), activity: .typingText, isPresent: isPresent)
|
|
}
|
|
}
|
|
}))
|
|
}
|
|
|
|
|
|
}
|
|
private var updateFirstTime: Bool = true
|
|
func updateAdditions(_ state:ChatPresentationInterfaceState, _ animated:Bool = true) -> Void {
|
|
accessory.update(with: state, context: chatInteraction.context, animated: animated)
|
|
|
|
accessoryDisposable.set(accessory.nodeReady.get().start(next: { [weak self] animated in
|
|
self?.updateAccesory(animated: animated)
|
|
}))
|
|
self.textView.placeholder = textPlaceholder
|
|
}
|
|
|
|
func updatePlaceholder() {
|
|
self.textView.placeholder = textPlaceholder
|
|
}
|
|
|
|
private func updateAccesory(animated: Bool) {
|
|
self.accessory.measureSize(self.frame.width - 40.0)
|
|
self.inputDidUpdateLayout(animated: animated)
|
|
self.updateLayout(size: self.frame.size, transition: animated ? .animated(duration: 0.2, curve: .easeOut) : .immediate)
|
|
if self.updateFirstTime {
|
|
self.updateFirstTime = false
|
|
self.textView.scrollToCursor()
|
|
}
|
|
}
|
|
|
|
|
|
func updateAttachments(_ inputState:ChatPresentationInterfaceState, _ animated:Bool = true) -> Void {
|
|
if let botMenu = inputState.botMenu, !botMenu.isEmpty, inputState.interfaceState.inputState.inputText.isEmpty {
|
|
let current: ChatInputMenuView
|
|
if let view = self.botMenuView {
|
|
current = view
|
|
} else {
|
|
current = ChatInputMenuView(frame: NSMakeRect(0, 0, 60, 50))
|
|
self.botMenuView = current
|
|
contentView.addSubview(current)
|
|
|
|
if animated {
|
|
current.layer?.animateAlpha(from: 0, to: 1, duration: 0.2)
|
|
current.layer?.animateScaleSpring(from: 0.1, to: 1, duration: 0.2, bounce: false)
|
|
}
|
|
}
|
|
current.chatInteraction = self.chatInteraction
|
|
current.update(botMenu, animated: animated)
|
|
} else {
|
|
if let view = self.botMenuView {
|
|
self.botMenuView = nil
|
|
performSubviewRemoval(view, animated: animated, scale: true)
|
|
}
|
|
}
|
|
var anim = animated
|
|
if let sendAsPeers = inputState.sendAsPeers, !sendAsPeers.isEmpty && inputState.state == .normal {
|
|
let current: ChatInputSendAsView
|
|
if let view = self.sendAsView {
|
|
current = view
|
|
} else {
|
|
current = ChatInputSendAsView(frame: NSMakeRect(0, 0, 50, 50))
|
|
self.sendAsView = current
|
|
contentView.addSubview(current)
|
|
anim = false
|
|
}
|
|
current.update(sendAsPeers, currentPeerId: inputState.currentSendAsPeerId ?? self.chatInteraction.context.peerId, chatInteraction: self.chatInteraction, animated: animated)
|
|
} else {
|
|
if let view = self.sendAsView {
|
|
self.sendAsView = nil
|
|
performSubviewRemoval(view, animated: animated)
|
|
}
|
|
}
|
|
updateLayout(size: frame.size, transition: anim ? .animated(duration: 0.2, curve: .easeOut) : .immediate)
|
|
}
|
|
|
|
|
|
func updateLayout(size: NSSize, transition: ContainedViewLayoutTransition) {
|
|
|
|
|
|
let bottomInset = chatInteraction.presentation.isKeyboardShown ? bottomHeight : 0
|
|
let keyboardWidth = frame.width - 30
|
|
var leftInset: CGFloat = 0
|
|
let contentHeight:CGFloat = contentHeight(for: size.width)
|
|
|
|
transition.updateFrame(view: contentView, frame: NSMakeRect(0, bottomInset, size.width, contentHeight))
|
|
transition.updateFrame(view: bottomView, frame: NSMakeRect(20, chatInteraction.presentation.isKeyboardShown ? 0 : -bottomHeight, keyboardWidth, bottomHeight))
|
|
|
|
let actionsSize = actionsView.size(chatInteraction.presentation)
|
|
let immediate: ContainedViewLayoutTransition = .immediate
|
|
immediate.updateFrame(view: actionsView, frame: CGRect(origin: CGPoint(x: size.width - actionsSize.width, y: 0), size: actionsSize))
|
|
actionsView.updateLayout(size: actionsSize, transition: immediate)
|
|
|
|
|
|
if let view = messageEffect {
|
|
transition.updateFrame(view: view, frame: NSMakeRect(size.width - view.frame.width - 10, 5, view.frame.width, view.frame.height))
|
|
}
|
|
|
|
if let view = botMenuView {
|
|
leftInset += view.frame.width
|
|
transition.updateFrame(view: view, frame: NSMakeRect(0, 0, view.frame.width, view.frame.height))
|
|
}
|
|
if let view = sendAsView {
|
|
leftInset += view.frame.width
|
|
transition.updateFrame(view: view, frame: NSMakeRect(0, 0, view.frame.width, view.frame.height))
|
|
}
|
|
if let markup = replyMarkupModel, markup.hasButtons, let view = markup.view {
|
|
markup.measureSize(keyboardWidth)
|
|
transition.updateFrame(view: view, frame: NSMakeRect(0, 0, markup.size.width, markup.size.height))
|
|
markup.layout(transition: transition)
|
|
}
|
|
|
|
if let current = self.blockText {
|
|
transition.updateFrame(view: current, frame: CGRect(origin: .zero, size: NSMakeSize(size.width, size.height - 1)))
|
|
if let subview = current.subviews.first {
|
|
transition.updateFrame(view: subview, frame: subview.centerFrame())
|
|
}
|
|
}
|
|
|
|
transition.updateFrame(view: attachView, frame: NSMakeRect(leftInset, 0, attachView.frame.width, attachView.frame.height))
|
|
leftInset += attachView.frame.width
|
|
|
|
|
|
let (textSize, textHeight) = self.textViewSize(size.width)
|
|
|
|
let viewRect = NSMakeRect(leftInset, 0, textSize.width, textSize.height)
|
|
transition.updateFrame(view: textView, frame: viewRect)
|
|
textView.updateLayout(size: viewRect.size, textHeight: textHeight, transition: transition)
|
|
|
|
if let view = disallowText {
|
|
transition.updateFrame(view: view, frame: textView.frame)
|
|
}
|
|
|
|
if let view = additionBlockedActionView {
|
|
transition.updateFrame(view: view, frame: view.centerFrameY(x: size.width - view.frame.width - 22))
|
|
}
|
|
|
|
transition.updateFrame(view: _ts, frame: NSMakeRect(0, size.height - .borderSize, size.width, .borderSize))
|
|
|
|
accessory.measureSize(size.width - 64)
|
|
transition.updateFrame(view: accessory, frame: NSMakeRect(15, contentView.frame.maxY, size.width - 39, accessory.size.height))
|
|
accessory.updateLayout(NSMakeSize(size.width - 39, accessory.size.height), transition: transition)
|
|
|
|
if let view = messageActionsPanelView {
|
|
transition.updateFrame(view: view, frame: size.bounds)
|
|
}
|
|
if let view = blockedActionView {
|
|
if view.scaleOnClick {
|
|
transition.updateFrame(view: view, frame: size.bounds.insetBy(dx: 5, dy: 5))
|
|
} else {
|
|
transition.updateFrame(view: view, frame: size.bounds)
|
|
}
|
|
for subview in view.subviews {
|
|
if let shimmer = subview as? ShimmerEffectView {
|
|
transition.updateFrame(view: subview, frame: view.bounds)
|
|
shimmer.updateAbsoluteRect(view.bounds, within: view.frame.size)
|
|
shimmer.update(backgroundColor: .clear, foregroundColor: .clear, shimmeringColor: NSColor.white.withAlphaComponent(0.3), shapes: [.roundedRect(rect: view.bounds, cornerRadius: view.frame.height / 2)], horizontal: true, size: view.frame.size)
|
|
|
|
}
|
|
}
|
|
}
|
|
if let view = chatDiscussionView {
|
|
transition.updateFrame(view: view, frame: size.bounds)
|
|
}
|
|
if let view = restrictedView {
|
|
transition.updateFrame(view: view, frame: size.bounds)
|
|
}
|
|
|
|
guard let superview = superview else { return }
|
|
textInteractions.max_height = floorToScreenPixels(backingScaleFactor, superview.frame.height / 2) + 50.0
|
|
|
|
}
|
|
|
|
|
|
override func layout() {
|
|
super.layout()
|
|
self.updateLayout(size: self.frame.size, transition: .immediate)
|
|
}
|
|
|
|
|
|
var stringValue:String {
|
|
return textView.string()
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
required init(frame frameRect: NSRect) {
|
|
fatalError("init(frame:) has not been implemented")
|
|
}
|
|
|
|
|
|
func inputDidUpdateLayout(animated: Bool) {
|
|
let contentHeight:CGFloat = contentHeight(for: self.frame.width)
|
|
var sumHeight:CGFloat = contentHeight + (accessory.isVisibility() ? accessory.size.height + 5 : 0)
|
|
if let markup = replyMarkupModel {
|
|
bottomHeight = min(
|
|
ChatInputView.maxBottomHeight,
|
|
markup.size.height + ChatInputView.bottomPadding
|
|
)
|
|
} else {
|
|
bottomHeight = 0
|
|
}
|
|
if chatInteraction.presentation.isKeyboardShown {
|
|
sumHeight += bottomHeight
|
|
}
|
|
|
|
delegate?.inputChanged(height: sumHeight, animated: animated)
|
|
|
|
}
|
|
|
|
var currentActionView: NSView {
|
|
return self.actionsView.currentActionView
|
|
}
|
|
var emojiView: NSView {
|
|
return self.actionsView.entertaiments
|
|
}
|
|
func makeSpoiler() {
|
|
self.textView.inputApplyTransform(.attribute(TextInputAttributes.spoiler))
|
|
}
|
|
func makeUnderline() {
|
|
self.textView.inputApplyTransform(.attribute(TextInputAttributes.underline))
|
|
}
|
|
func makeQuote() {
|
|
self.textView.inputApplyTransform(.attribute(TextInputAttributes.quote))
|
|
}
|
|
func makeStrikethrough() {
|
|
self.textView.inputApplyTransform(.attribute(TextInputAttributes.strikethrough))
|
|
}
|
|
func makeBold() {
|
|
self.textView.inputApplyTransform(.attribute(TextInputAttributes.bold))
|
|
}
|
|
func removeAllAttributes() {
|
|
self.textView.inputApplyTransform(.clear)
|
|
}
|
|
func makeUrl() {
|
|
self.textView.inputApplyTransform(.url)
|
|
}
|
|
func makeItalic() {
|
|
self.textView.inputApplyTransform(.attribute(TextInputAttributes.italic))
|
|
}
|
|
func makeMonospace() {
|
|
self.textView.inputApplyTransform(.attribute(TextInputAttributes.monospace))
|
|
}
|
|
|
|
override func becomeFirstResponder() -> Bool {
|
|
return self.textView.inputView.becomeFirstResponder()
|
|
}
|
|
|
|
func makeFirstResponder() {
|
|
self.window?.makeFirstResponder(self.responder)
|
|
}
|
|
|
|
|
|
deinit {
|
|
self.accessoryDisposable.dispose()
|
|
self.rtfAttachmentsDisposable.dispose()
|
|
self.slowModeUntilDisposable.dispose()
|
|
self.chatInteraction.remove(observer: self)
|
|
}
|
|
|
|
func textViewSize(_ width: CGFloat) -> (NSSize, CGFloat) {
|
|
var leftInset: CGFloat = attachView.frame.width
|
|
if let botMenu = self.botMenuView {
|
|
leftInset += botMenu.frame.width
|
|
}
|
|
if let sendAsView = self.sendAsView {
|
|
leftInset += sendAsView.frame.width
|
|
}
|
|
let w = width - actionsView.size(chatInteraction.presentation).width - leftInset
|
|
let height = self.textView.height(for: w)
|
|
return (NSMakeSize(w, min(max(height, textView.min_height), textView.max_height)), height)
|
|
}
|
|
|
|
func isEnabled() -> Bool {
|
|
if let editState = chatInteraction.presentation.interfaceState.editState {
|
|
if editState.loadingState != .none {
|
|
return false
|
|
}
|
|
}
|
|
return self.chatState == .normal || self.chatState == .editing
|
|
}
|
|
|
|
|
|
func copyAttributedString(_ attributedString: NSAttributedString!) -> Bool {
|
|
return globalLinkExecutor.copyAttributedString(attributedString)
|
|
}
|
|
|
|
func processPaste(_ pasteboard: NSPasteboard) -> Bool {
|
|
|
|
let interaction = self.chatInteraction
|
|
|
|
defer {
|
|
DispatchQueue.main.async { [weak self] in
|
|
self?.textView.scrollToCursor()
|
|
}
|
|
}
|
|
|
|
if let window = _window, self.chatState == .normal || self.chatState == .editing {
|
|
|
|
if let string = pasteboard.string(forType: .string) {
|
|
interaction.update { current in
|
|
if let disabled = current.interfaceState.composeDisableUrlPreview, disabled.lowercased() == string.lowercased() {
|
|
return current.updatedInterfaceState {$0.withUpdatedComposeDisableUrlPreview(nil)}
|
|
}
|
|
return current
|
|
}
|
|
}
|
|
|
|
let result = InputPasteboardParser.proccess(pasteboard: pasteboard, chatInteraction: interaction, window: window)
|
|
if result {
|
|
if let disallowText = disallowText {
|
|
disallowText.send(event: .Click)
|
|
textView.shake(beep: true)
|
|
} else {
|
|
if let data = pasteboard.data(forType: .kInApp) {
|
|
let decoder = AdaptedPostboxDecoder()
|
|
if let decoded = try? decoder.decode(ChatTextInputState.self, from: data) {
|
|
let state = decoded.unique(isPremium: interaction.context.isPremium)
|
|
|
|
interaction.appendText(state.attributedString())
|
|
|
|
return true
|
|
}
|
|
} else if let data = pasteboard.data(forType: .rtf) {
|
|
if let attributed = (try? NSAttributedString(data: data, options: [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.rtfd], documentAttributes: nil)) ?? (try? NSAttributedString(data: data, options: [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.rtf], documentAttributes: nil)) {
|
|
|
|
let (attributed, attachments) = attributed.applyRtf()
|
|
|
|
if !attachments.isEmpty {
|
|
rtfAttachmentsDisposable.set((prepareTextAttachments(attachments) |> deliverOnMainQueue).start(next: { [weak self] urls in
|
|
if !urls.isEmpty, let interaction = self?.chatInteraction {
|
|
interaction.showPreviewSender(urls, true, attributed)
|
|
}
|
|
}))
|
|
} else {
|
|
self.chatInteraction.appendText(attributed)
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return !result
|
|
}
|
|
|
|
return self.chatState != .normal
|
|
}
|
|
|
|
}
|