From 871072ba95ce3186b27bd8a20158cbc783258cc8 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Mon, 1 Sep 2025 21:45:31 +0200 Subject: [PATCH] Refactor text input --- MODULE.bazel.lock | 2 +- .../Sources/ChatController.swift | 2 + .../Sources/AttachmentPanel.swift | 2 + .../ChatPresentationInterfaceState/BUILD | 2 + .../ChatPanelInterfaceInteraction.swift | 10 + submodules/TelegramUI/BUILD | 2 + .../Sources/ChatRecentActionsController.swift | 2 + .../ChatRecordingViewOnceButtonNode/BUILD | 20 ++ .../ChatRecordingViewOnceButtonNode.swift | 113 +++++++ .../Chat/ChatTextInputActionButtonsNode/BUILD | 32 ++ .../ChatTextInputActionButtonsNode.swift | 40 +-- .../BUILD | 20 ++ ...xtInputAudioRecordingCancelIndicator.swift | 20 +- .../ChatTextInputAudioRecordingTimeNode/BUILD | 23 ++ .../ChatTextInputAudioRecordingTimeNode.swift | 16 +- .../Chat/ChatTextInputPanelNode/BUILD | 66 ++++ .../Sources/ChatTextInputPanelNode.swift | 243 ++++++++------- .../BUILD | 24 ++ ...ChatTextInputSlowmodePlaceholderNode.swift | 8 +- .../Sources/PeerInfoScreen.swift | 2 + .../Sources/PeerSelectionControllerNode.swift | 2 + .../Chat/ChatControllerLoadDisplayNode.swift | 10 + .../TelegramUI/Sources/ChatController.swift | 2 +- .../Sources/ChatControllerNode.swift | 12 +- .../Sources/ChatInterfaceInputContexts.swift | 1 + .../Sources/ChatInterfaceInputNodes.swift | 1 + .../ChatInterfaceStateInputPanels.swift | 5 +- .../ChatRecordingPreviewInputPanelNode.swift | 293 ++++++------------ 28 files changed, 616 insertions(+), 359 deletions(-) create mode 100644 submodules/TelegramUI/Components/Chat/ChatRecordingViewOnceButtonNode/BUILD create mode 100644 submodules/TelegramUI/Components/Chat/ChatRecordingViewOnceButtonNode/Sources/ChatRecordingViewOnceButtonNode.swift create mode 100644 submodules/TelegramUI/Components/Chat/ChatTextInputActionButtonsNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatTextInputActionButtonsNode}/Sources/ChatTextInputActionButtonsNode.swift (91%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatTextInputAudioRecordingCancelIndicator/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatTextInputAudioRecordingCancelIndicator}/Sources/ChatTextInputAudioRecordingCancelIndicator.swift (91%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatTextInputAudioRecordingTimeNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatTextInputAudioRecordingTimeNode}/Sources/ChatTextInputAudioRecordingTimeNode.swift (91%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatTextInputPanelNode}/Sources/ChatTextInputPanelNode.swift (96%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatTextInputSlowmodePlaceholderNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatTextInputSlowmodePlaceholderNode}/Sources/ChatTextInputSlowmodePlaceholderNode.swift (95%) diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index c8f2a8c8b2..ed86535e8b 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -159,7 +159,7 @@ "moduleExtensions": { "@@apple_support+//crosstool:setup.bzl%apple_cc_configure_extension": { "general": { - "bzlTransitiveDigest": "xcBTf2+GaloFpg7YEh/Bv+1yAczRkiCt3DGws4K7kSk=", + "bzlTransitiveDigest": "RjubjYIojbv0PxTpnoknalV9QzT9asbV7elDuN7m2A4=", "usagesDigest": "lfcV4HxPD+NLaRIT/v7BtSGFgE7c9xrWU7jDiwBAxzo=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index ccd93181b5..b71ca3baf8 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -1096,6 +1096,8 @@ public protocol ChatController: ViewController { func playShakeAnimation() func removeAd(opaqueId: Data) + + func restrictedSendingContentsText() -> String } public protocol ChatMessagePreviewItemNode: AnyObject { diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index a59105ca76..8781c6116d 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -1273,6 +1273,8 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { }, updateRecordingTrimRange: { _, _, _, _ in }, dismissAllTooltips: { }, editTodoMessage: { _, _, _ in + }, displayUndo: { _ in + }, sendEmoji: { _, _, _ in }, updateHistoryFilter: { _ in }, updateChatLocationThread: { _, _ in }, toggleChatSidebarMode: { diff --git a/submodules/ChatPresentationInterfaceState/BUILD b/submodules/ChatPresentationInterfaceState/BUILD index 566e775d53..3ca6b4d9dc 100644 --- a/submodules/ChatPresentationInterfaceState/BUILD +++ b/submodules/ChatPresentationInterfaceState/BUILD @@ -22,6 +22,8 @@ swift_library( "//submodules/ChatContextQuery", "//submodules/TooltipUI", "//submodules/AudioWaveform", + "//submodules/UndoUI", + "//submodules/TextFormat", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift index 394b9e6fc9..8a209885fc 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift @@ -8,6 +8,8 @@ import Display import AccountContext import ContextUI import TooltipUI +import UndoUI +import TextFormat public enum ChatLoadingMessageSubject { case generic @@ -187,6 +189,8 @@ public final class ChatPanelInterfaceInteraction { public let updateRecordingTrimRange: (Double, Double, Bool, Bool) -> Void public let dismissAllTooltips: () -> Void public let editTodoMessage: (MessageId, Int32?, Bool) -> Void + public let displayUndo: (UndoOverlayContent) -> Void + public let sendEmoji: (String, ChatTextInputTextCustomEmojiAttribute, Bool) -> Void public let requestLayout: (ContainedViewLayoutTransition) -> Void public let chatController: () -> ViewController? public let statuses: ChatPanelInterfaceInteractionStatuses? @@ -306,6 +310,8 @@ public final class ChatPanelInterfaceInteraction { updateRecordingTrimRange: @escaping (Double, Double, Bool, Bool) -> Void, dismissAllTooltips: @escaping () -> Void, editTodoMessage: @escaping (MessageId, Int32?, Bool) -> Void, + displayUndo: @escaping (UndoOverlayContent) -> Void, + sendEmoji: @escaping (String, ChatTextInputTextCustomEmojiAttribute, Bool) -> Void, updateHistoryFilter: @escaping ((ChatPresentationInterfaceState.HistoryFilter?) -> ChatPresentationInterfaceState.HistoryFilter?) -> Void, updateChatLocationThread: @escaping (Int64?, ChatControllerAnimateInnerChatSwitchDirection?) -> Void, toggleChatSidebarMode: @escaping () -> Void, @@ -428,6 +434,8 @@ public final class ChatPanelInterfaceInteraction { self.updateRecordingTrimRange = updateRecordingTrimRange self.dismissAllTooltips = dismissAllTooltips self.editTodoMessage = editTodoMessage + self.displayUndo = displayUndo + self.sendEmoji = sendEmoji self.updateHistoryFilter = updateHistoryFilter self.updateChatLocationThread = updateChatLocationThread self.toggleChatSidebarMode = toggleChatSidebarMode @@ -559,6 +567,8 @@ public final class ChatPanelInterfaceInteraction { }, updateRecordingTrimRange: { _, _, _, _ in }, dismissAllTooltips: { }, editTodoMessage: { _, _, _ in + }, displayUndo: { _ in + }, sendEmoji: { _, _, _ in }, updateHistoryFilter: { _ in }, updateChatLocationThread: { _, _ in }, toggleChatSidebarMode: { diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 3003487c92..2aca20f64f 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -486,7 +486,9 @@ swift_library( "//submodules/TelegramUI/Components/FaceScanScreen", "//submodules/TelegramUI/Components/MediaManager/PeerMessagesMediaPlaylist", "//submodules/TelegramUI/Components/ChatThemeScreen", + "//submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode", "//submodules/ContactsHelper", + "//submodules/TelegramUI/Components/Chat/ChatRecordingViewOnceButtonNode", ] + select({ "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "//build-system:ios_sim_arm64": [], diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift index d267817ca8..5208fece2a 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift @@ -173,6 +173,8 @@ public final class ChatRecentActionsController: TelegramBaseController { }, updateRecordingTrimRange: { _, _, _, _ in }, dismissAllTooltips: { }, editTodoMessage: { _, _, _ in + }, displayUndo: { _ in + }, sendEmoji: { _, _, _ in }, updateHistoryFilter: { _ in }, updateChatLocationThread: { _, _ in }, toggleChatSidebarMode: { diff --git a/submodules/TelegramUI/Components/Chat/ChatRecordingViewOnceButtonNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatRecordingViewOnceButtonNode/BUILD new file mode 100644 index 0000000000..d21af7da98 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatRecordingViewOnceButtonNode/BUILD @@ -0,0 +1,20 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatRecordingViewOnceButtonNode", + module_name = "ChatRecordingViewOnceButtonNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/TelegramPresentationData", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Chat/ChatRecordingViewOnceButtonNode/Sources/ChatRecordingViewOnceButtonNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecordingViewOnceButtonNode/Sources/ChatRecordingViewOnceButtonNode.swift new file mode 100644 index 0000000000..45e7997479 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatRecordingViewOnceButtonNode/Sources/ChatRecordingViewOnceButtonNode.swift @@ -0,0 +1,113 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import TelegramPresentationData + +public final class ChatRecordingViewOnceButtonNode: HighlightTrackingButtonNode { + public enum Icon { + case viewOnce + case recordMore + } + + private let icon: Icon + + private let backgroundNode: ASImageNode + private let iconNode: ASImageNode + + private var theme: PresentationTheme? + + public init(icon: Icon) { + self.icon = icon + + self.backgroundNode = ASImageNode() + self.backgroundNode.isUserInteractionEnabled = false + + self.iconNode = ASImageNode() + self.iconNode.isUserInteractionEnabled = false + + super.init(pointerStyle: .default) + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.iconNode) + + self.highligthedChanged = { [weak self] highlighted in + if let self, self.bounds.width > 0.0 { + let topScale: CGFloat = (self.bounds.width - 8.0) / self.bounds.width + let maxScale: CGFloat = (self.bounds.width + 2.0) / self.bounds.width + + if highlighted { + self.layer.removeAnimation(forKey: "sublayerTransform") + let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) + transition.updateTransformScale(node: self, scale: topScale) + } else { + let transition = ContainedViewLayoutTransition.immediate + transition.updateTransformScale(node: self, scale: 1.0) + + self.layer.animateScale(from: topScale, to: maxScale, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in + guard let self else { + return + } + + self.layer.animateScale(from: maxScale, to: 1.0, duration: 0.1, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue) + }) + } + } + } + } + + private var innerIsSelected = false + public func update(isSelected: Bool, animated: Bool = false) { + guard let theme = self.theme else { + return + } + + let updated = self.iconNode.image == nil || self.innerIsSelected != isSelected + self.innerIsSelected = isSelected + + if animated, updated && self.iconNode.image != nil, let snapshot = self.iconNode.view.snapshotContentTree() { + self.view.addSubview(snapshot) + snapshot.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + snapshot.removeFromSuperview() + }) + + self.iconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + if updated { + if case .viewOnce = self.icon { + self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: self.innerIsSelected ? "Media Gallery/ViewOnceEnabled" : "Media Gallery/ViewOnce"), color: theme.chat.inputPanel.panelControlAccentColor) + } + } + } + + public func update(theme: PresentationTheme) -> CGSize { + let size = CGSize(width: 44.0, height: 44.0) + let innerSize = CGSize(width: 40.0, height: 40.0) + + if self.theme !== theme { + self.theme = theme + + self.backgroundNode.image = generateFilledCircleImage(diameter: innerSize.width, color: theme.rootController.navigationBar.opaqueBackgroundColor, strokeColor: theme.chat.inputPanel.panelSeparatorColor, strokeWidth: 0.5, backgroundColor: nil) + + switch self.icon { + case .viewOnce: + self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: self.innerIsSelected ? "Media Gallery/ViewOnceEnabled" : "Media Gallery/ViewOnce"), color: theme.chat.inputPanel.panelControlAccentColor) + + case .recordMore: + self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/IconMicrophone"), color: theme.chat.inputPanel.panelControlAccentColor) + } + } + + if let backgroundImage = self.backgroundNode.image { + let backgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(size.width / 2.0 - backgroundImage.size.width / 2.0), y: floorToScreenPixels(size.height / 2.0 - backgroundImage.size.height / 2.0)), size: backgroundImage.size) + self.backgroundNode.frame = backgroundFrame + } + + if let iconImage = self.iconNode.image { + let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(size.width / 2.0 - iconImage.size.width / 2.0), y: floorToScreenPixels(size.height / 2.0 - iconImage.size.height / 2.0)), size: iconImage.size) + self.iconNode.frame = iconFrame + } + return size + } +} diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputActionButtonsNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatTextInputActionButtonsNode/BUILD new file mode 100644 index 0000000000..00ec3bc877 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputActionButtonsNode/BUILD @@ -0,0 +1,32 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatTextInputActionButtonsNode", + module_name = "ChatTextInputActionButtonsNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/ContextUI", + "//submodules/ChatPresentationInterfaceState", + "//submodules/ChatMessageBackground", + "//submodules/TelegramUI/Components/ChatControllerInteraction", + "//submodules/AccountContext", + "//submodules/TelegramUI/Components/ChatTextInputMediaRecordingButton", + "//submodules/TelegramUI/Components/ChatSendButtonRadialStatusNode", + "//submodules/ChatSendMessageActionUI", + "//submodules/ComponentFlow", + "//submodules/AnimatedCountLabelNode", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatTextInputActionButtonsNode.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputActionButtonsNode/Sources/ChatTextInputActionButtonsNode.swift similarity index 91% rename from submodules/TelegramUI/Sources/ChatTextInputActionButtonsNode.swift rename to submodules/TelegramUI/Components/Chat/ChatTextInputActionButtonsNode/Sources/ChatTextInputActionButtonsNode.swift index 79d7e1d41b..e41a301cff 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputActionButtonsNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputActionButtonsNode/Sources/ChatTextInputActionButtonsNode.swift @@ -126,29 +126,29 @@ private final class EffectBadgeView: UIView { } } -final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessageActionSheetControllerSourceSendButtonNode { +public final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessageActionSheetControllerSourceSendButtonNode { private let context: AccountContext private let presentationContext: ChatPresentationContext? private let strings: PresentationStrings - let micButton: ChatTextInputMediaRecordingButton - let sendContainerNode: ASDisplayNode - let backdropNode: ChatMessageBubbleBackdrop - let backgroundNode: ASDisplayNode - let sendButton: HighlightTrackingButtonNode - var sendButtonRadialStatusNode: ChatSendButtonRadialStatusNode? - var sendButtonHasApplyIcon = false - var animatingSendButton = false + public let micButton: ChatTextInputMediaRecordingButton + public let sendContainerNode: ASDisplayNode + public let backdropNode: ChatMessageBubbleBackdrop + public let backgroundNode: ASDisplayNode + public let sendButton: HighlightTrackingButtonNode + public var sendButtonRadialStatusNode: ChatSendButtonRadialStatusNode? + public var sendButtonHasApplyIcon = false + public var animatingSendButton = false - let textNode: ImmediateAnimatedCountLabelNode + public let textNode: ImmediateAnimatedCountLabelNode - let expandMediaInputButton: HighlightableButtonNode + public let expandMediaInputButton: HighlightableButtonNode private var effectBadgeView: EffectBadgeView? - var sendButtonLongPressed: ((ASDisplayNode, ContextGesture) -> Void)? + public var sendButtonLongPressed: ((ASDisplayNode, ContextGesture) -> Void)? private var gestureRecognizer: ContextGesture? - var sendButtonLongPressEnabled = false { + public var sendButtonLongPressEnabled = false { didSet { self.gestureRecognizer?.isEnabled = self.sendButtonLongPressEnabled } @@ -159,7 +159,7 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessageAction private var validLayout: CGSize? - init(context: AccountContext, presentationInterfaceState: ChatPresentationInterfaceState, presentationContext: ChatPresentationContext?, presentController: @escaping (ViewController) -> Void) { + public init(context: AccountContext, presentationInterfaceState: ChatPresentationInterfaceState, presentationContext: ChatPresentationContext?, presentController: @escaping (ViewController) -> Void) { self.context = context self.presentationContext = presentationContext let theme = presentationInterfaceState.theme @@ -222,7 +222,7 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessageAction self.addSubnode(self.expandMediaInputButton) } - override func didLoad() { + override public func didLoad() { super.didLoad() let gestureRecognizer = ContextGesture(target: nil, action: nil) @@ -239,7 +239,7 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessageAction self.sendButtonPointerInteraction = PointerInteraction(view: self.sendButton.view, customInteractionView: self.backgroundNode.view, style: .lift) } - func updateTheme(theme: PresentationTheme, wallpaper: TelegramWallpaper) { + public func updateTheme(theme: PresentationTheme, wallpaper: TelegramWallpaper) { self.micButton.updateTheme(theme: theme) self.expandMediaInputButton.setImage(PresentationResourcesChat.chatInputPanelExpandButtonImage(theme), for: []) @@ -256,7 +256,7 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessageAction } private var absoluteRect: (CGRect, CGSize)? - func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition) { + public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition) { let previousContaierSize = self.absoluteRect?.1 self.absoluteRect = (rect, containerSize) self.backdropNode.update(rect: rect, within: containerSize, transition: transition) @@ -268,7 +268,7 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessageAction } } - func updateLayout(size: CGSize, isMediaInputExpanded: Bool, showTitle: Bool, currentMessageEffectId: Int64?, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGSize { + public func updateLayout(size: CGSize, isMediaInputExpanded: Bool, showTitle: Bool, currentMessageEffectId: Int64?, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGSize { self.validLayout = size var innerSize = size @@ -367,7 +367,7 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessageAction return innerSize } - func updateAccessibility() { + public func updateAccessibility() { self.accessibilityTraits = .button if !self.micButton.alpha.isZero { switch self.micButton.mode { @@ -384,7 +384,7 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessageAction } } - func makeCustomContents() -> UIView? { + public func makeCustomContents() -> UIView? { if self.sendButtonHasApplyIcon || self.effectBadgeView != nil { let result = UIView() result.frame = self.bounds diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputAudioRecordingCancelIndicator/BUILD b/submodules/TelegramUI/Components/Chat/ChatTextInputAudioRecordingCancelIndicator/BUILD new file mode 100644 index 0000000000..990e3a9844 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputAudioRecordingCancelIndicator/BUILD @@ -0,0 +1,20 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatTextInputAudioRecordingCancelIndicator", + module_name = "ChatTextInputAudioRecordingCancelIndicator", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/TelegramPresentationData", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatTextInputAudioRecordingCancelIndicator.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputAudioRecordingCancelIndicator/Sources/ChatTextInputAudioRecordingCancelIndicator.swift similarity index 91% rename from submodules/TelegramUI/Sources/ChatTextInputAudioRecordingCancelIndicator.swift rename to submodules/TelegramUI/Components/Chat/ChatTextInputAudioRecordingCancelIndicator/Sources/ChatTextInputAudioRecordingCancelIndicator.swift index 5d94685c16..fae9145d52 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputAudioRecordingCancelIndicator.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputAudioRecordingCancelIndicator/Sources/ChatTextInputAudioRecordingCancelIndicator.swift @@ -6,7 +6,7 @@ import TelegramPresentationData private let cancelFont = Font.regular(17.0) -final class ChatTextInputAudioRecordingCancelIndicator: ASDisplayNode { +public final class ChatTextInputAudioRecordingCancelIndicator: ASDisplayNode { private let cancel: () -> Void private let arrowNode: ASImageNode @@ -14,9 +14,9 @@ final class ChatTextInputAudioRecordingCancelIndicator: ASDisplayNode { private let cancelButton: HighlightableButtonNode private let strings: PresentationStrings - private(set) var isDisplayingCancel = false + public private(set) var isDisplayingCancel = false - init(theme: PresentationTheme, strings: PresentationStrings, cancel: @escaping () -> Void) { + public init(theme: PresentationTheme, strings: PresentationStrings, cancel: @escaping () -> Void) { self.cancel = cancel self.arrowNode = ASImageNode() @@ -59,7 +59,7 @@ final class ChatTextInputAudioRecordingCancelIndicator: ASDisplayNode { self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside) } - func updateTheme(theme: PresentationTheme) { + public func updateTheme(theme: PresentationTheme) { self.arrowNode.image = PresentationResourcesChat.chatInputPanelMediaRecordingCancelArrowImage(theme) self.cancelButton.setTitle(self.strings.Common_Cancel, with: cancelFont, with: theme.chat.inputPanel.panelControlAccentColor, for: []) let makeLayout = TextNode.asyncLayout(self.labelNode) @@ -67,7 +67,7 @@ final class ChatTextInputAudioRecordingCancelIndicator: ASDisplayNode { let _ = labelApply() } - func updateIsDisplayingCancel(_ isDisplayingCancel: Bool, animated: Bool) { + public func updateIsDisplayingCancel(_ isDisplayingCancel: Bool, animated: Bool) { if self.isDisplayingCancel != isDisplayingCancel { self.isDisplayingCancel = isDisplayingCancel if isDisplayingCancel { @@ -103,22 +103,20 @@ final class ChatTextInputAudioRecordingCancelIndicator: ASDisplayNode { } } - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if !self.cancelButton.alpha.isZero, self.cancelButton.frame.insetBy(dx: -5.0, dy: -5.0).contains(point) { return self.cancelButton.view } return super.hitTest(point, with: event) } - @objc func cancelPressed() { + @objc private func cancelPressed() { self.cancel() } - func animateIn() { - + public func animateIn() { } - func animateOut() { - + public func animateOut() { } } diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputAudioRecordingTimeNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatTextInputAudioRecordingTimeNode/BUILD new file mode 100644 index 0000000000..58514177fe --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputAudioRecordingTimeNode/BUILD @@ -0,0 +1,23 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatTextInputAudioRecordingTimeNode", + module_name = "ChatTextInputAudioRecordingTimeNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/ChatPresentationInterfaceState", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatTextInputAudioRecordingTimeNode.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputAudioRecordingTimeNode/Sources/ChatTextInputAudioRecordingTimeNode.swift similarity index 91% rename from submodules/TelegramUI/Sources/ChatTextInputAudioRecordingTimeNode.swift rename to submodules/TelegramUI/Components/Chat/ChatTextInputAudioRecordingTimeNode/Sources/ChatTextInputAudioRecordingTimeNode.swift index 415eba149f..3f99955fcb 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputAudioRecordingTimeNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputAudioRecordingTimeNode/Sources/ChatTextInputAudioRecordingTimeNode.swift @@ -21,7 +21,7 @@ private final class ChatTextInputAudioRecordingTimeNodeParameters: NSObject { private let textFont = Font.regular(15.0) -final class ChatTextInputAudioRecordingTimeNode: ASDisplayNode { +public final class ChatTextInputAudioRecordingTimeNode: ASDisplayNode { private let textNode: TextNode private var timestamp: Double = 0.0 { @@ -32,9 +32,9 @@ final class ChatTextInputAudioRecordingTimeNode: ASDisplayNode { private let stateDisposable = MetaDisposable() private var didStart = false - var started = {} + public var started = {} - var audioRecorder: ManagedAudioRecorder? { + public var audioRecorder: ManagedAudioRecorder? { didSet { if self.audioRecorder !== oldValue { if let audioRecorder = self.audioRecorder { @@ -65,7 +65,7 @@ final class ChatTextInputAudioRecordingTimeNode: ASDisplayNode { private var durationDisposable: MetaDisposable? - var videoRecordingStatus: InstantVideoControllerRecordingStatus? { + public var videoRecordingStatus: InstantVideoControllerRecordingStatus? { didSet { if self.videoRecordingStatus !== oldValue { if self.durationDisposable == nil { @@ -93,7 +93,7 @@ final class ChatTextInputAudioRecordingTimeNode: ASDisplayNode { private var theme: PresentationTheme - init(theme: PresentationTheme) { + public init(theme: PresentationTheme) { self.theme = theme self.textNode = TextNode() @@ -106,13 +106,13 @@ final class ChatTextInputAudioRecordingTimeNode: ASDisplayNode { self.durationDisposable?.dispose() } - func updateTheme(theme: PresentationTheme) { + public func updateTheme(theme: PresentationTheme) { self.theme = theme self.setNeedsDisplay() } - override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { + override public func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { let makeLayout = TextNode.asyncLayout(self.textNode) let (size, apply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "0:00:00,00", font: Font.regular(15.0), textColor: theme.chat.inputPanel.primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 200.0, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let _ = apply() @@ -120,7 +120,7 @@ final class ChatTextInputAudioRecordingTimeNode: ASDisplayNode { return size.size } - override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { + override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { return ChatTextInputAudioRecordingTimeNodeParameters(timestamp: self.timestamp, theme: self.theme) } diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/BUILD new file mode 100644 index 0000000000..a4ac176ca7 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/BUILD @@ -0,0 +1,66 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatTextInputPanelNode", + module_name = "ChatTextInputPanelNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/AsyncDisplayKit", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/TextFormat", + "//submodules/AccountContext", + "//submodules/TouchDownGesture", + "//submodules/ImageTransparency", + "//submodules/ActivityIndicator", + "//submodules/AnimationUI", + "//submodules/Speak", + "//submodules/ObjCRuntimeUtils", + "//submodules/AvatarNode", + "//submodules/ContextUI", + "//submodules/InvisibleInkDustNode", + "//submodules/TextInputMenu", + "//submodules/Pasteboard", + "//submodules/ChatPresentationInterfaceState", + "//submodules/ManagedAnimationNode", + "//submodules/AttachmentUI", + "//submodules/TelegramUI/Components/EditableChatTextNode", + "//submodules/TelegramUI/Components/EmojiTextAttachmentView", + "//submodules/Components/LottieAnimationComponent", + "//submodules/ComponentFlow", + "//submodules/TelegramUI/Components/EmojiSuggestionsComponent", + "//submodules/TelegramUI/Components/ChatControllerInteraction", + "//submodules/UndoUI", + "//submodules/PremiumUI", + "//submodules/StickerPeekUI", + "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/SolidRoundedButtonNode", + "//submodules/TooltipUI", + "//submodules/TelegramUI/Components/ChatTextInputMediaRecordingButton", + "//submodules/ChatContextQuery", + "//submodules/TelegramUI/Components/Chat/ChatInputTextNode", + "//submodules/TelegramUI/Components/Chat/ChatInputPanelNode", + "//submodules/TelegramNotices", + "//submodules/AnimatedCountLabelNode", + "//submodules/TelegramStringFormatting", + "//submodules/TelegramUI/Components/TextNodeWithEntities", + "//submodules/Utils/DeviceModel", + "//submodules/PhotoResources", + "//submodules/TelegramUI/Components/Chat/ChatTextInputSlowmodePlaceholderNode", + "//submodules/TelegramUI/Components/Chat/ChatTextInputActionButtonsNode", + "//submodules/TelegramUI/Components/Chat/ChatTextInputAudioRecordingTimeNode", + "//submodules/TelegramUI/Components/Chat/ChatTextInputAudioRecordingCancelIndicator", + "//submodules/TelegramUI/Components/Chat/ChatRecordingViewOnceButtonNode", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift similarity index 96% rename from submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift rename to submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift index fa0851362f..01e364ea98 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift @@ -47,6 +47,26 @@ import TelegramStringFormatting import TextNodeWithEntities import DeviceModel import PhotoResources +import ChatTextInputSlowmodePlaceholderNode +import ChatTextInputActionButtonsNode +import ChatTextInputAudioRecordingTimeNode +import ChatTextInputAudioRecordingCancelIndicator +import ChatRecordingViewOnceButtonNode + +public protocol ChatRecordingPreviewInputPanelNode: ASDisplayNode { + var viewOnceButton: ChatRecordingViewOnceButtonNode { get } + var recordMoreButton: ChatRecordingViewOnceButtonNode { get } + var waveformBackgroundNode: ASDisplayNode { get } + var waveformScrubberNode: ASDisplayNode { get } + var scrubber: ComponentView { get } + var binNode: AnimationNode { get } + var deleteButton: ASDisplayNode { get } + var sendButton: ASDisplayNode { get } + var playButtonNode: ASDisplayNode { get } + var trimView: UIView { get } + var viewOnce: Bool { get } + var gestureRecognizer: ContextGesture? { get } +} private let accessoryButtonFont = Font.medium(14.0) private let counterFont = Font.with(size: 14.0, design: .regular, traits: [.monospacedNumbers]) @@ -344,7 +364,7 @@ private final class AccessoryItemIconButtonNode: HighlightTrackingButtonNode { } } -let chatTextInputMinFontSize: CGFloat = 5.0 +public let chatTextInputMinFontSize: CGFloat = 5.0 private let minInputFontSize = chatTextInputMinFontSize @@ -431,7 +451,7 @@ private func textInputBackgroundImage(backgroundColor: UIColor?, inputBackground } } -enum ChatTextInputPanelPasteData { +public enum ChatTextInputPanelPasteData { case images([UIImage]) case video(Data) case gif(Data) @@ -529,27 +549,27 @@ private func makeTextInputTheme(context: AccountContext, interfaceState: ChatPre ) } -class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, ChatInputTextNodeDelegate { - let clippingNode: ASDisplayNode - var textPlaceholderNode: ImmediateTextNodeWithEntities - var textLockIconNode: ASImageNode? - var contextPlaceholderNode: TextNode? - var slowmodePlaceholderNode: ChatTextInputSlowmodePlaceholderNode? - let textInputContainerBackgroundNode: ASImageNode - let textInputContainer: ASDisplayNode - var textInputNode: ChatInputTextNode? - var dustNode: InvisibleInkDustNode? - var customEmojiContainerView: CustomEmojiContainerView? +public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, ChatInputTextNodeDelegate { + public let clippingNode: ASDisplayNode + public var textPlaceholderNode: ImmediateTextNodeWithEntities + public var textLockIconNode: ASImageNode? + public var contextPlaceholderNode: TextNode? + public var slowmodePlaceholderNode: ChatTextInputSlowmodePlaceholderNode? + public let textInputContainerBackgroundNode: ASImageNode + public let textInputContainer: ASDisplayNode + public var textInputNode: ChatInputTextNode? + public var dustNode: InvisibleInkDustNode? + public var customEmojiContainerView: CustomEmojiContainerView? - let textInputBackgroundNode: ASImageNode - var textInputBackgroundTapRecognizer: TouchDownGestureRecognizer? + public let textInputBackgroundNode: ASImageNode + public var textInputBackgroundTapRecognizer: TouchDownGestureRecognizer? private var transparentTextInputBackgroundImage: UIImage? - let actionButtons: ChatTextInputActionButtonsNode + public let actionButtons: ChatTextInputActionButtonsNode private let slowModeButton: BoostSlowModeButton - var mediaRecordingAccessibilityArea: AccessibilityAreaNode? + public var mediaRecordingAccessibilityArea: AccessibilityAreaNode? private let counterTextNode: ImmediateTextNode - let menuButton: HighlightTrackingButtonNode + public let menuButton: HighlightTrackingButtonNode private let menuButtonBackgroundNode: ASDisplayNode private let menuButtonClippingNode: ASDisplayNode private let menuButtonIconNode: MenuIconNode @@ -557,28 +577,28 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch private let startButton: SolidRoundedButtonNode - let sendAsAvatarButtonNode: HighlightableButtonNode - let sendAsAvatarReferenceNode: ContextReferenceContentNode - let sendAsAvatarContainerNode: ContextControllerSourceNode + public let sendAsAvatarButtonNode: HighlightableButtonNode + public let sendAsAvatarReferenceNode: ContextReferenceContentNode + public let sendAsAvatarContainerNode: ContextControllerSourceNode private let sendAsAvatarNode: AvatarNode - let attachmentButton: HighlightableButtonNode - let attachmentButtonDisabledNode: HighlightableButtonNode + public let attachmentButton: HighlightableButtonNode + public let attachmentButtonDisabledNode: HighlightableButtonNode - var attachmentImageNode: TransformImageNode? + public var attachmentImageNode: TransformImageNode? - let searchLayoutClearButton: HighlightableButton + public let searchLayoutClearButton: HighlightableButton private let searchLayoutClearImageNode: ASImageNode private var searchActivityIndicator: ActivityIndicator? - var audioRecordingInfoContainerNode: ASDisplayNode? - var audioRecordingDotNode: AnimationNode? - var audioRecordingDotNodeDismissed = false - var audioRecordingTimeNode: ChatTextInputAudioRecordingTimeNode? - var audioRecordingCancelIndicator: ChatTextInputAudioRecordingCancelIndicator? - var animatingBinNode: AnimationNode? + public var audioRecordingInfoContainerNode: ASDisplayNode? + public var audioRecordingDotNode: AnimationNode? + public var audioRecordingDotNodeDismissed = false + public var audioRecordingTimeNode: ChatTextInputAudioRecordingTimeNode? + public var audioRecordingCancelIndicator: ChatTextInputAudioRecordingCancelIndicator? + public var animatingBinNode: AnimationNode? - var viewOnce = false - let viewOnceButton: ChatRecordingViewOnceButtonNode + public var viewOnce = false + public let viewOnceButton: ChatRecordingViewOnceButtonNode private var accessoryItemButtons: [(ChatTextInputAccessoryItem, AccessoryItemIconButtonNode)] = [] @@ -587,14 +607,14 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch private var rightSlowModeInset: CGFloat = 0.0 private var currentTextInputBackgroundWidthOffset: CGFloat = 0.0 - var displayAttachmentMenu: () -> Void = { } - var sendMessage: () -> Void = { } - var paste: (ChatTextInputPanelPasteData) -> Void = { _ in } - var updateHeight: (Bool) -> Void = { _ in } - var toggleExpandMediaInput: (() -> Void)? - var switchToTextInputIfNeeded: (() -> Void)? + public var displayAttachmentMenu: () -> Void = { } + public var sendMessage: () -> Void = { } + public var paste: (ChatTextInputPanelPasteData) -> Void = { _ in } + public var updateHeight: (Bool) -> Void = { _ in } + public var toggleExpandMediaInput: (() -> Void)? + public var switchToTextInputIfNeeded: (() -> Void)? - var updateActivity: () -> Void = { } + public var updateActivity: () -> Void = { } private var updatingInputState = false @@ -607,7 +627,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch private var keepSendButtonEnabled = false private var extendedSearchLayout = false - var isMediaDeleted: Bool = false + public var isMediaDeleted: Bool = false private var recordingPaused = false private let inputMenu: TextInputMenu @@ -617,7 +637,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch private let hapticFeedback = HapticFeedback() - var inputTextState: ChatTextInputState { + public var inputTextState: ChatTextInputState { if let textInputNode = self.textInputNode { let selectionRange: Range = textInputNode.selectedRange.location ..< (textInputNode.selectedRange.location + textInputNode.selectedRange.length) return ChatTextInputState(inputText: stateAttributedStringForText(textInputNode.attributedText ?? NSAttributedString()), selectionRange: selectionRange) @@ -626,8 +646,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } } - var storedInputLanguage: String? - var effectiveInputLanguage: String? { + public var storedInputLanguage: String? + public var effectiveInputLanguage: String? { if let textInputNode = textInputNode, textInputNode.isFirstResponder() { return textInputNode.textInputMode?.primaryLanguage } else { @@ -635,7 +655,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } } - var enablePredictiveInput: Bool = true { + public var enablePredictiveInput: Bool = true { didSet { if let textInputNode = self.textInputNode { textInputNode.textView.autocorrectionType = self.enablePredictiveInput ? .default : .no @@ -643,18 +663,18 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } } - override var context: AccountContext? { + override public var context: AccountContext? { didSet { self.actionButtons.micButton.statusBarHost = self.context?.sharedContext.mainWindow?.statusBarHost } } - var micButton: ChatTextInputMediaRecordingButton? { + public var micButton: ChatTextInputMediaRecordingButton? { return self.actionButtons.micButton } private let statusDisposable = MetaDisposable() - override var interfaceInteraction: ChatPanelInterfaceInteraction? { + override public var interfaceInteraction: ChatPanelInterfaceInteraction? { didSet { if let statuses = self.interfaceInteraction?.statuses { self.statusDisposable.set((statuses.inlineSearch @@ -666,7 +686,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } } - func updateInputTextState(_ state: ChatTextInputState, keepSendButtonEnabled: Bool, extendedSearchLayout: Bool, accessoryItems: [ChatTextInputAccessoryItem], animated: Bool) { + public func updateInputTextState(_ state: ChatTextInputState, keepSendButtonEnabled: Bool, extendedSearchLayout: Bool, accessoryItems: [ChatTextInputAccessoryItem], animated: Bool) { if let currentState = self.presentationInterfaceState { var updateAccessoryButtons = false if accessoryItems.count == self.accessoryItemButtons.count { @@ -740,7 +760,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } } - func updateKeepSendButtonEnabled(keepSendButtonEnabled: Bool, extendedSearchLayout: Bool, animated: Bool) { + public func updateKeepSendButtonEnabled(keepSendButtonEnabled: Bool, extendedSearchLayout: Bool, animated: Bool) { if keepSendButtonEnabled != self.keepSendButtonEnabled || extendedSearchLayout != self.extendedSearchLayout { self.keepSendButtonEnabled = keepSendButtonEnabled self.extendedSearchLayout = extendedSearchLayout @@ -748,7 +768,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } } - var text: String { + public var text: String { get { return self.textInputNode?.attributedText?.string ?? "" } set(value) { @@ -772,17 +792,17 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch private var spoilersRevealed = false private var animatingTransition = false - var finishedTransitionToPreview: Bool? + public var finishedTransitionToPreview: Bool? private var touchDownGestureRecognizer: TouchDownGestureRecognizer? - var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)? + public var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)? private let presentationContext: ChatPresentationContext? private var tooltipController: TooltipScreen? - init(context: AccountContext, presentationInterfaceState: ChatPresentationInterfaceState, presentationContext: ChatPresentationContext?, presentController: @escaping (ViewController) -> Void) { + public init(context: AccountContext, presentationInterfaceState: ChatPresentationInterfaceState, presentationContext: ChatPresentationContext?, presentController: @escaping (ViewController) -> Void) { self.presentationInterfaceState = presentationInterfaceState self.presentationContext = presentationContext @@ -1070,7 +1090,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch recognizer.touchDown = { [weak self] in if let strongSelf = self { if strongSelf.sendingTextDisabled { - guard let controller = strongSelf.interfaceInteraction?.chatController() as? ChatControllerImpl else { + guard let controller = (strongSelf.interfaceInteraction?.chatController() as? ChatController) else { return } @@ -1079,7 +1099,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch return } - controller.controllerInteraction?.displayUndo(.universal(animation: "premium_unlock", scale: 1.0, colors: ["__allcolors__": UIColor(white: 1.0, alpha: 1.0)], title: nil, text: controller.restrictedSendingContentsText(), customUndoText: nil, timeout: nil)) + strongSelf.interfaceInteraction?.displayUndo(.universal(animation: "premium_unlock", scale: 1.0, colors: ["__allcolors__": UIColor(white: 1.0, alpha: 1.0)], title: nil, text: controller.restrictedSendingContentsText(), customUndoText: nil, timeout: nil)) } else { strongSelf.ensureFocused() } @@ -1114,7 +1134,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch self.viewOnceButton.addTarget(self, action: #selector(self.viewOncePressed), forControlEvents: [.touchUpInside]) } - required init?(coder aDecoder: NSCoder) { + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -1124,7 +1144,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch self.currentEmojiSuggestion?.disposable.dispose() } - override func didLoad() { + override public func didLoad() { super.didLoad() if let viewForOverlayContent = self.viewForOverlayContent { @@ -1132,7 +1152,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } } - func loadTextInputNodeIfNeeded() { + public func loadTextInputNodeIfNeeded() { if self.textInputNode == nil { self.loadTextInputNode() } @@ -1360,7 +1380,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch return result } - override func minimalHeight(interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { + override public func minimalHeight(interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { let textFieldMinHeight = calclulateTextFieldMinHeight(interfaceState, metrics: metrics) var minimalHeight: CGFloat = 14.0 + textFieldMinHeight if case .regular = metrics.widthClass, case .regular = metrics.heightClass { @@ -1413,7 +1433,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch transition.updatePosition(layer: menuTextSnapshotView.layer, position: CGPoint(x: menuTextSnapshotView.center.x + menuContentDelta, y: self.startButton.position.y)) } - func animateBotButtonOutToMenu(transition: ContainedViewLayoutTransition) { + public func animateBotButtonOutToMenu(transition: ContainedViewLayoutTransition) { guard !self.animatingTransition else { return } @@ -1473,7 +1493,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } private var absoluteRect: (CGRect, CGSize)? - override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition) { + override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition) { self.absoluteRect = (rect, containerSize) if !self.actionButtons.frame.width.isZero { @@ -1488,14 +1508,14 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } } - func requestLayout(transition: ContainedViewLayoutTransition = .immediate) { + public func requestLayout(transition: ContainedViewLayoutTransition = .immediate) { guard let presentationInterfaceState = self.presentationInterfaceState, let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, metrics, isSecondary, isMediaInputExpanded) = self.validLayout else { return } let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: transition, interfaceState: presentationInterfaceState, metrics: metrics, isMediaInputExpanded: isMediaInputExpanded) } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat { + override public func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat { let previousAdditionalSideInsets = self.validLayout?.4 self.validLayout = (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, metrics, isSecondary, isMediaInputExpanded) @@ -3034,11 +3054,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch parentController.present(tooltipController, in: .current) } - override func canHandleTransition(from prevInputPanelNode: ChatInputPanelNode?) -> Bool { + override public func canHandleTransition(from prevInputPanelNode: ChatInputPanelNode?) -> Bool { return prevInputPanelNode is ChatRecordingPreviewInputPanelNode } - func chatInputTextNodeDidUpdateText() { + public func chatInputTextNodeDidUpdateText() { if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState, let context = self.context { let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) refreshChatTextInputAttributes(context: context, textView: textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: (self.context?.animatedEmojiStickersValue.keys).flatMap(Set.init) ?? Set(), emojiViewProvider: self.emojiViewProvider, makeCollapsedQuoteAttachment: { text, attributes in @@ -3058,7 +3078,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } } - @objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { + @objc public func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { self.chatInputTextNodeDidUpdateText() } @@ -3642,9 +3662,6 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch guard let self else { return } - guard let controller = (self.interfaceInteraction?.chatController() as? ChatControllerImpl) else { - return - } var text = "." var emojiAttribute: ChatTextInputTextCustomEmojiAttribute? @@ -3665,32 +3682,22 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } if let emojiAttribute { - controller.controllerInteraction?.sendEmoji(text, emojiAttribute, true) + self.interfaceInteraction?.sendEmoji(text, emojiAttribute, true) } } let setStatus: (TelegramMediaFile) -> Void = { file in guard let self, let context = self.context else { return } - guard let controller = (self.interfaceInteraction?.chatController() as? ChatControllerImpl) else { - return - } let _ = context.engine.accountData.setEmojiStatus(file: file, expirationDate: nil).startStandalone() var animateInAsReplacement = false animateInAsReplacement = false - /*if let currentUndoOverlayController = strongSelf.currentUndoOverlayController { - currentUndoOverlayController.dismissWithCommitActionAndReplacementAnimation() - strongSelf.currentUndoOverlayController = nil - animateInAsReplacement = true - }*/ - let presentationData = context.sharedContext.currentPresentationData.with { $0 } let undoController = UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, loop: true, title: nil, text: presentationData.strings.EmojiStatus_AppliedText, undoText: nil, customAction: nil), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { _ in return false }) - //strongSelf.currentUndoOverlayController = controller - controller.controllerInteraction?.presentController(undoController, nil) + strongSelf.interfaceInteraction?.presentController(undoController, nil) } let copyEmoji: (TelegramMediaFile) -> Void = { file in var text = "." @@ -4023,7 +4030,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } } - func updateIsProcessingInlineRequest(_ value: Bool) { + public func updateIsProcessingInlineRequest(_ value: Bool) { if value { if self.searchActivityIndicator == nil, let currentState = self.presentationInterfaceState { let searchActivityIndicator = ActivityIndicator(type: .custom(currentState.theme.list.itemAccentColor, 20.0, 1.5, true)) @@ -4042,14 +4049,14 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } } - func chatInputTextNodeShouldReturn() -> Bool { + public func chatInputTextNodeShouldReturn() -> Bool { if self.actionButtons.sendButton.supernode != nil && !self.actionButtons.sendButton.isHidden && !self.actionButtons.sendContainerNode.alpha.isZero { self.sendButtonPressed() } return false } - @objc func editableTextNodeShouldReturn(_ editableTextNode: ASEditableTextNode) -> Bool { + @objc public func editableTextNodeShouldReturn(_ editableTextNode: ASEditableTextNode) -> Bool { return self.chatInputTextNodeShouldReturn() } @@ -4082,7 +4089,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } } - func chatInputTextNodeDidChangeSelection(dueToEditing: Bool) { + public func chatInputTextNodeDidChangeSelection(dueToEditing: Bool) { if !dueToEditing && !self.updatingInputState { let inputTextState = self.inputTextState self.interfaceInteraction?.updateTextInputStateAndMode({ _, inputMode in return (inputTextState, inputMode) }) @@ -4102,11 +4109,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } } - @objc func editableTextNodeDidChangeSelection(_ editableTextNode: ASEditableTextNode, fromSelectedRange: NSRange, toSelectedRange: NSRange, dueToEditing: Bool) { + @objc public func editableTextNodeDidChangeSelection(_ editableTextNode: ASEditableTextNode, fromSelectedRange: NSRange, toSelectedRange: NSRange, dueToEditing: Bool) { self.chatInputTextNodeDidChangeSelection(dueToEditing: dueToEditing) } - func chatInputTextNodeDidBeginEditing() { + public func chatInputTextNodeDidBeginEditing() { guard let interfaceInteraction = self.interfaceInteraction, let presentationInterfaceState = self.presentationInterfaceState else { return } @@ -4131,12 +4138,12 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } } - @objc func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) { + @objc public func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) { self.chatInputTextNodeDidBeginEditing() } - var skipPresentationInterfaceStateUpdate = false - func chatInputTextNodeDidFinishEditing() { + public var skipPresentationInterfaceStateUpdate = false + public func chatInputTextNodeDidFinishEditing() { guard let editableTextNode = self.textInputNode else { return } @@ -4169,14 +4176,14 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } } - func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) { + public func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) { self.chatInputTextNodeDidFinishEditing() } - func chatInputTextNodeBackspaceWhileEmpty() { + public func chatInputTextNodeBackspaceWhileEmpty() { } - func editableTextNodeTarget(forAction action: Selector) -> ASEditableTextNodeTargetForAction? { + public func editableTextNodeTarget(forAction action: Selector) -> ASEditableTextNodeTargetForAction? { if action == makeSelectorFromString("_accessibilitySpeak:") { if case .format = self.inputMenu.state { return ASEditableTextNodeTargetForAction(target: nil) @@ -4257,7 +4264,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch var suggestedActionCounter: Int = 0 @available(iOS 13.0, *) - func chatInputTextNodeMenu(forTextRange textRange: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu { + public func chatInputTextNodeMenu(forTextRange textRange: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu { guard let editableTextNode = self.textInputNode else { return UIMenu(children: []) } @@ -4345,7 +4352,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } @available(iOS 16.0, *) - func editableTextNodeMenu(_ editableTextNode: ASEditableTextNode, forTextRange textRange: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu { + public func editableTextNodeMenu(_ editableTextNode: ASEditableTextNode, forTextRange textRange: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu { return chatInputTextNodeMenu(forTextRange: textRange, suggestedActions: suggestedActions) } @@ -4455,7 +4462,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch self.updateSpoilersRevealed(animated: animated) } - func chatInputTextNode(shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + public func chatInputTextNode(shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { guard let editableTextNode = self.textInputNode, let context = self.context else { return false } @@ -4495,11 +4502,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch return true } - @objc func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + @objc public func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { return self.chatInputTextNode(shouldChangeTextIn: range, replacementText: text) } - func chatInputTextNodeShouldCopy() -> Bool { + public func chatInputTextNodeShouldCopy() -> Bool { self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in storeInputTextInPasteboard(current.inputText.attributedSubstring(from: NSMakeRange(current.selectionRange.lowerBound, current.selectionRange.count))) return (current, inputMode) @@ -4507,7 +4514,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch return false } - @objc func editableTextNodeShouldCopy(_ editableTextNode: ASEditableTextNode) -> Bool { + @objc public func editableTextNodeShouldCopy(_ editableTextNode: ASEditableTextNode) -> Bool { return self.chatInputTextNodeShouldCopy() } @@ -4523,7 +4530,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } } - func chatInputTextNodeShouldPaste() -> Bool { + public func chatInputTextNodeShouldPaste() -> Bool { let pasteboard = UIPasteboard.general var attributedString: NSAttributedString? @@ -4603,7 +4610,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch return true } - @objc func editableTextNodeShouldPaste(_ editableTextNode: ASEditableTextNode) -> Bool { + @objc public func editableTextNodeShouldPaste(_ editableTextNode: ASEditableTextNode) -> Bool { return self.chatInputTextNodeShouldPaste() } @@ -4688,15 +4695,15 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } } - var isFocused: Bool { + public var isFocused: Bool { return self.textInputNode?.isFirstResponder() ?? false } - func ensureUnfocused() { + public func ensureUnfocused() { self.textInputNode?.resignFirstResponder() } - func ensureFocused() { + public func ensureFocused() { if self.sendingTextDisabled { return } @@ -4711,7 +4718,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } private var switching = false - func ensureFocusedOnTap() { + public func ensureFocusedOnTap() { if self.textInputNode == nil { self.loadTextInputNode() } @@ -4725,7 +4732,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } } - func backwardsDeleteText() { + public func backwardsDeleteText() { guard let textInputNode = self.textInputNode else { return } @@ -4788,7 +4795,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } } - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if let audioRecordingCancelIndicator = self.audioRecordingCancelIndicator { if let result = audioRecordingCancelIndicator.hitTest(point.offsetBy(dx: -audioRecordingCancelIndicator.frame.minX, dy: -audioRecordingCancelIndicator.frame.minY), with: event) { return result @@ -4807,7 +4814,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch return result } - func frameForAccessoryButton(_ item: ChatTextInputAccessoryItem) -> CGRect? { + public func frameForAccessoryButton(_ item: ChatTextInputAccessoryItem) -> CGRect? { for (buttonItem, buttonNode) in self.accessoryItemButtons { if buttonItem == item { return buttonNode.frame @@ -4816,21 +4823,21 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch return nil } - func frameForAttachmentButton() -> CGRect? { + public func frameForAttachmentButton() -> CGRect? { if !self.attachmentButton.alpha.isZero { return self.attachmentButton.frame.insetBy(dx: 0.0, dy: 6.0).offsetBy(dx: 2.0, dy: 0.0) } return nil } - func frameForMenuButton() -> CGRect? { + public func frameForMenuButton() -> CGRect? { if !self.menuButton.alpha.isZero { return self.menuButton.frame } return nil } - func frameForInputActionButton() -> CGRect? { + public func frameForInputActionButton() -> CGRect? { if !self.actionButtons.alpha.isZero { if self.actionButtons.micButton.alpha.isZero { return self.actionButtons.frame.insetBy(dx: 0.0, dy: 6.0).offsetBy(dx: 4.0, dy: 0.0) @@ -4841,7 +4848,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch return nil } - func frameForStickersButton() -> CGRect? { + public func frameForStickersButton() -> CGRect? { for (item, button) in self.accessoryItemButtons { if case let .input(_, inputMode) = item, case .stickers = inputMode { return button.frame.insetBy(dx: 0.0, dy: 6.0) @@ -4850,7 +4857,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch return nil } - func frameForEmojiButton() -> CGRect? { + public func frameForEmojiButton() -> CGRect? { for (item, button) in self.accessoryItemButtons { if case let .input(_, inputMode) = item, case .emoji = inputMode { return button.frame.insetBy(dx: 0.0, dy: 6.0) @@ -4859,7 +4866,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch return nil } - func frameForGiftButton() -> CGRect? { + public func frameForGiftButton() -> CGRect? { for (item, button) in self.accessoryItemButtons { if case .gift = item { return button.frame.insetBy(dx: 0.0, dy: 6.0) @@ -4868,7 +4875,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch return nil } - func makeSnapshotForTransition() -> ChatMessageTransitionNodeImpl.Source.TextInput? { + public func makeSnapshotForTransition() -> (backgroundView: UIView, contentView: UIView, sourceRect: CGRect, scrollOffset: CGFloat)? { guard let backgroundImage = self.transparentTextInputBackgroundImage else { return nil } @@ -4891,7 +4898,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch contentView.frame = textInputNode.frame - return ChatMessageTransitionNodeImpl.Source.TextInput( + return ( backgroundView: backgroundView, contentView: contentView, sourceRect: self.view.convert(self.bounds, to: nil), @@ -4899,7 +4906,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch ) } - func makeAttachmentMenuTransition(accessoryPanelNode: ASDisplayNode?) -> AttachmentController.InputPanelTransition { + public func makeAttachmentMenuTransition(accessoryPanelNode: ASDisplayNode?) -> AttachmentController.InputPanelTransition { return AttachmentController.InputPanelTransition(inputNode: self, accessoryPanelNode: accessoryPanelNode, menuButtonNode: self.menuButton, menuButtonBackgroundNode: self.menuButtonBackgroundNode, menuIconNode: self.menuButtonIconNode, menuTextNode: self.menuButtonTextNode, prepareForDismiss: { self.menuButtonIconNode.enqueueState(.app, animated: false) }) } } diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputSlowmodePlaceholderNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatTextInputSlowmodePlaceholderNode/BUILD new file mode 100644 index 0000000000..6d5e1e870e --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputSlowmodePlaceholderNode/BUILD @@ -0,0 +1,24 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatTextInputSlowmodePlaceholderNode", + module_name = "ChatTextInputSlowmodePlaceholderNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramPresentationData", + "//submodules/TelegramStringFormatting", + "//submodules/AppBundle", + "//submodules/ChatPresentationInterfaceState", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatTextInputSlowmodePlaceholderNode.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputSlowmodePlaceholderNode/Sources/ChatTextInputSlowmodePlaceholderNode.swift similarity index 95% rename from submodules/TelegramUI/Sources/ChatTextInputSlowmodePlaceholderNode.swift rename to submodules/TelegramUI/Components/Chat/ChatTextInputSlowmodePlaceholderNode/Sources/ChatTextInputSlowmodePlaceholderNode.swift index 09a9c63300..77fc9d6a5c 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputSlowmodePlaceholderNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputSlowmodePlaceholderNode/Sources/ChatTextInputSlowmodePlaceholderNode.swift @@ -8,7 +8,7 @@ import TelegramStringFormatting import AppBundle import ChatPresentationInterfaceState -final class ChatTextInputSlowmodePlaceholderNode: ASDisplayNode { +public final class ChatTextInputSlowmodePlaceholderNode: ASDisplayNode { private var theme: PresentationTheme private let iconNode: ASImageNode private let iconArrowContainerNode: ASDisplayNode @@ -20,7 +20,7 @@ final class ChatTextInputSlowmodePlaceholderNode: ASDisplayNode { private var timer: SwiftSignalKit.Timer? - init(theme: PresentationTheme) { + public init(theme: PresentationTheme) { self.theme = theme self.textNode = ImmediateTextNode() self.textNode.displaysAsynchronously = false @@ -52,7 +52,7 @@ final class ChatTextInputSlowmodePlaceholderNode: ASDisplayNode { self.timer?.invalidate() } - func updateState(_ slowmodeState: ChatSlowmodeState) { + public func updateState(_ slowmodeState: ChatSlowmodeState) { if self.slowmodeState != slowmodeState { self.slowmodeState = slowmodeState self.update() @@ -97,7 +97,7 @@ final class ChatTextInputSlowmodePlaceholderNode: ASDisplayNode { } } - func updateLayout(size: CGSize) { + public func updateLayout(size: CGSize) { self.validLayout = size var leftInset: CGFloat = 0.0 diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 5627e06ed1..46e99a72a3 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -439,6 +439,8 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode { }, updateRecordingTrimRange: { _, _, _, _ in }, dismissAllTooltips: { }, editTodoMessage: { _, _, _ in + }, displayUndo: { _ in + }, sendEmoji: { _, _, _ in }, updateHistoryFilter: { _ in }, updateChatLocationThread: { _, _ in }, toggleChatSidebarMode: { diff --git a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift index a151b5416f..81a60d5bfd 100644 --- a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift @@ -826,6 +826,8 @@ final class PeerSelectionControllerNode: ASDisplayNode { }, updateRecordingTrimRange: { _, _, _, _ in }, dismissAllTooltips: { }, editTodoMessage: { _, _, _ in + }, displayUndo: { _ in + }, sendEmoji: { _, _, _ in }, updateHistoryFilter: { _ in }, updateChatLocationThread: { _, _ in }, toggleChatSidebarMode: { diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift index a0ea346e17..4921df6ae2 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -4340,6 +4340,16 @@ extension ChatControllerImpl { return } self.openTodoEditing(messageId: messageId, itemId: itemId, append: append) + }, displayUndo: { [weak self] content in + guard let self else { + return + } + self.controllerInteraction?.displayUndo(content) + }, sendEmoji: { [weak self] text, attribute, immediately in + guard let self else { + return + } + self.controllerInteraction?.sendEmoji(text, attribute, immediately) }, updateHistoryFilter: { [weak self] update in guard let self else { return diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 70c2e024cd..246ca78a4b 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -9961,7 +9961,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G transition.updateSublayerTransformScale(node: self.chatDisplayNode.historyNode, scale: scale) } - func restrictedSendingContentsText() -> String { + public func restrictedSendingContentsText() -> String { guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { return self.presentationData.strings.Chat_SendNotAllowedText } diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 4666344780..36d12919ee 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -47,6 +47,7 @@ import ChatEmptyNode import SpaceWarpView import ChatSideTopicsPanel import ChatThemeScreen +import ChatTextInputPanelNode final class VideoNavigationControllerDropContentItem: NavigationControllerDropContentItem { let itemNode: OverlayMediaItemNode @@ -3969,7 +3970,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } func sendButtonFrame() -> CGRect? { - if let mediaPreviewNode = self.inputPanelNode as? ChatRecordingPreviewInputPanelNode { + if let mediaPreviewNode = self.inputPanelNode as? ChatRecordingPreviewInputPanelNodeImpl { return mediaPreviewNode.convert(mediaPreviewNode.sendButton.frame, to: self) } else if let frame = self.textInputPanelNode?.actionButtons.frame { return self.textInputPanelNode?.convert(frame, to: self) @@ -4012,7 +4013,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { return textInputPanelNode.frameForInputActionButton().flatMap { return $0.offsetBy(dx: textInputPanelNode.frame.minX, dy: textInputPanelNode.frame.minY) } - } else if let recordingPreviewPanelNode = self.inputPanelNode as? ChatRecordingPreviewInputPanelNode { + } else if let recordingPreviewPanelNode = self.inputPanelNode as? ChatRecordingPreviewInputPanelNodeImpl { return recordingPreviewPanelNode.frameForInputActionButton().flatMap { return $0.offsetBy(dx: recordingPreviewPanelNode.frame.minX, dy: recordingPreviewPanelNode.frame.minY) } @@ -4761,7 +4762,12 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } if self.shouldAnimateMessageTransition, let inputPanelNode = self.inputPanelNode as? ChatTextInputPanelNode, let textInput = inputPanelNode.makeSnapshotForTransition() { usedCorrelationId = correlationId - let source: ChatMessageTransitionNodeImpl.Source = .textInput(textInput: textInput, replyPanel: replyPanel) + let source: ChatMessageTransitionNodeImpl.Source = .textInput(textInput: ChatMessageTransitionNodeImpl.Source.TextInput( + backgroundView: textInput.backgroundView, + contentView: textInput.contentView, + sourceRect: textInput.sourceRect, + scrollOffset: textInput.scrollOffset + ), replyPanel: replyPanel) self.messageTransitionNode.add(correlationId: correlationId, source: source, initiated: { }) } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift b/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift index 9567217139..997524d0e8 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift @@ -10,6 +10,7 @@ import ChatPresentationInterfaceState import SwiftSignalKit import TextFormat import ChatContextQuery +import ChatTextInputPanelNode func serviceTasksForChatPresentationIntefaceState(context: AccountContext, chatPresentationInterfaceState: ChatPresentationInterfaceState, updateState: @escaping ((ChatPresentationInterfaceState) -> ChatPresentationInterfaceState) -> Void) -> [AnyHashable: () -> Disposable] { var missingEmoji = Set() diff --git a/submodules/TelegramUI/Sources/ChatInterfaceInputNodes.swift b/submodules/TelegramUI/Sources/ChatInterfaceInputNodes.swift index fcaad73f80..68e8f2aa23 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceInputNodes.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceInputNodes.swift @@ -10,6 +10,7 @@ import ChatInputNode import ChatEntityKeyboardInputNode import ChatInputPanelNode import ChatButtonKeyboardInputNode +import ChatTextInputPanelNode func inputNodeForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, currentNode: ChatInputNode?, interfaceInteraction: ChatPanelInterfaceInteraction?, controllerInteraction: ChatControllerInteraction, inputPanelNode: ChatInputPanelNode?, makeMediaInputNode: () -> ChatInputNode?) -> ChatInputNode? { if let inputPanelNode = inputPanelNode, !(inputPanelNode is ChatTextInputPanelNode) { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift index 2e3a1c482f..42d04e1903 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift @@ -8,6 +8,7 @@ import ChatInputPanelNode import ChatBotStartInputPanelNode import ChatChannelSubscriberInputPanelNode import ChatMessageSelectionInputPanelNode +import ChatTextInputPanelNode func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, currentPanel: ChatInputPanelNode?, currentSecondaryPanel: ChatInputPanelNode?, textInputPanelNode: ChatTextInputPanelNode?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> (primary: ChatInputPanelNode?, secondary: ChatInputPanelNode?) { if let renderedPeer = chatPresentationInterfaceState.renderedPeer, renderedPeer.peer?.restrictionText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) != nil { @@ -417,10 +418,10 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState } } else { if let _ = chatPresentationInterfaceState.interfaceState.mediaDraftState { - if let currentPanel = (currentPanel as? ChatRecordingPreviewInputPanelNode) ?? (currentSecondaryPanel as? ChatRecordingPreviewInputPanelNode) { + if let currentPanel = (currentPanel as? ChatRecordingPreviewInputPanelNodeImpl) ?? (currentSecondaryPanel as? ChatRecordingPreviewInputPanelNodeImpl) { return (currentPanel, nil) } else { - let panel = ChatRecordingPreviewInputPanelNode(theme: chatPresentationInterfaceState.theme) + let panel = ChatRecordingPreviewInputPanelNodeImpl(theme: chatPresentationInterfaceState.theme) panel.context = context panel.interfaceInteraction = interfaceInteraction return (panel, nil) diff --git a/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift index 13204771c7..959b4254be 100644 --- a/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift @@ -20,6 +20,8 @@ import TelegramNotices import ComponentFlow import MediaScrubberComponent import AnimatedCountLabelNode +import ChatTextInputPanelNode +import ChatRecordingViewOnceButtonNode #if SWIFT_PACKAGE extension AudioWaveformNode: CustomMediaPlayerScrubbingForegroundNode { @@ -127,20 +129,35 @@ final class PlayButtonNode: ASDisplayNode { } } -final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { - let deleteButton: HighlightableButtonNode +final class ChatRecordingPreviewInputPanelNodeImpl: ChatInputPanelNode, ChatRecordingPreviewInputPanelNode { + let deleteButtonImpl: HighlightableButtonNode + var deleteButton: ASDisplayNode { + return self.deleteButtonImpl + } let binNode: AnimationNode - let sendButton: HighlightTrackingButtonNode + let sendButtonImpl: HighlightTrackingButtonNode + var sendButton: ASDisplayNode { + return self.sendButtonImpl + } let sendBackgroundNode: ASDisplayNode let sendIconNode: ASImageNode let textNode: ImmediateAnimatedCountLabelNode private var sendButtonRadialStatusNode: ChatSendButtonRadialStatusNode? private let waveformButton: ASButtonNode - let waveformBackgroundNode: ASImageNode + let waveformBackgroundNodeImpl: ASImageNode + var waveformBackgroundNode: ASDisplayNode { + return self.waveformBackgroundNodeImpl + } - let trimView: TrimView - let playButtonNode: PlayButtonNode + let trimViewImpl: TrimView + var trimView: UIView { + return self.trimViewImpl + } + let playButtonNodeImpl: PlayButtonNode + var playButtonNode: ASDisplayNode { + return self.playButtonNodeImpl + } let scrubber = ComponentView() @@ -150,7 +167,10 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { private let waveformNode: AudioWaveformNode private let waveformForegroundNode: AudioWaveformNode - let waveformScrubberNode: MediaPlayerScrubbingNode + let waveformScrubberNodeImpl: MediaPlayerScrubbingNode + var waveformScrubberNode: ASDisplayNode { + return self.waveformScrubberNodeImpl + } private var presentationInterfaceState: ChatPresentationInterfaceState? @@ -165,8 +185,8 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { private(set) var gestureRecognizer: ContextGesture? init(theme: PresentationTheme) { - self.deleteButton = HighlightableButtonNode() - self.deleteButton.displaysAsynchronously = false + self.deleteButtonImpl = HighlightableButtonNode() + self.deleteButtonImpl.displaysAsynchronously = false self.binNode = AnimationNode( animation: "BinBlue", @@ -181,9 +201,9 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { ] ) - self.sendButton = HighlightTrackingButtonNode() - self.sendButton.displaysAsynchronously = false - self.sendButton.isExclusiveTouch = true + self.sendButtonImpl = HighlightTrackingButtonNode() + self.sendButtonImpl.displaysAsynchronously = false + self.sendButtonImpl.isExclusiveTouch = true self.sendBackgroundNode = ASDisplayNode() self.sendBackgroundNode.backgroundColor = theme.chat.inputPanel.actionControlFillColor @@ -198,11 +218,11 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { self.viewOnceButton = ChatRecordingViewOnceButtonNode(icon: .viewOnce) self.recordMoreButton = ChatRecordingViewOnceButtonNode(icon: .recordMore) - self.waveformBackgroundNode = ASImageNode() - self.waveformBackgroundNode.isLayerBacked = true - self.waveformBackgroundNode.displaysAsynchronously = false - self.waveformBackgroundNode.displayWithoutProcessing = true - self.waveformBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 33.0, color: theme.chat.inputPanel.actionControlFillColor) + self.waveformBackgroundNodeImpl = ASImageNode() + self.waveformBackgroundNodeImpl.isLayerBacked = true + self.waveformBackgroundNodeImpl.displaysAsynchronously = false + self.waveformBackgroundNodeImpl.displayWithoutProcessing = true + self.waveformBackgroundNodeImpl.image = generateStretchableFilledCircleImage(diameter: 33.0, color: theme.chat.inputPanel.actionControlFillColor) self.waveformButton = ASButtonNode() self.waveformButton.accessibilityTraits.insert(.startsMediaSession) @@ -212,11 +232,11 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { self.waveformForegroundNode = AudioWaveformNode() self.waveformForegroundNode.isLayerBacked = true - self.waveformScrubberNode = MediaPlayerScrubbingNode(content: .custom(backgroundNode: self.waveformNode, foregroundContentNode: self.waveformForegroundNode)) + self.waveformScrubberNodeImpl = MediaPlayerScrubbingNode(content: .custom(backgroundNode: self.waveformNode, foregroundContentNode: self.waveformForegroundNode)) - self.trimView = TrimView(frame: .zero) - self.trimView.isHollow = true - self.playButtonNode = PlayButtonNode(theme: theme) + self.trimViewImpl = TrimView(frame: .zero) + self.trimViewImpl.isHollow = true + self.playButtonNodeImpl = PlayButtonNode(theme: theme) super.init() @@ -235,37 +255,37 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { } ) - self.addSubnode(self.deleteButton) - self.deleteButton.addSubnode(self.binNode) - self.addSubnode(self.waveformBackgroundNode) - self.addSubnode(self.sendButton) - self.sendButton.addSubnode(self.sendBackgroundNode) - self.sendButton.addSubnode(self.sendIconNode) - self.sendButton.addSubnode(self.textNode) + self.addSubnode(self.deleteButtonImpl) + self.deleteButtonImpl.addSubnode(self.binNode) + self.addSubnode(self.waveformBackgroundNodeImpl) + self.addSubnode(self.sendButtonImpl) + self.sendButtonImpl.addSubnode(self.sendBackgroundNode) + self.sendButtonImpl.addSubnode(self.sendIconNode) + self.sendButtonImpl.addSubnode(self.textNode) self.addSubnode(self.waveformScrubberNode) //self.addSubnode(self.waveformButton) - self.view.addSubview(self.trimView) - self.addSubnode(self.playButtonNode) + self.view.addSubview(self.trimViewImpl) + self.addSubnode(self.playButtonNodeImpl) - self.sendButton.highligthedChanged = { [weak self] highlighted in + self.sendButtonImpl.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { if highlighted { - strongSelf.sendButton.layer.animateScale(from: 1.0, to: 0.75, duration: 0.4, removeOnCompletion: false) - } else if let presentationLayer = strongSelf.sendButton.layer.presentation() { - strongSelf.sendButton.layer.animateScale(from: CGFloat((presentationLayer.value(forKeyPath: "transform.scale.y") as? NSNumber)?.floatValue ?? 1.0), to: 1.0, duration: 0.25, removeOnCompletion: false) + strongSelf.sendButtonImpl.layer.animateScale(from: 1.0, to: 0.75, duration: 0.4, removeOnCompletion: false) + } else if let presentationLayer = strongSelf.sendButtonImpl.layer.presentation() { + strongSelf.sendButtonImpl.layer.animateScale(from: CGFloat((presentationLayer.value(forKeyPath: "transform.scale.y") as? NSNumber)?.floatValue ?? 1.0), to: 1.0, duration: 0.25, removeOnCompletion: false) } } } - self.playButtonNode.pressed = { [weak self] in + self.playButtonNodeImpl.pressed = { [weak self] in guard let self else { return } self.waveformPressed() } - self.waveformScrubberNode.seek = { [weak self] timestamp in + self.waveformScrubberNodeImpl.seek = { [weak self] timestamp in guard let self else { return } @@ -276,17 +296,17 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { self.mediaPlayer?.seek(timestamp: timestamp) } - self.scrubbingDisposable = (self.waveformScrubberNode.scrubbingPosition + self.scrubbingDisposable = (self.waveformScrubberNodeImpl.scrubbingPosition |> deliverOnMainQueue).startStrict(next: { [weak self] value in guard let self else { return } let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .easeInOut) - transition.updateAlpha(node: self.playButtonNode, alpha: value != nil ? 0.0 : 1.0) + transition.updateAlpha(node: self.playButtonNodeImpl, alpha: value != nil ? 0.0 : 1.0) }) - self.deleteButton.addTarget(self, action: #selector(self.deletePressed), forControlEvents: [.touchUpInside]) - self.sendButton.addTarget(self, action: #selector(self.sendPressed), forControlEvents: [.touchUpInside]) + self.deleteButtonImpl.addTarget(self, action: #selector(self.deletePressed), forControlEvents: [.touchUpInside]) + self.sendButtonImpl.addTarget(self, action: #selector(self.sendPressed), forControlEvents: [.touchUpInside]) self.viewOnceButton.addTarget(self, action: #selector(self.viewOncePressed), forControlEvents: [.touchUpInside]) self.recordMoreButton.addTarget(self, action: #selector(self.recordMorePressed), forControlEvents: [.touchUpInside]) @@ -304,7 +324,7 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { super.didLoad() let gestureRecognizer = ContextGesture(target: nil, action: nil) - self.sendButton.view.addGestureRecognizer(gestureRecognizer) + self.sendButtonImpl.view.addGestureRecognizer(gestureRecognizer) self.gestureRecognizer = gestureRecognizer gestureRecognizer.shouldBegin = { [weak self] _ in if let self, self.viewOnce { @@ -316,7 +336,7 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { guard let strongSelf = self else { return } - strongSelf.interfaceInteraction?.displaySendMessageOptions(strongSelf.sendButton, gesture) + strongSelf.interfaceInteraction?.displaySendMessageOptions(strongSelf.sendButtonImpl, gesture) } if let viewForOverlayContent = self.viewForOverlayContent { @@ -422,7 +442,7 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { self.textNode.isHidden = true } - transition.updateFrame(node: self.sendButton, frame: CGRect(origin: CGPoint(x: width - rightInset - innerSize.width + 1.0 - UIScreenPixel, y: 1.0 + UIScreenPixel), size: innerSize)) + transition.updateFrame(node: self.sendButtonImpl, frame: CGRect(origin: CGPoint(x: width - rightInset - innerSize.width + 1.0 - UIScreenPixel, y: 1.0 + UIScreenPixel), size: innerSize)) let backgroundSize = CGSize(width: innerSize.width - 11.0, height: 33.0) let backgroundFrame = CGRect(origin: CGPoint(x: 5.0, y: floorToScreenPixels((innerSize.height - backgroundSize.height) / 2.0)), size: backgroundSize) transition.updateFrame(node: self.sendBackgroundNode, frame: backgroundFrame) @@ -440,8 +460,8 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { updateWaveform = true } if self.presentationInterfaceState?.strings !== interfaceState.strings { - self.deleteButton.accessibilityLabel = interfaceState.strings.VoiceOver_MessageContextDelete - self.sendButton.accessibilityLabel = interfaceState.strings.VoiceOver_MessageContextSend + self.deleteButtonImpl.accessibilityLabel = interfaceState.strings.VoiceOver_MessageContextDelete + self.sendButtonImpl.accessibilityLabel = interfaceState.strings.VoiceOver_MessageContextSend self.waveformButton.accessibilityLabel = interfaceState.strings.VoiceOver_Chat_RecordPreviewVoiceMessage } @@ -451,10 +471,10 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { switch recordedMediaPreview { case let .audio(audio): self.waveformButton.isHidden = false - self.waveformBackgroundNode.isHidden = false + self.waveformBackgroundNodeImpl.isHidden = false self.waveformForegroundNode.isHidden = false - self.waveformScrubberNode.isHidden = false - self.playButtonNode.isHidden = false + self.waveformScrubberNodeImpl.isHidden = false + self.playButtonNodeImpl.isHidden = false if let view = self.scrubber.view, view.superview != nil { view.removeFromSuperview() @@ -484,10 +504,10 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { } } self.mediaPlayer = mediaPlayer - self.playButtonNode.durationLabel.defaultDuration = Double(audio.duration) - self.playButtonNode.durationLabel.status = mediaPlayer.status - self.playButtonNode.durationLabel.trimRange = audio.trimRange - self.waveformScrubberNode.status = mediaPlayer.status + self.playButtonNodeImpl.durationLabel.defaultDuration = Double(audio.duration) + self.playButtonNodeImpl.durationLabel.status = mediaPlayer.status + self.playButtonNodeImpl.durationLabel.trimRange = audio.trimRange + self.waveformScrubberNodeImpl.status = mediaPlayer.status self.statusDisposable.set((mediaPlayer.status |> deliverOnMainQueue).startStrict(next: { [weak self] status in @@ -498,18 +518,18 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { if let recordedMediaPreview = self.presentationInterfaceState?.interfaceState.mediaDraftState, case let .audio(audio) = recordedMediaPreview, let _ = audio.trimRange { self.ensureHasTimer() } - self.playButtonNode.playPauseIconNode.enqueueState(.pause, animated: true) + self.playButtonNodeImpl.playPauseIconNode.enqueueState(.pause, animated: true) default: self.statusValue = nil self.stopTimer() - self.playButtonNode.playPauseIconNode.enqueueState(.play, animated: true) + self.playButtonNodeImpl.playPauseIconNode.enqueueState(.play, animated: true) } } })) } let minDuration = max(1.0, 56.0 * audio.duration / waveformBackgroundFrame.size.width) - let (leftHandleFrame, rightHandleFrame) = self.trimView.update( + let (leftHandleFrame, rightHandleFrame) = self.trimViewImpl.update( style: .voiceMessage, theme: interfaceState.theme, visualInsets: .zero, @@ -522,7 +542,7 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { maxDuration: Double(audio.duration), transition: .immediate ) - self.trimView.trimUpdated = { [weak self] start, end, updatedEnd, apply in + self.trimViewImpl.trimUpdated = { [weak self] start, end, updatedEnd, apply in if let self { self.mediaPlayer?.pause() self.interfaceInteraction?.updateRecordingTrimRange(start, end, updatedEnd, apply) @@ -532,12 +552,12 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { } else { self.mediaPlayer?.seek(timestamp: max(0.0, end - 1.0), play: true) } - self.playButtonNode.durationLabel.isScrubbing = false + self.playButtonNodeImpl.durationLabel.isScrubbing = false Queue.mainQueue().after(0.1) { self.waveformForegroundNode.alpha = 1.0 } } else { - self.playButtonNode.durationLabel.isScrubbing = true + self.playButtonNodeImpl.durationLabel.isScrubbing = true self.waveformForegroundNode.alpha = 0.0 } @@ -546,18 +566,18 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { self.waveformForegroundNode.trimRange = startFraction ..< endFraction } } - self.trimView.frame = waveformBackgroundFrame - self.trimView.isHidden = audio.duration < 2.0 + self.trimViewImpl.frame = waveformBackgroundFrame + self.trimViewImpl.isHidden = audio.duration < 2.0 let playButtonSize = CGSize(width: max(0.0, rightHandleFrame.minX - leftHandleFrame.maxX), height: waveformBackgroundFrame.height) - self.playButtonNode.update(size: playButtonSize, transition: transition) - transition.updateFrame(node: self.playButtonNode, frame: CGRect(origin: CGPoint(x: waveformBackgroundFrame.minX + leftHandleFrame.maxX, y: waveformBackgroundFrame.minY), size: playButtonSize)) + self.playButtonNodeImpl.update(size: playButtonSize, transition: transition) + transition.updateFrame(node: self.playButtonNodeImpl, frame: CGRect(origin: CGPoint(x: waveformBackgroundFrame.minX + leftHandleFrame.maxX, y: waveformBackgroundFrame.minY), size: playButtonSize)) case let .video(video): self.waveformButton.isHidden = true - self.waveformBackgroundNode.isHidden = true + self.waveformBackgroundNodeImpl.isHidden = true self.waveformForegroundNode.isHidden = true - self.waveformScrubberNode.isHidden = true - self.playButtonNode.isHidden = true + self.waveformScrubberNodeImpl.isHidden = true + self.playButtonNodeImpl.isHidden = true let scrubberSize = self.scrubber.update( transition: .immediate, @@ -612,9 +632,9 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { } let panelHeight = defaultHeight(metrics: metrics) - transition.updateFrame(node: self.deleteButton, frame: CGRect(origin: CGPoint(x: leftInset + 2.0 - UIScreenPixel, y: 1), size: CGSize(width: 40.0, height: 40))) + transition.updateFrame(node: self.deleteButtonImpl, frame: CGRect(origin: CGPoint(x: leftInset + 2.0 - UIScreenPixel, y: 1), size: CGSize(width: 40.0, height: 40))) - self.binNode.frame = self.deleteButton.bounds + self.binNode.frame = self.deleteButtonImpl.bounds var viewOnceOffset: CGFloat = 0.0 if interfaceState.interfaceState.replyMessageSubject != nil { @@ -640,14 +660,14 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { sendButtonRadialStatusNode = current } else { sendButtonRadialStatusNode = ChatSendButtonRadialStatusNode(color: interfaceState.theme.chat.inputPanel.panelControlAccentColor) - sendButtonRadialStatusNode.alpha = self.sendButton.alpha + sendButtonRadialStatusNode.alpha = self.sendButtonImpl.alpha self.sendButtonRadialStatusNode = sendButtonRadialStatusNode self.addSubnode(sendButtonRadialStatusNode) } - transition.updateSublayerTransformScale(layer: self.sendButton.layer, scale: CGPoint(x: 0.7575, y: 0.7575)) + transition.updateSublayerTransformScale(layer: self.sendButtonImpl.layer, scale: CGPoint(x: 0.7575, y: 0.7575)) - sendButtonRadialStatusNode.frame = CGRect(origin: CGPoint(x: self.sendButton.frame.midX - 33.0 / 2.0, y: self.sendButton.frame.midY - 33.0 / 2.0), size: CGSize(width: 33.0, height: 33.0)) + sendButtonRadialStatusNode.frame = CGRect(origin: CGPoint(x: self.sendButtonImpl.frame.midX - 33.0 / 2.0, y: self.sendButtonImpl.frame.midY - 33.0 / 2.0), size: CGSize(width: 33.0, height: 33.0)) sendButtonRadialStatusNode.slowmodeState = slowmodeState } else { if let sendButtonRadialStatusNode = self.sendButtonRadialStatusNode { @@ -657,7 +677,7 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { transition.updateSublayerTransformScale(layer: self.sendButton.layer, scale: CGPoint(x: 1.0, y: 1.0)) } - transition.updateFrame(node: self.waveformBackgroundNode, frame: waveformBackgroundFrame) + transition.updateFrame(node: self.waveformBackgroundNodeImpl, frame: waveformBackgroundFrame) transition.updateFrame(node: self.waveformButton, frame: CGRect(origin: CGPoint(x: leftInset + 45.0, y: 0.0), size: CGSize(width: width - leftInset - rightInset - 45.0 - innerSize.width - 1.0, height: panelHeight))) transition.updateFrame(node: self.waveformScrubberNode, frame: CGRect(origin: CGPoint(x: leftInset + 45.0 + 21.0, y: 7.0 + floor((33.0 - 13.0) / 2.0)), size: CGSize(width: width - leftInset - rightInset - 45.0 - innerSize.width - 41.0, height: 13.0))) @@ -710,20 +730,20 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { view.layer.animatePosition(from: CGPoint(x: 0.0, y: 64.0), to: .zero, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) } - self.deleteButton.layer.animateScale(from: 0.3, to: 1.0, duration: 0.15) - self.deleteButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + self.deleteButtonImpl.layer.animateScale(from: 0.3, to: 1.0, duration: 0.15) + self.deleteButtonImpl.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) - self.playButtonNode.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3, delay: 0.1) - self.playButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.1) + self.playButtonNodeImpl.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3, delay: 0.1) + self.playButtonNodeImpl.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.1) - self.trimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.1) + self.trimViewImpl.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.1) - self.waveformScrubberNode.layer.animateScaleY(from: 0.1, to: 1.0, duration: 0.3, delay: 0.1) - self.waveformScrubberNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.1) + self.waveformScrubberNodeImpl.layer.animateScaleY(from: 0.1, to: 1.0, duration: 0.3, delay: 0.1) + self.waveformScrubberNodeImpl.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.1) - self.waveformBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) - self.waveformBackgroundNode.layer.animateFrame( - from: self.sendButton.frame.insetBy(dx: 5.5, dy: 5.5), + self.waveformBackgroundNodeImpl.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + self.waveformBackgroundNodeImpl.layer.animateFrame( + from: self.sendButtonImpl.frame.insetBy(dx: 5.5, dy: 5.5), to: waveformBackgroundFrame, duration: 0.2, delay: 0.12, @@ -853,7 +873,7 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { } func frameForInputActionButton() -> CGRect? { - return self.sendButton.frame + return self.sendButtonImpl.frame } } @@ -906,112 +926,3 @@ private final class PlayPauseIconNode: ManagedAnimationNode { } } } - - -final class ChatRecordingViewOnceButtonNode: HighlightTrackingButtonNode { - enum Icon { - case viewOnce - case recordMore - } - - private let icon: Icon - - private let backgroundNode: ASImageNode - private let iconNode: ASImageNode - - private var theme: PresentationTheme? - - init(icon: Icon) { - self.icon = icon - - self.backgroundNode = ASImageNode() - self.backgroundNode.isUserInteractionEnabled = false - - self.iconNode = ASImageNode() - self.iconNode.isUserInteractionEnabled = false - - super.init(pointerStyle: .default) - - self.addSubnode(self.backgroundNode) - self.addSubnode(self.iconNode) - - self.highligthedChanged = { [weak self] highlighted in - if let self, self.bounds.width > 0.0 { - let topScale: CGFloat = (self.bounds.width - 8.0) / self.bounds.width - let maxScale: CGFloat = (self.bounds.width + 2.0) / self.bounds.width - - if highlighted { - self.layer.removeAnimation(forKey: "sublayerTransform") - let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) - transition.updateTransformScale(node: self, scale: topScale) - } else { - let transition = ContainedViewLayoutTransition.immediate - transition.updateTransformScale(node: self, scale: 1.0) - - self.layer.animateScale(from: topScale, to: maxScale, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in - guard let self else { - return - } - - self.layer.animateScale(from: maxScale, to: 1.0, duration: 0.1, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue) - }) - } - } - } - } - - private var innerIsSelected = false - func update(isSelected: Bool, animated: Bool = false) { - guard let theme = self.theme else { - return - } - - let updated = self.iconNode.image == nil || self.innerIsSelected != isSelected - self.innerIsSelected = isSelected - - if animated, updated && self.iconNode.image != nil, let snapshot = self.iconNode.view.snapshotContentTree() { - self.view.addSubview(snapshot) - snapshot.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in - snapshot.removeFromSuperview() - }) - - self.iconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - } - - if updated { - if case .viewOnce = self.icon { - self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: self.innerIsSelected ? "Media Gallery/ViewOnceEnabled" : "Media Gallery/ViewOnce"), color: theme.chat.inputPanel.panelControlAccentColor) - } - } - } - - func update(theme: PresentationTheme) -> CGSize { - let size = CGSize(width: 44.0, height: 44.0) - let innerSize = CGSize(width: 40.0, height: 40.0) - - if self.theme !== theme { - self.theme = theme - - self.backgroundNode.image = generateFilledCircleImage(diameter: innerSize.width, color: theme.rootController.navigationBar.opaqueBackgroundColor, strokeColor: theme.chat.inputPanel.panelSeparatorColor, strokeWidth: 0.5, backgroundColor: nil) - - switch self.icon { - case .viewOnce: - self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: self.innerIsSelected ? "Media Gallery/ViewOnceEnabled" : "Media Gallery/ViewOnce"), color: theme.chat.inputPanel.panelControlAccentColor) - - case .recordMore: - self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/IconMicrophone"), color: theme.chat.inputPanel.panelControlAccentColor) - } - } - - if let backgroundImage = self.backgroundNode.image { - let backgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(size.width / 2.0 - backgroundImage.size.width / 2.0), y: floorToScreenPixels(size.height / 2.0 - backgroundImage.size.height / 2.0)), size: backgroundImage.size) - self.backgroundNode.frame = backgroundFrame - } - - if let iconImage = self.iconNode.image { - let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(size.width / 2.0 - iconImage.size.width / 2.0), y: floorToScreenPixels(size.height / 2.0 - iconImage.size.height / 2.0)), size: iconImage.size) - self.iconNode.frame = iconFrame - } - return size - } -}