import Foundation import UIKit import Display import AsyncDisplayKit import TelegramPresentationData import ComponentFlow import ChatControllerInteraction import AccountContext import ChatPresentationInterfaceState import TelegramCore import ComponentDisplayAdapters private final class EmptyInputView: UIView, UIInputViewAudioFeedback { var enableInputClicksWhenVisible: Bool { return true } } public final class ChatTextInputPanelComponent: Component { public final class ExternalState { public fileprivate(set) var isEditing: Bool = false public fileprivate(set) var textInputState: ChatTextInputState = ChatTextInputState() public var resetInputState: ChatTextInputState? public init() { } } public enum InputMode { case text case emoji case stickers case commands } public final class InlineAction: Equatable { public enum Kind: Equatable { case paidMessage case inputMode(InputMode) } public let kind: Kind public let action: () -> Void public init(kind: Kind, action: @escaping () -> Void) { self.kind = kind self.action = action } public static func ==(lhs: InlineAction, rhs: InlineAction) -> Bool { if lhs.kind != rhs.kind { return false } return true } } public final class LeftAction: Equatable { public enum Kind: Equatable { case attach case toggleExpanded(isVisible: Bool, isExpanded: Bool) } public let kind: Kind public let action: () -> Void public init(kind: Kind, action: @escaping () -> Void) { self.kind = kind self.action = action } public static func ==(lhs: LeftAction, rhs: LeftAction) -> Bool { if lhs.kind != rhs.kind { return false } return true } } public final class RightAction: Equatable { public enum Kind: Equatable { case stars(count: Int, isFilled: Bool) } public let kind: Kind public let action: () -> Void public init(kind: Kind, action: @escaping () -> Void) { self.kind = kind self.action = action } public static func ==(lhs: RightAction, rhs: RightAction) -> Bool { if lhs.kind != rhs.kind { return false } return true } } let externalState: ExternalState let context: AccountContext let theme: PresentationTheme let strings: PresentationStrings let chatPeerId: EnginePeer.Id let inlineActions: [InlineAction] let leftAction: LeftAction? let rightAction: RightAction? let placeholder: String let paidMessagePrice: StarsAmount? let sendColor: UIColor? let hideKeyboard: Bool let insets: UIEdgeInsets let maxHeight: CGFloat let maxLength: Int? let sendAction: (() -> Void)? let sendContextAction: ((UIView, ContextGesture) -> Void)? public init( externalState: ExternalState, context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, chatPeerId: EnginePeer.Id, inlineActions: [InlineAction], leftAction: LeftAction?, rightAction: RightAction?, placeholder: String, paidMessagePrice: StarsAmount?, sendColor: UIColor?, hideKeyboard: Bool, insets: UIEdgeInsets, maxHeight: CGFloat, maxLength: Int?, sendAction: (() -> Void)?, sendContextAction: ((UIView, ContextGesture) -> Void)? ) { self.externalState = externalState self.context = context self.theme = theme self.strings = strings self.chatPeerId = chatPeerId self.inlineActions = inlineActions self.leftAction = leftAction self.rightAction = rightAction self.placeholder = placeholder self.paidMessagePrice = paidMessagePrice self.sendColor = sendColor self.hideKeyboard = hideKeyboard self.insets = insets self.maxHeight = maxHeight self.maxLength = maxLength self.sendAction = sendAction self.sendContextAction = sendContextAction } public static func ==(lhs: ChatTextInputPanelComponent, rhs: ChatTextInputPanelComponent) -> Bool { if lhs.externalState !== rhs.externalState { return false } if lhs.context !== rhs.context { return false } if lhs.theme !== rhs.theme { return false } if lhs.strings !== rhs.strings { return false } if lhs.chatPeerId != rhs.chatPeerId { return false } if lhs.inlineActions != rhs.inlineActions { return false } if lhs.leftAction != rhs.leftAction { return false } if lhs.rightAction != rhs.rightAction { return false } if lhs.placeholder != rhs.placeholder { return false } if lhs.paidMessagePrice != rhs.paidMessagePrice { return false } if lhs.sendColor != rhs.sendColor { return false } if lhs.hideKeyboard != rhs.hideKeyboard { return false } if lhs.insets != rhs.insets { return false } if lhs.maxHeight != rhs.maxHeight { return false } if lhs.maxLength != rhs.maxLength { return false } if (lhs.sendAction == nil) != (rhs.sendAction == nil) { return false } if (lhs.sendContextAction == nil) != (rhs.sendContextAction == nil) { return false } return true } public final class View: UIView { private var panelNode: ChatTextInputPanelNode? private var interfaceInteraction: ChatPanelInterfaceInteraction? private var component: ChatTextInputPanelComponent? private weak var state: EmptyComponentState? private var isUpdating: Bool = false override public init(frame: CGRect) { super.init(frame: frame) } required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } public func insertText(text: NSAttributedString) { guard let panelNode = self.panelNode else { return } panelNode.insertText(text: text) } public func deleteBackward() { guard let panelNode = self.panelNode, let textView = panelNode.textInputNode?.textView else { return } textView.deleteBackward() } public func activateInput() { guard let panelNode = self.panelNode else { return } panelNode.ensureFocused() } public func updateState(transition: ComponentTransition) { self.state?.updated(transition: transition) } func update(component: ChatTextInputPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false } self.component = component self.state = state if self.interfaceInteraction == nil { let inputModeFromComponent: (ChatTextInputPanelComponent) -> ChatInputMode = { component in for inlineAction in component.inlineActions { switch inlineAction.kind { case let .inputMode(inputMode): switch inputMode { case .text: return .media(mode: .other, expanded: nil, focused: false) case .commands: return .text case .stickers: return .media(mode: .other, expanded: nil, focused: false) case .emoji: return .text } default: break } } return .text } self.interfaceInteraction = ChatPanelInterfaceInteraction( setupReplyMessage: { _, _, _ in }, setupEditMessage: { _, _ in }, beginMessageSelection: { _, _ in }, cancelMessageSelection: { _ in }, deleteSelectedMessages: { }, reportSelectedMessages: { }, reportMessages: { _, _ in }, blockMessageAuthor: { _, _ in }, deleteMessages: { _, _, f in f(.default) }, forwardSelectedMessages: { }, forwardCurrentForwardMessages: { }, forwardMessages: { _ in }, updateForwardOptionsState: { _ in }, presentForwardOptions: { _ in }, presentReplyOptions: { _ in }, presentLinkOptions: { _ in }, presentSuggestPostOptions: { }, shareSelectedMessages: { }, updateTextInputStateAndMode: { [weak self] f in guard let self else { return } if let component = self.component { let currentMode = inputModeFromComponent(component) let (updatedTextInputState, updatedMode) = f(component.externalState.textInputState, currentMode) component.externalState.textInputState = updatedTextInputState if !self.isUpdating { self.state?.updated(transition: .spring(duration: 0.4)) } if updatedMode != currentMode { /*for inlineAction in component.inlineActions { switch inlineAction.kind { case .inputMode: inlineAction.action() return default: break } }*/ } } }, updateInputModeAndDismissedButtonKeyboardMessageId: { [weak self] f in guard let self, let component = self.component else { return } var presentationInterfaceState = ChatPresentationInterfaceState( chatWallpaper: .color(0), theme: component.theme, strings: component.strings, dateTimeFormat: PresentationDateTimeFormat(), nameDisplayOrder: .firstLast, limitsConfiguration: component.context.currentLimitsConfiguration.with({ $0 }), fontSize: .regular, bubbleCorners: PresentationChatBubbleCorners( mainRadius: 16.0, auxiliaryRadius: 8.0, mergeBubbleCorners: true ), accountPeerId: component.context.account.peerId, mode: .standard(.default), chatLocation: .peer(id: component.chatPeerId), subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil, threadData: nil, isGeneralThreadClosed: false, replyMessage: nil, accountPeerColor: nil, businessIntro: nil ) let currentMode = inputModeFromComponent(component) presentationInterfaceState = presentationInterfaceState.updatedInputMode { _ in return currentMode } let (updatedMode, _) = f(presentationInterfaceState) if updatedMode != currentMode { /*for inlineAction in component.inlineActions { switch inlineAction.kind { case .inputMode: inlineAction.action() return default: break } }*/ } if let panelNode = self.panelNode, let textView = panelNode.textInputNode?.textView { component.externalState.isEditing = textView.isFirstResponder } else { component.externalState.isEditing = false } }, openStickers: { [weak self] in guard let self, let component = self.component else { return } for inlineAction in component.inlineActions { switch inlineAction.kind { case .inputMode: inlineAction.action() return default: break } } }, editMessage: { }, beginMessageSearch: { _, _ in }, dismissMessageSearch: { }, updateMessageSearch: { _ in }, openSearchResults: { }, navigateMessageSearch: { _ in }, openCalendarSearch: { }, toggleMembersSearch: { _ in }, navigateToMessage: { _, _, _, _ in }, navigateToChat: { _ in }, navigateToProfile: { _ in }, openPeerInfo: { }, togglePeerNotifications: { }, sendContextResult: { _, _, _, _ in return false }, sendBotCommand: { _, _ in }, sendShortcut: { _ in }, openEditShortcuts: { }, sendBotStart: { _ in }, botSwitchChatWithPayload: { _, _ in }, beginMediaRecording: { _ in }, finishMediaRecording: { _ in }, stopMediaRecording: { }, lockMediaRecording: { }, resumeMediaRecording: { }, deleteRecordedMedia: { }, sendRecordedMedia: { _, _ in }, displayRestrictedInfo: { _, _ in }, displayVideoUnmuteTip: { _ in }, switchMediaRecordingMode: { }, setupMessageAutoremoveTimeout: { }, sendSticker: { _, _, _, _, _, _ in return false }, unblockPeer: { }, pinMessage: { _, _ in }, unpinMessage: { _, _, _ in }, unpinAllMessages: { }, openPinnedList: { _ in }, shareAccountContact: { }, reportPeer: { }, presentPeerContact: { }, dismissReportPeer: { }, deleteChat: { }, beginCall: { _ in }, toggleMessageStickerStarred: { _ in }, presentController: { _, _ in }, presentControllerInCurrent: { _, _ in }, getNavigationController: { return nil }, presentGlobalOverlayController: { _, _ in }, navigateFeed: { }, openGrouping: { }, toggleSilentPost: { }, requestUnvoteInMessage: { _ in }, requestStopPollInMessage: { _ in }, updateInputLanguage: { _ in }, unarchiveChat: { }, openLinkEditing: { }, displaySlowmodeTooltip: { _, _ in }, displaySendMessageOptions: { [weak self] node, gesture in guard let self, let component = self.component else { return } component.sendContextAction?(node.view, gesture) }, openScheduledMessages: { }, openPeersNearby: { }, displaySearchResultsTooltip: { _, _ in }, unarchivePeer: { }, scrollToTop: { }, viewReplies: { _, _ in }, activatePinnedListPreview: { _, _ in }, joinGroupCall: { _ in }, presentInviteMembers: { }, presentGigagroupHelp: { }, openMonoforum: { }, editMessageMedia: { _, _ in }, updateShowCommands: { _ in }, updateShowSendAsPeers: { _ in }, openInviteRequests: { }, openSendAsPeer: { _, _ in }, presentChatRequestAdminInfo: { }, displayCopyProtectionTip: { _, _ in }, openWebView: { _, _, _, _ in }, updateShowWebView: { _ in }, insertText: { _ in }, backwardsDeleteText: { }, restartTopic: { }, toggleTranslation: { _ in }, changeTranslationLanguage: { _ in }, addDoNotTranslateLanguage: { _ in }, hideTranslationPanel: { }, openPremiumGift: { }, openSuggestPost: { [weak self] _, _ in guard let self, let component = self.component else { return } for action in component.inlineActions { if case .paidMessage = action.kind { action.action() break } } }, openPremiumRequiredForMessaging: { }, openStarsPurchase: { _ in }, openMessagePayment: { }, openBoostToUnrestrict: { }, updateRecordingTrimRange: { _, _, _, _ in }, dismissAllTooltips: { }, editTodoMessage: { _, _, _ in }, dismissUrlPreview: { }, dismissForwardMessages: { }, dismissSuggestPost: { }, displayUndo: { _ in }, sendEmoji: { _, _, _ in }, updateHistoryFilter: { _ in }, updateChatLocationThread: { _, _ in }, toggleChatSidebarMode: { }, updateDisplayHistoryFilterAsList: { _ in }, requestLayout: { _ in }, chatController: { return nil }, statuses: nil ) } var presentationInterfaceState = ChatPresentationInterfaceState( chatWallpaper: .color(0), theme: component.theme, strings: component.strings, dateTimeFormat: PresentationDateTimeFormat(), nameDisplayOrder: .firstLast, limitsConfiguration: component.context.currentLimitsConfiguration.with({ $0 }), fontSize: .regular, bubbleCorners: PresentationChatBubbleCorners( mainRadius: 16.0, auxiliaryRadius: 8.0, mergeBubbleCorners: true ), accountPeerId: component.context.account.peerId, mode: .standard(.default), chatLocation: .peer(id: component.chatPeerId), subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil, threadData: nil, isGeneralThreadClosed: false, replyMessage: nil, accountPeerColor: nil, businessIntro: nil ) var inputAccessoryItems: [ChatTextInputAccessoryItem] = [] for inlineAction in component.inlineActions { switch inlineAction.kind { case .paidMessage: inputAccessoryItems.append(.suggestPost) case let .inputMode(inputMode): let mappedInputMode: ChatTextInputAccessoryItem.InputMode switch inputMode { case .emoji: mappedInputMode = .emoji case .stickers: mappedInputMode = .stickers case .text: mappedInputMode = .keyboard case .commands: mappedInputMode = .bot } inputAccessoryItems.append(.input(isEnabled: true, inputMode: mappedInputMode)) } } presentationInterfaceState = presentationInterfaceState.updatedInputTextPanelState { _ in return ChatTextInputPanelState( accessoryItems: inputAccessoryItems, contextPlaceholder: nil, mediaRecordingState: nil ) } presentationInterfaceState = presentationInterfaceState.updatedSendPaidMessageStars(component.paidMessagePrice) let panelNode: ChatTextInputPanelNode if let current = self.panelNode { panelNode = current } else { panelNode = ChatTextInputPanelNode( context: component.context, presentationInterfaceState: presentationInterfaceState, presentationContext: ChatPresentationContext( context: component.context, backgroundNode: nil ), presentController: { c in } ) self.panelNode = panelNode self.addSubview(panelNode.view) panelNode.interfaceInteraction = self.interfaceInteraction panelNode.loadTextInputNodeIfNeeded() panelNode.sendMessage = { [weak self] in guard let self, let component = self.component else { return } component.sendAction?() } panelNode.updateHeight = { [weak self] _ in guard let self else { return } if !self.isUpdating { self.state?.updated(transition: .spring(duration: 0.4)) } } panelNode.displayAttachmentMenu = { [weak self] in guard let self, let component = self.component else { return } if let leftAction = component.leftAction { leftAction.action() } } } if let textView = panelNode.textInputNode?.textView { if component.hideKeyboard { if textView.inputView == nil { textView.inputView = EmptyInputView() textView.reloadInputViews() } } else if textView.inputView != nil { textView.inputView = nil textView.reloadInputViews() } } panelNode.customPlaceholder = component.placeholder if let leftAction = component.leftAction { switch leftAction.kind { case .attach: panelNode.customLeftAction = nil case let .toggleExpanded(isVisible, isExpanded): panelNode.customLeftAction = .toggleExpanded(isVisible: isVisible, isExpanded: isExpanded) } } else { panelNode.customLeftAction = nil } if let rightAction = component.rightAction { switch rightAction.kind { case let .stars(count, isFilled): panelNode.customRightAction = .stars(count: count, isFilled: isFilled, action: { rightAction.action() }) } } else { panelNode.customRightAction = nil } panelNode.customSendColor = component.sendColor panelNode.customInputTextMaxLength = component.maxLength if let resetInputState = component.externalState.resetInputState { component.externalState.resetInputState = nil let _ = resetInputState panelNode.text = "" } let panelHeight = panelNode.updateLayout( width: availableSize.width, leftInset: component.insets.left, rightInset: component.insets.right, bottomInset: component.insets.bottom, additionalSideInsets: UIEdgeInsets(), maxHeight: component.maxHeight, maxOverlayHeight: component.maxHeight, isSecondary: false, transition: transition.containedViewLayoutTransition, interfaceState: presentationInterfaceState, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), isMediaInputExpanded: false ) let panelSize = CGSize(width: availableSize.width, height: panelHeight) let panelFrame = CGRect(origin: CGPoint(), size: panelSize) transition.setFrame(view: panelNode.view, frame: panelFrame) return panelSize } } public func makeView() -> View { return View() } public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } }