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/AttachmentController.swift b/submodules/AttachmentUI/Sources/AttachmentController.swift index 64072bfc41..e77adda347 100644 --- a/submodules/AttachmentUI/Sources/AttachmentController.swift +++ b/submodules/AttachmentUI/Sources/AttachmentController.swift @@ -1374,7 +1374,7 @@ public class AttachmentController: ViewController, MinimizableController { let inputNode: ASDisplayNode let accessoryPanelNode: ASDisplayNode? let menuButtonNode: ASDisplayNode - let menuButtonBackgroundNode: ASDisplayNode + let menuButtonBackgroundView: UIView let menuIconNode: ASDisplayNode let menuTextNode: ASDisplayNode let prepareForDismiss: () -> Void @@ -1383,7 +1383,7 @@ public class AttachmentController: ViewController, MinimizableController { inputNode: ASDisplayNode, accessoryPanelNode: ASDisplayNode?, menuButtonNode: ASDisplayNode, - menuButtonBackgroundNode: ASDisplayNode, + menuButtonBackgroundView: UIView, menuIconNode: ASDisplayNode, menuTextNode: ASDisplayNode, prepareForDismiss: @escaping () -> Void @@ -1391,7 +1391,7 @@ public class AttachmentController: ViewController, MinimizableController { self.inputNode = inputNode self.accessoryPanelNode = accessoryPanelNode self.menuButtonNode = menuButtonNode - self.menuButtonBackgroundNode = menuButtonBackgroundNode + self.menuButtonBackgroundView = menuButtonBackgroundView self.menuIconNode = menuIconNode self.menuTextNode = menuTextNode self.prepareForDismiss = prepareForDismiss diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index 8baa9978ff..ed89869a34 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -1276,6 +1276,8 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { }, dismissUrlPreview: { }, dismissForwardMessages: { }, dismissSuggestPost: { + }, displayUndo: { _ in + }, sendEmoji: { _, _, _ in }, updateHistoryFilter: { _ in }, updateChatLocationThread: { _, _ in }, toggleChatSidebarMode: { @@ -1628,7 +1630,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { self.animatingTransition = true let targetButtonColor = self.mainButtonNode.backgroundColor - self.mainButtonNode.backgroundColor = inputTransition.menuButtonBackgroundNode.backgroundColor + self.mainButtonNode.backgroundColor = inputTransition.menuButtonBackgroundView.backgroundColor transition.updateBackgroundColor(node: self.mainButtonNode, color: targetButtonColor ?? .clear) transition.animateFrame(layer: self.mainButtonNode.layer, from: inputTransition.menuButtonNode.frame) @@ -1686,7 +1688,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { } let sourceButtonColor = self.mainButtonNode.backgroundColor - transition.updateBackgroundColor(node: self.mainButtonNode, color: inputTransition.menuButtonBackgroundNode.backgroundColor ?? .clear) + transition.updateBackgroundColor(node: self.mainButtonNode, color: inputTransition.menuButtonBackgroundView.backgroundColor ?? .clear) let sourceButtonFrame = self.mainButtonNode.frame transition.updateFrame(node: self.mainButtonNode, frame: inputTransition.menuButtonNode.frame) diff --git a/submodules/AudioBlob/BUILD b/submodules/AudioBlob/BUILD index dc0d2ee1d4..1943af5c36 100644 --- a/submodules/AudioBlob/BUILD +++ b/submodules/AudioBlob/BUILD @@ -15,6 +15,7 @@ swift_library( "//submodules/LegacyComponents", "//submodules/MetalEngine", "//submodules/TelegramUI/Components/Calls/CallScreen", + "//submodules/TelegramUI/Components/GlassBackgroundComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/AudioBlob/Sources/BlobView.swift b/submodules/AudioBlob/Sources/BlobView.swift index c1b287fbe5..34d7c1c8f1 100644 --- a/submodules/AudioBlob/Sources/BlobView.swift +++ b/submodules/AudioBlob/Sources/BlobView.swift @@ -3,6 +3,7 @@ import UIKit import AsyncDisplayKit import Display import LegacyComponents +import GlassBackgroundComponent public final class VoiceBlobNode: ASDisplayNode { public init( @@ -265,6 +266,8 @@ final class BlobNode: ASDisplayNode { return layer }() + private var backgroundView: GlassBackgroundView? + private var transition: CGFloat = 0 { didSet { guard let currentPoints = currentPoints else { return } @@ -291,6 +294,8 @@ final class BlobNode: ASDisplayNode { private let hierarchyTrackingNode: HierarchyTrackingNode private var isCurrentlyInHierarchy = true + private var color: UIColor? + init( pointsCount: Int, minRandomness: CGFloat, @@ -326,6 +331,13 @@ final class BlobNode: ASDisplayNode { self.layer.addSublayer(self.shapeLayer) self.shapeLayer.transform = CATransform3DMakeScale(minScale, minScale, 1) + + if isCircle { + let backgroundView = GlassBackgroundView() + self.backgroundView = backgroundView + self.shapeLayer.removeFromSuperlayer() + self.view.addSubview(backgroundView) + } updateInHierarchy = { [weak self] value in if let strongSelf = self { @@ -339,6 +351,8 @@ final class BlobNode: ASDisplayNode { } func setColor(_ color: UIColor, animated: Bool) { + self.color = color + let previousColor = self.shapeLayer.fillColor self.shapeLayer.fillColor = color.cgColor if animated, let previousColor = previousColor, self.isCurrentlyInHierarchy { @@ -444,6 +458,12 @@ final class BlobNode: ASDisplayNode { ).cgPath } CATransaction.commit() + + if let backgroundView = self.backgroundView, let color = self.color { + let halfWidth = floor(self.bounds.width * self.minScale) + backgroundView.update(size: CGSize(width: halfWidth, height: halfWidth), cornerRadius: halfWidth * 0.5, isDark: false, tintColor: color, transition: .immediate) + backgroundView.frame = CGRect(origin: CGPoint(x: (self.bounds.width - halfWidth) * 0.5, y: (self.bounds.height - halfWidth) * 0.5), size: CGSize(width: halfWidth, height: halfWidth)) + } } } 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 5761e92de2..57f9b9ef08 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 @@ -190,6 +192,8 @@ public final class ChatPanelInterfaceInteraction { public let dismissUrlPreview: () -> Void public let dismissForwardMessages: () -> Void public let dismissSuggestPost: () -> 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? @@ -312,6 +316,8 @@ public final class ChatPanelInterfaceInteraction { dismissUrlPreview: @escaping () -> Void, dismissForwardMessages: @escaping () -> Void, dismissSuggestPost: @escaping () -> 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, @@ -437,6 +443,8 @@ public final class ChatPanelInterfaceInteraction { self.dismissUrlPreview = dismissUrlPreview self.dismissForwardMessages = dismissForwardMessages self.dismissSuggestPost = dismissSuggestPost + self.displayUndo = displayUndo + self.sendEmoji = sendEmoji self.updateHistoryFilter = updateHistoryFilter self.updateChatLocationThread = updateChatLocationThread self.toggleChatSidebarMode = toggleChatSidebarMode @@ -571,6 +579,8 @@ public final class ChatPanelInterfaceInteraction { }, dismissUrlPreview: { }, dismissForwardMessages: { }, dismissSuggestPost: { + }, displayUndo: { _ in + }, sendEmoji: { _, _, _ in }, updateHistoryFilter: { _ in }, updateChatLocationThread: { _, _ in }, toggleChatSidebarMode: { diff --git a/submodules/GradientBackground/Sources/SoftwareGradientBackground.swift b/submodules/GradientBackground/Sources/SoftwareGradientBackground.swift index b5e3fe6c42..c0819f8704 100644 --- a/submodules/GradientBackground/Sources/SoftwareGradientBackground.swift +++ b/submodules/GradientBackground/Sources/SoftwareGradientBackground.swift @@ -201,22 +201,33 @@ public protocol GradientBackgroundPatternOverlayLayer: CALayer { public final class GradientBackgroundNode: ASDisplayNode { public final class CloneNode: ASImageNode { private weak var parentNode: GradientBackgroundNode? + private let isDimmed: Bool private var index: SparseBag>.Index? - public init(parentNode: GradientBackgroundNode) { + public init(parentNode: GradientBackgroundNode, isDimmed: Bool) { self.parentNode = parentNode + self.isDimmed = isDimmed super.init() self.displaysAsynchronously = false - self.index = parentNode.cloneNodes.add(Weak(self)) - self.image = parentNode.dimmedImage + if isDimmed { + self.index = parentNode.cloneNodes.add(Weak(self)) + self.image = parentNode.dimmedImage + } else { + self.index = parentNode.rawCloneNodes.add(Weak(self)) + self.image = parentNode.rawImage + } } deinit { if let parentNode = self.parentNode, let index = self.index { - parentNode.cloneNodes.remove(index) + if self.isDimmed { + parentNode.cloneNodes.remove(index) + } else { + parentNode.rawCloneNodes.remove(index) + } } } } @@ -258,9 +269,14 @@ public final class GradientBackgroundNode: ASDisplayNode { return nil } } + + private var rawImage: UIImage? { + return self.contentView.image + } private var validLayout: CGSize? private let cloneNodes = SparseBag>() + private let rawCloneNodes = SparseBag>() private let useSharedAnimationPhase: Bool static var sharedPhase: Int = 0 @@ -490,6 +506,24 @@ public final class GradientBackgroundNode: ASDisplayNode { } } } + + if !self.rawCloneNodes.isEmpty { + let cloneAnimation = CAKeyframeAnimation(keyPath: "contents") + cloneAnimation.values = images.map { $0.0.cgImage! } + cloneAnimation.duration = animation.duration + cloneAnimation.calculationMode = animation.calculationMode + cloneAnimation.isRemovedOnCompletion = animation.isRemovedOnCompletion + cloneAnimation.fillMode = animation.fillMode + cloneAnimation.beginTime = animation.beginTime + + for cloneNode in self.rawCloneNodes { + if let value = cloneNode.value { + value.image = images.last?.0 + value.layer.removeAnimation(forKey: "contents") + value.layer.add(cloneAnimation, forKey: "contents") + } + } + } } else { let (image, imageHash) = generateGradient(size: imageSize, colors: self.colors, positions: positions) self.contentView.image = image @@ -502,6 +536,9 @@ public final class GradientBackgroundNode: ASDisplayNode { for cloneNode in self.cloneNodes { cloneNode.value?.image = dimmedImage } + for cloneNode in self.rawCloneNodes { + cloneNode.value?.image = image + } completion() } @@ -516,6 +553,9 @@ public final class GradientBackgroundNode: ASDisplayNode { for cloneNode in self.cloneNodes { cloneNode.value?.image = dimmedImage } + for cloneNode in self.rawCloneNodes { + cloneNode.value?.image = image + } self.validPhase = self.phase @@ -534,6 +574,9 @@ public final class GradientBackgroundNode: ASDisplayNode { for cloneNode in self.cloneNodes { cloneNode.value?.image = dimmedImage } + for cloneNode in self.rawCloneNodes { + cloneNode.value?.image = image + } self.validPhase = self.phase diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGModernConversationInputMicButton.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGModernConversationInputMicButton.h index ed78e3cbef..631114d168 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGModernConversationInputMicButton.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGModernConversationInputMicButton.h @@ -65,6 +65,12 @@ @end +@protocol TGModernConversationInputMicButtonLockPanelView + +- (void)updateSize:(CGSize)size; + +@end + @interface TGModernConversationInputMicButton : UIButton @property (nonatomic, weak) id delegate; @@ -88,6 +94,6 @@ - (void)_commitLocked; - (void)setHidesPanelOnLock; -- (UIView *)createLockPanelView; +- (UIView *)createLockPanelView; @end diff --git a/submodules/LegacyComponents/Sources/TGModernConversationInputMicButton.m b/submodules/LegacyComponents/Sources/TGModernConversationInputMicButton.m index 88bdbba5e9..d30009fbb9 100644 --- a/submodules/LegacyComponents/Sources/TGModernConversationInputMicButton.m +++ b/submodules/LegacyComponents/Sources/TGModernConversationInputMicButton.m @@ -98,6 +98,17 @@ static const CGFloat outerCircleMinScale = innerCircleRadius / outerCircleRadius @end +@interface TGModernConversationInputMicButtonLockPanelViewNativeImpl : UIImageView + +@end + +@implementation TGModernConversationInputMicButtonLockPanelViewNativeImpl + +- (void)updateSize:(CGSize)size { +} + +@end + @interface TGModernConversationInputMicButton () { CGPoint _touchLocation; @@ -116,7 +127,7 @@ static const CGFloat outerCircleMinScale = innerCircleRadius / outerCircleRadius UIImageView *_innerIconView; UIView *_lockPanelWrapperView; - UIView *_lockPanelView; + UIView *_lockPanelView; UIImageView *_lockArrowView; TGModernConversationInputLockView *_lockView; UIImage *_previousIcon; @@ -353,8 +364,8 @@ static const CGFloat outerCircleMinScale = innerCircleRadius / outerCircleRadius return stopButtonImage; } -- (UIView *)createLockPanelView { - UIImageView *view = [[UIImageView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 40.0f, 72.0f)]; +- (UIView *)createLockPanelView { + TGModernConversationInputMicButtonLockPanelViewNativeImpl *view = [[TGModernConversationInputMicButtonLockPanelViewNativeImpl alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 40.0f, 72.0f)]; view.userInteractionEnabled = true; view.image = [self panelBackgroundImage]; return view; @@ -634,6 +645,8 @@ static const CGFloat outerCircleMinScale = innerCircleRadius / outerCircleRadius [snapshotView removeFromSuperview]; }]; + [_lockPanelView updateSize:CGSizeMake(_lockPanelView.frame.size.width, 72.0f - 32.0f)]; + [UIView animateWithDuration:0.2 animations:^ { snapshotView.alpha = 0.0f; diff --git a/submodules/PasscodeUI/Sources/PasscodeBackground.swift b/submodules/PasscodeUI/Sources/PasscodeBackground.swift index c3901f496a..9a470abfe9 100644 --- a/submodules/PasscodeUI/Sources/PasscodeBackground.swift +++ b/submodules/PasscodeUI/Sources/PasscodeBackground.swift @@ -37,7 +37,7 @@ final class CustomPasscodeBackground: PasscodeBackground { func makeForegroundNode(backgroundNode: ASDisplayNode?) -> ASDisplayNode? { if self.inverted, let backgroundNode = backgroundNode as? GradientBackgroundNode { - return GradientBackgroundNode.CloneNode(parentNode: backgroundNode) + return GradientBackgroundNode.CloneNode(parentNode: backgroundNode, isDimmed: true) } else { return nil } diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 784ad0f407..2761b29c3f 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -486,10 +486,12 @@ 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/GlassBackgroundComponent", "//submodules/TelegramUI/Components/Chat/ChatInputAccessoryPanel", "//submodules/TelegramUI/Components/Chat/ChatInputMessageAccessoryPanel", + "//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/ChatButtonKeyboardInputNode/Sources/ChatButtonKeyboardInputNode.swift b/submodules/TelegramUI/Components/Chat/ChatButtonKeyboardInputNode/Sources/ChatButtonKeyboardInputNode.swift index dc4e563224..4eda6cfaec 100644 --- a/submodules/TelegramUI/Components/Chat/ChatButtonKeyboardInputNode/Sources/ChatButtonKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatButtonKeyboardInputNode/Sources/ChatButtonKeyboardInputNode.swift @@ -24,7 +24,6 @@ private final class ChatButtonKeyboardInputButtonNode: HighlightTrackingButtonNo private let backgroundColorNode: ASDisplayNode private let backgroundAdditionalColorNode: ASDisplayNode - private let shadowNode: ASImageNode private let highlightNode: ASImageNode private let textNode: ImmediateTextNode @@ -37,24 +36,17 @@ private final class ChatButtonKeyboardInputButtonNode: HighlightTrackingButtonNo self.backgroundContainerNode.clipsToBounds = true self.backgroundContainerNode.allowsGroupOpacity = true self.backgroundContainerNode.isUserInteractionEnabled = false - self.backgroundContainerNode.cornerRadius = 5.0 - if #available(iOS 13.0, *) { - self.backgroundContainerNode.layer.cornerCurve = .continuous - } + self.backgroundContainerNode.cornerRadius = 10.0 + self.backgroundContainerNode.layer.cornerCurve = .continuous self.backgroundColorNode = ASDisplayNode() - self.backgroundColorNode.cornerRadius = 5.0 - if #available(iOS 13.0, *) { - self.backgroundColorNode.layer.cornerCurve = .continuous - } + self.backgroundColorNode.cornerRadius = 10.0 + self.backgroundColorNode.layer.cornerCurve = .continuous self.backgroundAdditionalColorNode = ASDisplayNode() self.backgroundAdditionalColorNode.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.1) self.backgroundAdditionalColorNode.isHidden = true - self.shadowNode = ASImageNode() - self.shadowNode.isUserInteractionEnabled = false - self.highlightNode = ASImageNode() self.highlightNode.isUserInteractionEnabled = false @@ -73,7 +65,6 @@ private final class ChatButtonKeyboardInputButtonNode: HighlightTrackingButtonNo self.backgroundContainerNode.addSubnode(self.backgroundAdditionalColorNode) self.addSubnode(self.textNode) - self.backgroundContainerNode.addSubnode(self.shadowNode) self.backgroundContainerNode.addSubnode(self.highlightNode) self.highligthedChanged = { [weak self] highlighted in @@ -146,7 +137,6 @@ private final class ChatButtonKeyboardInputButtonNode: HighlightTrackingButtonNo self.theme = theme self.highlightNode.image = PresentationResourcesChat.chatInputButtonPanelButtonHighlightImage(theme) - self.shadowNode.image = PresentationResourcesChat.chatInputButtonPanelButtonShadowImage(theme) self.updateIcon() } @@ -182,7 +172,6 @@ private final class ChatButtonKeyboardInputButtonNode: HighlightTrackingButtonNo self.backgroundNode?.frame = self.backgroundColorNode.frame self.highlightNode.frame = self.bounds - self.shadowNode.frame = self.bounds if let (rect, containerSize) = self.absoluteRect { self.update(rect: rect, within: containerSize, transition: .immediate) @@ -201,7 +190,6 @@ public final class ChatButtonKeyboardInputNode: ChatInputNode { private let context: AccountContext private let controllerInteraction: ChatControllerInteraction - private let separatorNode: ASDisplayNode private let scrollNode: ASScrollNode private var backgroundNode: WallpaperBubbleBackgroundNode? @@ -220,9 +208,6 @@ public final class ChatButtonKeyboardInputNode: ChatInputNode { self.backgroundColorNode = ASDisplayNode() - self.separatorNode = ASDisplayNode() - self.separatorNode.isLayerBacked = true - super.init() self.addSubnode(self.backgroundColorNode) @@ -232,8 +217,6 @@ public final class ChatButtonKeyboardInputNode: ChatInputNode { self.scrollNode.view.canCancelContentTouches = true self.scrollNode.view.alwaysBounceHorizontal = false self.scrollNode.view.alwaysBounceVertical = false - - self.addSubnode(self.separatorNode) } override public func didLoad() { @@ -264,8 +247,6 @@ public final class ChatButtonKeyboardInputNode: ChatInputNode { } override public func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, layoutMetrics: LayoutMetrics, deviceMetrics: DeviceMetrics, isVisible: Bool, isExpanded: Bool) -> (CGFloat, CGFloat) { - transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: UIScreenPixel))) - if self.backgroundNode == nil { if let backgroundNode = self.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) { self.backgroundNode = backgroundNode @@ -277,7 +258,6 @@ public final class ChatButtonKeyboardInputNode: ChatInputNode { if updatedTheme { self.theme = interfaceState.theme - self.separatorNode.backgroundColor = interfaceState.theme.chat.inputButtonPanel.panelSeparatorColor self.backgroundColorNode.backgroundColor = interfaceState.theme.chat.inputButtonPanel.panelBackgroundColor } @@ -296,8 +276,8 @@ public final class ChatButtonKeyboardInputNode: ChatInputNode { self.message = interfaceState.keyboardButtonsMessage if let markup = validatedMarkup { - let verticalInset: CGFloat = 10.0 - let sideInset: CGFloat = 6.0 + leftInset + let verticalInset: CGFloat = 16.0 + let sideInset: CGFloat = 16.0 + leftInset var buttonHeight: CGFloat = 43.0 let columnSpacing: CGFloat = 6.0 let rowSpacing: CGFloat = 5.0 diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift index 20e7678405..459319a3d5 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift @@ -176,6 +176,8 @@ public final class ChatRecentActionsController: TelegramBaseController { }, dismissUrlPreview: { }, dismissForwardMessages: { }, dismissSuggestPost: { + }, displayUndo: { _ in + }, sendEmoji: { _, _, _ in }, updateHistoryFilter: { _ in }, updateChatLocationThread: { _, _ in }, toggleChatSidebarMode: { diff --git a/submodules/TelegramUI/Components/Chat/ChatRecordingPreviewInputPanelNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatRecordingPreviewInputPanelNode/BUILD new file mode 100644 index 0000000000..8db571013a --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatRecordingPreviewInputPanelNode/BUILD @@ -0,0 +1,40 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatRecordingPreviewInputPanelNode", + module_name = "ChatRecordingPreviewInputPanelNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/TelegramCore", + "//submodules/Postbox", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramPresentationData", + "//submodules/MediaPlayer:UniversalMediaPlayer", + "//submodules/AppBundle", + "//submodules/ContextUI", + "//submodules/AnimationUI", + "//submodules/ManagedAnimationNode", + "//submodules/ChatPresentationInterfaceState", + "//submodules/TelegramUI/Components/ChatSendButtonRadialStatusNode", + "//submodules/TelegramUI/Components/AudioWaveformNode", + "//submodules/TelegramUI/Components/Chat/ChatInputPanelNode", + "//submodules/TooltipUI", + "//submodules/TelegramNotices", + "//submodules/ComponentFlow", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/TelegramUI/Components/MediaScrubberComponent", + "//submodules/AnimatedCountLabelNode", + "//submodules/TelegramUI/Components/Chat/ChatRecordingViewOnceButtonNode", + "//submodules/TelegramUI/Components/GlassBackgroundComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Chat/ChatRecordingPreviewInputPanelNode/Sources/ChatRecordingPreviewInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecordingPreviewInputPanelNode/Sources/ChatRecordingPreviewInputPanelNode.swift new file mode 100644 index 0000000000..326548d1ef --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatRecordingPreviewInputPanelNode/Sources/ChatRecordingPreviewInputPanelNode.swift @@ -0,0 +1,721 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import TelegramCore +import Postbox +import SwiftSignalKit +import TelegramPresentationData +import UniversalMediaPlayer +import AppBundle +import ContextUI +import AnimationUI +import ManagedAnimationNode +import ChatPresentationInterfaceState +import ChatSendButtonRadialStatusNode +import AudioWaveformNode +import ChatInputPanelNode +import TooltipUI +import TelegramNotices +import ComponentFlow +import MediaScrubberComponent +import AnimatedCountLabelNode +import ChatRecordingViewOnceButtonNode +import GlassBackgroundComponent +import ComponentFlow +import ComponentDisplayAdapters + +#if SWIFT_PACKAGE +extension AudioWaveformNode: CustomMediaPlayerScrubbingForegroundNode { +} +#else +extension AudioWaveformNode: @retroactive CustomMediaPlayerScrubbingForegroundNode { +} +#endif + +final class ChatRecordingPreviewViewForOverlayContent: UIView, ChatInputPanelViewForOverlayContent { + let ignoreHit: (UIView, CGPoint) -> Bool + + init(ignoreHit: @escaping (UIView, CGPoint) -> Bool) { + self.ignoreHit = ignoreHit + + super.init(frame: CGRect()) + } + + required init(coder: NSCoder) { + preconditionFailure() + } + + func maybeDismissContent(point: CGPoint) { + for subview in self.subviews.reversed() { + if let _ = subview.hitTest(self.convert(point, to: subview), with: nil) { + return + } + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + for subview in self.subviews.reversed() { + if let result = subview.hitTest(self.convert(point, to: subview), with: event) { + return result + } + } + + if event == nil || self.ignoreHit(self, point) { + return nil + } + + return nil + } +} + +final class PlayButtonNode: ASDisplayNode { + let backgroundView: GlassBackgroundView + let playButton: HighlightableButtonNode + fileprivate let playPauseIconNode: PlayPauseIconNode + let durationLabel: MediaPlayerTimeTextNode + + var pressed: () -> Void = {} + + init(theme: PresentationTheme) { + self.backgroundView = GlassBackgroundView(frame: CGRect()) + + self.playButton = HighlightableButtonNode() + self.playButton.displaysAsynchronously = false + + self.playPauseIconNode = PlayPauseIconNode() + self.playPauseIconNode.enqueueState(.play, animated: false) + self.playPauseIconNode.customColor = theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.7) + + self.durationLabel = MediaPlayerTimeTextNode(textColor: theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.7), textFont: Font.with(size: 13.0, weight: .semibold, traits: .monospacedNumbers)) + self.durationLabel.alignment = .right + self.durationLabel.mode = .normal + self.durationLabel.showDurationIfNotStarted = true + + super.init() + + self.view.addSubview(self.backgroundView) + self.addSubnode(self.playButton) + self.backgroundView.contentView.addSubview(self.playPauseIconNode.view) + self.backgroundView.contentView.addSubview(self.durationLabel.view) + + self.playButton.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + } + + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + return self.backgroundView.frame.contains(point) + } + + @objc private func buttonPressed() { + self.pressed() + } + + func update(theme: PresentationTheme, size: CGSize, transition: ContainedViewLayoutTransition) { + var buttonSize = CGSize(width: 63.0, height: 22.0) + if size.width < 70.0 { + buttonSize.width = 27.0 + } + + let backgroundFrame = buttonSize.centered(in: CGRect(origin: .zero, size: size)) + transition.updateFrame(view: self.backgroundView, frame: backgroundFrame) + self.backgroundView.update(size: backgroundFrame.size, cornerRadius: backgroundFrame.height * 0.5, isDark: theme.overallDarkAppearance, tintColor: theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.4), transition: ComponentTransition(transition)) + + self.playPauseIconNode.frame = CGRect(origin: CGPoint(x: 3.0, y: 1.0 - UIScreenPixel), size: CGSize(width: 21.0, height: 21.0)) + + transition.updateFrame(node: self.durationLabel, frame: CGRect(origin: CGPoint(x: 18.0, y: 3.0), size: CGSize(width: 35.0, height: 20.0))) + transition.updateAlpha(node: self.durationLabel, alpha: buttonSize.width > 27.0 ? 1.0 : 0.0) + + self.playButton.frame = CGRect(origin: .zero, size: size) + } +} + +private final class ClippedWaveformNode: ASDisplayNode, CustomMediaPlayerScrubbingForegroundNode { + let waveformNode: AudioWaveformNode + let waveformLeftMaskView: UIImageView + let waveformRightMaskView: UIImageView + let waveformMaskView: UIView + let foregroundClippingContainer: ASDisplayNode + let foregroundWaveformNode: AudioWaveformNode + + var progress: CGFloat? { + didSet { + if self.progress != oldValue { + self.waveformNode.progress = self.progress + self.foregroundWaveformNode.progress = self.progress + } + } + } + + override var frame: CGRect { + didSet { + self.updateLayout() + } + } + + override var bounds: CGRect { + didSet { + self.updateLayout() + } + } + + override init() { + self.waveformNode = AudioWaveformNode() + + self.waveformMaskView = UIView() + self.waveformLeftMaskView = UIImageView() + self.waveformLeftMaskView.layer.anchorPoint = CGPoint(x: 1.0, y: 0.0) + self.waveformLeftMaskView.backgroundColor = .white + self.waveformMaskView.addSubview(self.waveformLeftMaskView) + self.waveformRightMaskView = UIImageView() + self.waveformRightMaskView.layer.anchorPoint = CGPoint() + self.waveformRightMaskView.backgroundColor = .white + self.waveformMaskView.addSubview(self.waveformRightMaskView) + + self.foregroundClippingContainer = ASDisplayNode() + self.foregroundClippingContainer.clipsToBounds = true + self.foregroundClippingContainer.anchorPoint = CGPoint() + + self.foregroundWaveformNode = AudioWaveformNode() + self.foregroundWaveformNode.isLayerBacked = true + self.foregroundClippingContainer.addSubnode(self.foregroundWaveformNode) + + super.init() + + self.addSubnode(self.waveformNode) + self.waveformNode.view.mask = self.waveformMaskView + self.addSubnode(self.foregroundClippingContainer) + } + + private func updateLayout() { + self.waveformNode.frame = CGRect(origin: CGPoint(), size: self.bounds.size) + self.foregroundWaveformNode.frame = CGRect(origin: CGPoint(), size: self.bounds.size) + + self.waveformLeftMaskView.bounds = CGRect(origin: CGPoint(), size: self.bounds.size) + self.waveformRightMaskView.bounds = CGRect(origin: CGPoint(), size: self.bounds.size) + } + + func updateClipping(minX: CGFloat, maxX: CGFloat, transition: ContainedViewLayoutTransition) { + let clippingFrame = CGRect(origin: CGPoint(x: minX, y: 0.0), size: CGSize(width: max(0.0, maxX - minX), height: 40.0 - 2.0 * 2.0)) + transition.updatePosition(node: self.foregroundClippingContainer, position: clippingFrame.origin) + transition.updateBounds(node: self.foregroundClippingContainer, bounds: CGRect(origin: CGPoint(x: minX, y: 0.0), size: clippingFrame.size)) + + transition.updatePosition(layer: self.waveformLeftMaskView.layer, position: CGPoint(x: minX, y: 0.0)) + transition.updatePosition(layer: self.waveformRightMaskView.layer, position: CGPoint(x: maxX, y: 0.0)) + } +} + +public final class ChatRecordingPreviewInputPanelNodeImpl: ChatInputPanelNode { + private let waveformButton: ASButtonNode + let waveformBackgroundNodeImpl: ASImageNode + var waveformBackgroundNode: ASDisplayNode { + return self.waveformBackgroundNodeImpl + } + + let trimViewImpl: TrimView + var trimView: UIView { + return self.trimViewImpl + } + let playButtonNodeImpl: PlayButtonNode + var playButtonNode: ASDisplayNode { + return self.playButtonNodeImpl + } + + let scrubber = ComponentView() + + private let waveformNode: ClippedWaveformNode + private let tintWaveformNode: AudioWaveformNode + private let waveformForegroundNode: AudioWaveformNode + let waveformScrubberNodeImpl: MediaPlayerScrubbingNode + var waveformScrubberNode: ASDisplayNode { + return self.waveformScrubberNodeImpl + } + + private var presentationInterfaceState: ChatPresentationInterfaceState? + + private var mediaPlayer: MediaPlayer? + + private var statusValue: MediaPlayerStatus? + private let statusDisposable = MetaDisposable() + private var scrubbingDisposable: Disposable? + + private var positionTimer: SwiftSignalKit.Timer? + + private(set) var gestureRecognizer: ContextGesture? + + public let tintMaskView: UIView = UIView() + + public init(theme: PresentationTheme) { + self.waveformBackgroundNodeImpl = ASImageNode() + self.waveformBackgroundNodeImpl.isLayerBacked = true + self.waveformBackgroundNodeImpl.displaysAsynchronously = false + self.waveformBackgroundNodeImpl.displayWithoutProcessing = true + self.waveformBackgroundNodeImpl.image = generateStretchableFilledCircleImage(diameter: 40.0 - 2.0 * 2.0, color: theme.list.itemCheckColors.fillColor) + + self.waveformButton = ASButtonNode() + self.waveformButton.accessibilityTraits.insert(.startsMediaSession) + + self.waveformNode = ClippedWaveformNode() + self.waveformForegroundNode = AudioWaveformNode() + self.waveformForegroundNode.isLayerBacked = true + + self.tintWaveformNode = AudioWaveformNode() + self.tintWaveformNode.isLayerBacked = true + + self.waveformScrubberNodeImpl = MediaPlayerScrubbingNode(content: .custom(backgroundNode: self.waveformNode, foregroundContentNode: self.waveformForegroundNode)) + + self.trimViewImpl = TrimView(frame: .zero) + self.trimViewImpl.isHollow = true + self.playButtonNodeImpl = PlayButtonNode(theme: theme) + + super.init() + + self.tintMaskView.layer.addSublayer(self.tintWaveformNode.layer) + + self.viewForOverlayContent = ChatRecordingPreviewViewForOverlayContent( + ignoreHit: { [weak self] view, point in + guard let strongSelf = self else { + return false + } + if strongSelf.view.hitTest(view.convert(point, to: strongSelf.view), with: nil) != nil { + return true + } + if view.convert(point, to: strongSelf.view).y > strongSelf.view.bounds.maxY { + return true + } + return false + } + ) + + self.addSubnode(self.waveformBackgroundNodeImpl) + self.addSubnode(self.waveformScrubberNode) + //self.addSubnode(self.waveformButton) + + self.view.addSubview(self.trimViewImpl) + self.addSubnode(self.playButtonNodeImpl) + + self.playButtonNodeImpl.pressed = { [weak self] in + guard let self else { + return + } + self.waveformPressed() + } + + self.waveformScrubberNodeImpl.seek = { [weak self] timestamp in + guard let self else { + return + } + var timestamp = timestamp + if let recordedMediaPreview = self.presentationInterfaceState?.interfaceState.mediaDraftState, case let .audio(audio) = recordedMediaPreview, let trimRange = audio.trimRange { + timestamp = max(trimRange.lowerBound, min(timestamp, trimRange.upperBound)) + } + self.mediaPlayer?.seek(timestamp: timestamp) + } + + 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.playButtonNodeImpl, alpha: value != nil ? 0.0 : 1.0) + }) + + self.waveformButton.addTarget(self, action: #selector(self.waveformPressed), forControlEvents: .touchUpInside) + } + + deinit { + self.mediaPlayer?.pause() + self.statusDisposable.dispose() + self.scrubbingDisposable?.dispose() + self.positionTimer?.invalidate() + } + + override public func didLoad() { + super.didLoad() + + self.view.disablesInteractiveTransitionGestureRecognizer = true + } + + private func ensureHasTimer() { + if self.positionTimer == nil { + let timer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in + self?.checkPosition() + }, queue: Queue.mainQueue()) + self.positionTimer = timer + timer.start() + } + } + + func checkPosition() { + guard let statusValue = self.statusValue, let recordedMediaPreview = self.presentationInterfaceState?.interfaceState.mediaDraftState, case let .audio(audio) = recordedMediaPreview, let trimRange = audio.trimRange, let mediaPlayer = self.mediaPlayer else { + return + } + let timestampSeconds: Double + if !statusValue.generationTimestamp.isZero { + timestampSeconds = statusValue.timestamp + (CACurrentMediaTime() - statusValue.generationTimestamp) + } else { + timestampSeconds = statusValue.timestamp + } + if timestampSeconds >= trimRange.upperBound { + mediaPlayer.seek(timestamp: trimRange.lowerBound, play: false) + } + } + + private func stopTimer() { + self.positionTimer?.invalidate() + self.positionTimer = nil + } + + private func maybePresentViewOnceTooltip() { + /*guard let context = self.context else { + return + } + let _ = (ApplicationSpecificNotice.getVoiceMessagesPlayOnceSuggestion(accountManager: context.sharedContext.accountManager) + |> deliverOnMainQueue).startStandalone(next: { [weak self] counter in + guard let self, let interfaceState = self.presentationInterfaceState else { + return + } + if counter >= 3 { + return + } + + Queue.mainQueue().after(0.3) { + self.displayViewOnceTooltip(text: interfaceState.strings.Chat_TapToPlayVoiceMessageOnceTooltip, hasIcon: true) + } + + let _ = ApplicationSpecificNotice.incrementVoiceMessagesPlayOnceSuggestion(accountManager: context.sharedContext.accountManager).startStandalone() + })*/ + } + + 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 innerSize = CGSize(width: 40.0, height: 40.0) + + let waveformBackgroundFrame = CGRect(origin: CGPoint(x: 2.0, y: 2.0), size: CGSize(width: width - 2.0 * 2.0, height: 40.0 - 2.0 * 2.0)) + + if self.presentationInterfaceState != interfaceState { + var updateWaveform = false + if self.presentationInterfaceState?.interfaceState.mediaDraftState != interfaceState.interfaceState.mediaDraftState { + updateWaveform = true + } + if self.presentationInterfaceState?.strings !== interfaceState.strings { + self.waveformButton.accessibilityLabel = interfaceState.strings.VoiceOver_Chat_RecordPreviewVoiceMessage + } + + self.presentationInterfaceState = interfaceState + + if let recordedMediaPreview = interfaceState.interfaceState.mediaDraftState, let context = self.context { + switch recordedMediaPreview { + case let .audio(audio): + self.waveformButton.isHidden = false + self.waveformBackgroundNodeImpl.isHidden = false + self.waveformForegroundNode.isHidden = false + self.waveformScrubberNodeImpl.isHidden = false + self.playButtonNodeImpl.isHidden = false + + if let view = self.scrubber.view, view.superview != nil { + view.removeFromSuperview() + } + + if updateWaveform { + self.waveformNode.waveformNode.setup(color: interfaceState.theme.chat.inputPanel.inputControlColor.withMultipliedAlpha(0.4), gravity: .center, waveform: audio.waveform) + self.waveformNode.foregroundWaveformNode.setup(color: interfaceState.theme.list.itemCheckColors.foregroundColor.withMultipliedAlpha(0.5), gravity: .center, waveform: audio.waveform) + self.tintWaveformNode.setup(color: UIColor(white: 0.0, alpha: 0.5), gravity: .center, waveform: audio.waveform) + self.waveformForegroundNode.setup(color: interfaceState.theme.list.itemCheckColors.foregroundColor, gravity: .center, waveform: audio.waveform) + if self.mediaPlayer != nil { + self.mediaPlayer?.pause() + } + let mediaManager = context.sharedContext.mediaManager + let mediaPlayer = MediaPlayer(audioSessionManager: mediaManager.audioSession, postbox: context.account.postbox, userLocation: .other, userContentType: .audio, resourceReference: .standalone(resource: audio.resource), streamable: .none, video: false, preferSoftwareDecoding: false, enableSound: true, fetchAutomatically: true) + mediaPlayer.actionAtEnd = .action { [weak self] in + guard let self else { + return + } + Queue.mainQueue().async { + guard let interfaceState = self.presentationInterfaceState else { + return + } + var timestamp: Double = 0.0 + if let recordedMediaPreview = interfaceState.interfaceState.mediaDraftState, case let .audio(audio) = recordedMediaPreview, let trimRange = audio.trimRange { + timestamp = trimRange.lowerBound + } + self.mediaPlayer?.seek(timestamp: timestamp, play: false) + } + } + self.mediaPlayer = mediaPlayer + 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 + if let self { + switch status.status { + case .playing, .buffering(_, true, _, _): + self.statusValue = status + if let recordedMediaPreview = self.presentationInterfaceState?.interfaceState.mediaDraftState, case let .audio(audio) = recordedMediaPreview, let _ = audio.trimRange { + self.ensureHasTimer() + } + self.playButtonNodeImpl.playPauseIconNode.enqueueState(.pause, animated: true) + default: + self.statusValue = nil + self.stopTimer() + self.playButtonNodeImpl.playPauseIconNode.enqueueState(.play, animated: true) + } + } + })) + } + + let minDuration = max(1.0, 56.0 * audio.duration / waveformBackgroundFrame.size.width) + let (leftHandleFrame, rightHandleFrame) = self.trimViewImpl.update( + style: .voiceMessage, + theme: interfaceState.theme, + visualInsets: .zero, + scrubberSize: waveformBackgroundFrame.size, + duration: audio.duration, + startPosition: audio.trimRange?.lowerBound ?? 0.0, + endPosition: audio.trimRange?.upperBound ?? Double(audio.duration), + position: 0.0, + minDuration: minDuration, + maxDuration: Double(audio.duration), + transition: .immediate + ) + + let waveformForegroundFrame = CGRect(origin: CGPoint(x: 2.0 + leftHandleFrame.minX, y: 2.0), size: CGSize(width: rightHandleFrame.maxX - leftHandleFrame.minX, height: 40.0 - 2.0 * 2.0)) + transition.updateFrame(node: self.waveformBackgroundNodeImpl, frame: waveformForegroundFrame) + + self.waveformNode.updateClipping(minX: leftHandleFrame.minX - 19.0, maxX: rightHandleFrame.maxX - 19.0, transition: transition) + + self.trimViewImpl.trimUpdated = { [weak self] start, end, updatedEnd, apply in + if let self { + self.mediaPlayer?.pause() + self.interfaceInteraction?.updateRecordingTrimRange(start, end, updatedEnd, apply) + if apply { + if !updatedEnd { + self.mediaPlayer?.seek(timestamp: start, play: true) + } else { + self.mediaPlayer?.seek(timestamp: max(0.0, end - 1.0), play: true) + } + self.playButtonNodeImpl.durationLabel.isScrubbing = false + Queue.mainQueue().after(0.1) { + self.waveformForegroundNode.alpha = 1.0 + } + } else { + self.playButtonNodeImpl.durationLabel.isScrubbing = true + self.waveformForegroundNode.alpha = 0.0 + } + + let startFraction = start / Double(audio.duration) + let endFraction = end / Double(audio.duration) + self.waveformForegroundNode.trimRange = startFraction ..< endFraction + } + } + 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.playButtonNodeImpl.update(theme: interfaceState.theme, 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.waveformBackgroundNodeImpl.isHidden = true + self.waveformForegroundNode.isHidden = true + self.waveformScrubberNodeImpl.isHidden = true + self.playButtonNodeImpl.isHidden = true + + let scrubberSize = self.scrubber.update( + transition: .immediate, + component: AnyComponent( + MediaScrubberComponent( + context: context, + style: .videoMessage, + theme: interfaceState.theme, + generationTimestamp: 0, + position: 0, + minDuration: 1.0, + maxDuration: 60.0, + isPlaying: false, + tracks: [ + MediaScrubberComponent.Track( + id: 0, + content: .video(frames: video.frames, framesUpdateTimestamp: video.framesUpdateTimestamp), + duration: Double(video.duration), + trimRange: video.trimRange, + offset: nil, + isMain: true + ) + ], + isCollage: false, + positionUpdated: { _, _ in }, + trackTrimUpdated: { [weak self] _, start, end, updatedEnd, apply in + if let self { + self.interfaceInteraction?.updateRecordingTrimRange(start, end, updatedEnd, apply) + } + }, + trackOffsetUpdated: { _, _, _ in }, + trackLongPressed: { _, _ in } + ) + ), + environment: {}, + forceUpdate: false, + containerSize: CGSize(width: min(424.0, width - leftInset - rightInset - innerSize.width - 1.0), height: 40.0) + ) + + if let view = self.scrubber.view { + if view.superview == nil { + self.view.addSubview(view) + } + view.bounds = CGRect(origin: .zero, size: scrubberSize) + } + } + } + } + + if let view = self.scrubber.view { + view.frame = CGRect(origin: CGPoint(x: min(width - innerSize.width - view.bounds.width, max(leftInset + 45.0, floorToScreenPixels((width - view.bounds.width) / 2.0))), y: 7.0 - UIScreenPixel), size: view.bounds.size) + } + + let panelHeight = 40.0 + + transition.updateFrame(node: self.waveformButton, frame: waveformBackgroundFrame) + + let waveformScrubberFrame = CGRect(origin: CGPoint(x: 21.0, y: floor((40.0 - 13.0) / 2.0)), size: CGSize(width: width - 21.0 * 2.0, height: 13.0)) + transition.updateFrame(node: self.waveformScrubberNodeImpl, frame: waveformScrubberFrame) + transition.updateFrame(node: self.tintWaveformNode, frame: waveformScrubberFrame) + + return panelHeight + } + + override public func canHandleTransition(from prevInputPanelNode: ChatInputPanelNode?) -> Bool { + return false + } + + @objc private func deletePressed() { + self.tooltipController?.dismiss() + + self.mediaPlayer?.pause() + self.interfaceInteraction?.deleteRecordedMedia() + } + + private weak var tooltipController: TooltipScreen? + + @objc private func recordMorePressed() { + self.tooltipController?.dismiss() + + self.interfaceInteraction?.resumeMediaRecording() + } + + /*private func displayViewOnceTooltip(text: String, hasIcon: Bool) { + guard let context = self.context, let parentController = self.interfaceInteraction?.chatController() else { + return + } + + let absoluteFrame = self.viewOnceButton.view.convert(self.viewOnceButton.bounds, to: parentController.view) + let location = CGRect(origin: CGPoint(x: absoluteFrame.midX - 20.0, y: absoluteFrame.midY), size: CGSize()) + + let tooltipController = TooltipScreen( + account: context.account, + sharedContext: context.sharedContext, + text: .markdown(text: text), + balancedTextLayout: true, + constrainWidth: 240.0, + style: .customBlur(UIColor(rgb: 0x18181a), 0.0), + arrowStyle: .small, + icon: hasIcon ? .animation(name: "anim_autoremove_on", delay: 0.1, tintColor: nil) : nil, + location: .point(location, .right), + displayDuration: .default, + inset: 8.0, + cornerRadius: 8.0, + shouldDismissOnTouch: { _, _ in + return .ignore + } + ) + self.tooltipController = tooltipController + + parentController.present(tooltipController, in: .current) + }*/ + + @objc private func waveformPressed() { + guard let mediaPlayer = self.mediaPlayer else { + return + } + if let recordedMediaPreview = self.presentationInterfaceState?.interfaceState.mediaDraftState, case let .audio(audio) = recordedMediaPreview, let trimRange = audio.trimRange { + let _ = (mediaPlayer.status + |> map(Optional.init) + |> timeout(0.3, queue: Queue.mainQueue(), alternate: .single(nil)) + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] status in + guard let self, let mediaPlayer = self.mediaPlayer else { + return + } + if let status { + if case .playing = status.status { + mediaPlayer.pause() + } else if status.timestamp <= trimRange.lowerBound { + mediaPlayer.seek(timestamp: trimRange.lowerBound, play: true) + } else { + mediaPlayer.play() + } + } else { + mediaPlayer.seek(timestamp: trimRange.lowerBound, play: true) + } + }) + } else { + mediaPlayer.togglePlayPause() + } + } + + override public func minimalHeight(interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { + return defaultHeight(metrics: metrics) + } +} + +private enum PlayPauseIconNodeState: Equatable { + case play + case pause +} + +private final class PlayPauseIconNode: ManagedAnimationNode { + private let duration: Double = 0.35 + private var iconState: PlayPauseIconNodeState = .pause + + init() { + super.init(size: CGSize(width: 21.0, height: 21.0)) + + self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 41), duration: 0.01)) + } + + func enqueueState(_ state: PlayPauseIconNodeState, animated: Bool) { + guard self.iconState != state else { + return + } + + let previousState = self.iconState + self.iconState = state + + switch previousState { + case .pause: + switch state { + case .play: + if animated { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 83), duration: self.duration)) + } else { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01)) + } + case .pause: + break + } + case .play: + switch state { + case .pause: + if animated { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 41), duration: self.duration)) + } else { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 41), duration: 0.01)) + } + case .play: + break + } + } + } +} diff --git a/submodules/TelegramUI/Components/Chat/ChatRecordingViewOnceButtonNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatRecordingViewOnceButtonNode/BUILD new file mode 100644 index 0000000000..65652a0a36 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatRecordingViewOnceButtonNode/BUILD @@ -0,0 +1,21 @@ +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", + "//submodules/TelegramUI/Components/GlassBackgroundComponent", + ], + 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..b94b71e7c6 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatRecordingViewOnceButtonNode/Sources/ChatRecordingViewOnceButtonNode.swift @@ -0,0 +1,110 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import TelegramPresentationData +import GlassBackgroundComponent + +public final class ChatRecordingViewOnceButtonNode: HighlightTrackingButtonNode { + public enum Icon { + case viewOnce + case recordMore + } + + private let icon: Icon + + private let backgroundView: GlassBackgroundView + private let iconNode: ASImageNode + + private var theme: PresentationTheme? + + public init(icon: Icon) { + self.icon = icon + + self.backgroundView = GlassBackgroundView() + self.backgroundView.isUserInteractionEnabled = false + + self.iconNode = ASImageNode() + self.iconNode.isUserInteractionEnabled = false + + super.init(pointerStyle: .default) + + self.view.addSubview(self.backgroundView) + 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 + + 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) + } + } + + let backgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(size.width / 2.0 - innerSize.width / 2.0), y: floorToScreenPixels(size.height / 2.0 - innerSize.height / 2.0)), size: innerSize) + self.backgroundView.frame = backgroundFrame + self.backgroundView.update(size: backgroundFrame.size, cornerRadius: backgroundFrame.height * 0.5, isDark: theme.overallDarkAppearance, tintColor: theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.65), transition: .immediate) + + 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..0d85c5c138 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputActionButtonsNode/BUILD @@ -0,0 +1,33 @@ +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", + "//submodules/TelegramUI/Components/GlassBackgroundComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatTextInputActionButtonsNode.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputActionButtonsNode/Sources/ChatTextInputActionButtonsNode.swift similarity index 92% rename from submodules/TelegramUI/Sources/ChatTextInputActionButtonsNode.swift rename to submodules/TelegramUI/Components/Chat/ChatTextInputActionButtonsNode/Sources/ChatTextInputActionButtonsNode.swift index 306c0d879e..7e87297a08 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputActionButtonsNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputActionButtonsNode/Sources/ChatTextInputActionButtonsNode.swift @@ -128,33 +128,34 @@ 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 micButtonBackgroundView: GlassBackgroundView - let micButtonTintMaskView: UIImageView - let micButton: ChatTextInputMediaRecordingButton + public let micButtonBackgroundView: GlassBackgroundView + public let micButtonTintMaskView: UIImageView + public let micButton: ChatTextInputMediaRecordingButton - let sendContainerNode: ASDisplayNode - let sendButtonBackgroundView: GlassBackgroundView - let sendButton: HighlightTrackingButtonNode - var sendButtonRadialStatusNode: ChatSendButtonRadialStatusNode? - var sendButtonHasApplyIcon = false - var animatingSendButton = false + public let sendContainerNode: ASDisplayNode + public let sendButtonBackgroundView: GlassBackgroundView + 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: HighlightTrackingButton + public let expandMediaInputButton: HighlightTrackingButton private let expandMediaInputButtonBackgroundView: GlassBackgroundView private let expandMediaInputButtonIcon: GlassBackgroundView.ContentImageView + 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 } @@ -165,7 +166,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 @@ -244,7 +245,7 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessageAction } } - override func didLoad() { + override public func didLoad() { super.didLoad() let gestureRecognizer = ContextGesture(target: nil, action: nil) @@ -261,13 +262,13 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessageAction self.sendButtonPointerInteraction = PointerInteraction(view: self.sendButton.view, customInteractionView: self.sendButtonBackgroundView, style: .lift) } - func updateTheme(theme: PresentationTheme, wallpaper: TelegramWallpaper) { + public func updateTheme(theme: PresentationTheme, wallpaper: TelegramWallpaper) { self.micButton.updateTheme(theme: theme) self.expandMediaInputButtonIcon.tintColor = theme.chat.inputPanel.inputControlColor } 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) @@ -278,7 +279,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 @@ -381,7 +382,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 { @@ -398,7 +399,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..5d22432726 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputAudioRecordingCancelIndicator/BUILD @@ -0,0 +1,21 @@ +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", + "//submodules/TelegramUI/Components/GlassBackgroundComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatTextInputAudioRecordingCancelIndicator.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputAudioRecordingCancelIndicator/Sources/ChatTextInputAudioRecordingCancelIndicator.swift similarity index 57% rename from submodules/TelegramUI/Sources/ChatTextInputAudioRecordingCancelIndicator.swift rename to submodules/TelegramUI/Components/Chat/ChatTextInputAudioRecordingCancelIndicator/Sources/ChatTextInputAudioRecordingCancelIndicator.swift index 5d94685c16..1535781c7b 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputAudioRecordingCancelIndicator.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputAudioRecordingCancelIndicator/Sources/ChatTextInputAudioRecordingCancelIndicator.swift @@ -3,32 +3,40 @@ import UIKit import AsyncDisplayKit import Display import TelegramPresentationData +import GlassBackgroundComponent private let cancelFont = Font.regular(17.0) -final class ChatTextInputAudioRecordingCancelIndicator: ASDisplayNode { +public final class ChatTextInputAudioRecordingCancelIndicator: UIView, GlassBackgroundView.ContentView { private let cancel: () -> Void - private let arrowNode: ASImageNode + private let arrowView: GlassBackgroundView.ContentImageView private let labelNode: TextNode + private let tintLabelNode: TextNode private let cancelButton: HighlightableButtonNode private let strings: PresentationStrings - private(set) var isDisplayingCancel = false + public let tintMask: UIView - init(theme: PresentationTheme, strings: PresentationStrings, cancel: @escaping () -> Void) { + public private(set) var isDisplayingCancel = false + + public init(theme: PresentationTheme, strings: PresentationStrings, cancel: @escaping () -> Void) { + self.tintMask = UIView() + self.cancel = cancel - self.arrowNode = ASImageNode() - self.arrowNode.isLayerBacked = true - self.arrowNode.displayWithoutProcessing = true - self.arrowNode.displaysAsynchronously = false - self.arrowNode.image = PresentationResourcesChat.chatInputPanelMediaRecordingCancelArrowImage(theme) + self.arrowView = GlassBackgroundView.ContentImageView() + self.arrowView.image = UIImage(bundleImageName: "Chat/Input/Text/AudioRecordingCancelArrow")?.withRenderingMode(.alwaysTemplate) + self.arrowView.tintColor = theme.chat.inputPanel.inputControlColor self.labelNode = TextNode() self.labelNode.displaysAsynchronously = false self.labelNode.isUserInteractionEnabled = false + self.tintLabelNode = TextNode() + self.tintLabelNode.displaysAsynchronously = false + self.tintLabelNode.isUserInteractionEnabled = false + self.cancelButton = HighlightableButtonNode() self.cancelButton.setTitle(strings.Common_Cancel, with: cancelFont, with: theme.chat.inputPanel.panelControlAccentColor, for: []) self.cancelButton.alpha = 0.0 @@ -37,21 +45,28 @@ final class ChatTextInputAudioRecordingCancelIndicator: ASDisplayNode { self.strings = strings - super.init() + super.init(frame: CGRect()) - self.addSubnode(self.arrowNode) - self.addSubnode(self.labelNode) + self.addSubview(self.arrowView) + self.tintMask.addSubview(self.arrowView.tintMask) + + self.addSubview(self.labelNode.view) + self.tintMask.addSubview(self.tintLabelNode.view) self.addSubnode(self.cancelButton) let makeLayout = TextNode.asyncLayout(self.labelNode) - let (labelLayout, labelApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: strings.Conversation_SlideToCancel, font: Font.regular(14.0), textColor: theme.chat.inputPanel.panelControlColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 200.0, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let makeTintLayout = TextNode.asyncLayout(self.tintLabelNode) + let (labelLayout, labelApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: strings.Conversation_SlideToCancel, font: Font.regular(14.0), textColor: theme.chat.inputPanel.inputControlColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 200.0, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (_, tintLabelApply) = makeTintLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: strings.Conversation_SlideToCancel, font: Font.regular(14.0), textColor: .black), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 200.0, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let _ = labelApply() + let _ = tintLabelApply - let arrowSize = self.arrowNode.image?.size ?? CGSize() + let arrowSize = self.arrowView.image?.size ?? CGSize() let height = max(arrowSize.height, labelLayout.size.height) self.frame = CGRect(origin: CGPoint(), size: CGSize(width: arrowSize.width + 12.0 + labelLayout.size.width, height: height)) - self.arrowNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floor((height - arrowSize.height) / 2.0)), size: arrowSize) + self.arrowView.frame = CGRect(origin: CGPoint(x: 0.0, y: floor((height - arrowSize.height) / 2.0)), size: arrowSize) self.labelNode.frame = CGRect(origin: CGPoint(x: arrowSize.width + 6.0, y: 1.0 + floor((height - labelLayout.size.height) / 2.0)), size: labelLayout.size) + self.tintLabelNode.frame = self.labelNode.frame let cancelSize = self.cancelButton.measure(CGSize(width: 200.0, height: 100.0)) self.cancelButton.frame = CGRect(origin: CGPoint(x: floor((self.bounds.size.width - cancelSize.width) / 2.0), y: floor((height - cancelSize.height) / 2.0)), size: cancelSize) @@ -59,19 +74,26 @@ final class ChatTextInputAudioRecordingCancelIndicator: ASDisplayNode { self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside) } - 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) - let (_, labelApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: strings.Conversation_SlideToCancel, font: Font.regular(14.0), textColor: theme.chat.inputPanel.panelControlColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 200.0, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let _ = labelApply() + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") } - func updateIsDisplayingCancel(_ isDisplayingCancel: Bool, animated: Bool) { + public func updateTheme(theme: PresentationTheme) { + self.arrowView.tintColor = theme.chat.inputPanel.inputControlColor + self.cancelButton.setTitle(self.strings.Common_Cancel, with: cancelFont, with: theme.chat.inputPanel.panelControlAccentColor, for: []) + let makeLayout = TextNode.asyncLayout(self.labelNode) + let makeTintLayout = TextNode.asyncLayout(self.tintLabelNode) + let (_, labelApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: strings.Conversation_SlideToCancel, font: Font.regular(14.0), textColor: theme.chat.inputPanel.actionControlForegroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 200.0, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (_, tintLabelApply) = makeTintLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: strings.Conversation_SlideToCancel, font: Font.regular(14.0), textColor: .black), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 200.0, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let _ = labelApply() + let _ = tintLabelApply() + } + + public func updateIsDisplayingCancel(_ isDisplayingCancel: Bool, animated: Bool) { if self.isDisplayingCancel != isDisplayingCancel { self.isDisplayingCancel = isDisplayingCancel if isDisplayingCancel { - self.arrowNode.alpha = 0.0 + self.arrowView.alpha = 0.0 self.labelNode.alpha = 0.0 self.cancelButton.alpha = 1.0 @@ -85,17 +107,17 @@ final class ChatTextInputAudioRecordingCancelIndicator: ASDisplayNode { self.cancelButton.layer.animatePosition(from: CGPoint(x: 0.0, y: 22.0), to: CGPoint(), duration: 0.2, additive: true) self.cancelButton.layer.animateScale(from: 0.25, to: 1.0, duration: 0.25) - self.arrowNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25) + self.arrowView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25) self.labelNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25) self.cancelButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } } else { - self.arrowNode.alpha = 1.0 + self.arrowView.alpha = 1.0 self.labelNode.alpha = 1.0 self.cancelButton.alpha = 0.0 if animated { - self.arrowNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + self.arrowView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) self.labelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) self.cancelButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25) } @@ -103,22 +125,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 90% rename from submodules/TelegramUI/Sources/ChatTextInputAudioRecordingTimeNode.swift rename to submodules/TelegramUI/Components/Chat/ChatTextInputAudioRecordingTimeNode/Sources/ChatTextInputAudioRecordingTimeNode.swift index 415eba149f..1d705e4041 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputAudioRecordingTimeNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputAudioRecordingTimeNode/Sources/ChatTextInputAudioRecordingTimeNode.swift @@ -19,9 +19,9 @@ private final class ChatTextInputAudioRecordingTimeNodeParameters: NSObject { } } -private let textFont = Font.regular(15.0) +private let textFont = Font.with(size: 15.0, design: .camera) -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..7d9fb24ceb --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/BUILD @@ -0,0 +1,69 @@ +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", + "//submodules/TelegramUI/Components/GlassBackgroundComponent", + "//submodules/TelegramUI/Components/Chat/ChatInputAccessoryPanel", + "//submodules/TelegramUI/Components/Chat/ChatRecordingPreviewInputPanelNode", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/AccessoryItemIconButton.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/AccessoryItemIconButton.swift new file mode 100644 index 0000000000..ce7409da94 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/AccessoryItemIconButton.swift @@ -0,0 +1,365 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import TelegramPresentationData +import ChatPresentationInterfaceState +import GlassBackgroundComponent +import ComponentFlow +import LottieAnimationComponent +import LottieComponent + +private let accessoryButtonFont = Font.medium(14.0) + +final class AccessoryItemIconButton: HighlightTrackingButton, GlassBackgroundView.ContentView { + private var item: ChatTextInputAccessoryItem + private var theme: PresentationTheme + private var strings: PresentationStrings + private var width: CGFloat + private let iconImageView: UIImageView + private let tintMaskIconImageView: UIImageView + private var textView: ImmediateTextView? + private var tintMaskTextView: ImmediateTextView? + private var animationView: ComponentView? + private var tintMaskAnimationView: UIImageView? + + override static var layerClass: AnyClass { + return GlassBackgroundView.ContentLayer.self + } + + let tintMask = UIView() + + init(item: ChatTextInputAccessoryItem, theme: PresentationTheme, strings: PresentationStrings) { + self.item = item + self.theme = theme + self.strings = strings + + self.iconImageView = UIImageView() + self.tintMaskIconImageView = UIImageView() + + let (image, text, accessibilityLabel, alpha, _) = AccessoryItemIconButton.imageAndInsets(item: item, theme: theme, strings: strings) + + self.width = AccessoryItemIconButton.calculateWidth(item: item, image: image, text: text, strings: strings) + + super.init(frame: CGRect()) + + (self.layer as? GlassBackgroundView.ContentLayer)?.targetLayer = self.tintMask.layer + + self.isAccessibilityElement = true + self.accessibilityTraits = [.button] + + self.iconImageView.isUserInteractionEnabled = false + self.addSubview(self.iconImageView) + + self.tintMask.addSubview(self.tintMaskIconImageView) + + switch item { + case .input, .botInput, .silentPost: + self.iconImageView.isHidden = true + self.tintMaskIconImageView.isHidden = self.iconImageView.isHidden + self.animationView = ComponentView() + self.tintMaskAnimationView = UIImageView() + default: + break + } + + if let text { + if self.textView == nil { + let textView = ImmediateTextView() + self.textView = textView + self.addSubview(textView) + } + if self.tintMaskTextView == nil { + let tintMaskTextView = ImmediateTextView() + self.tintMaskTextView = tintMaskTextView + self.tintMask.addSubview(tintMaskTextView) + } + + self.textView?.attributedText = NSAttributedString(string: text, font: accessoryButtonFont, textColor: theme.chat.inputPanel.inputControlColor) + self.tintMaskTextView?.attributedText = NSAttributedString(string: text, font: accessoryButtonFont, textColor: .black) + } else { + if let textView = self.textView { + self.textView = nil + textView.removeFromSuperview() + } + if let tintMaskTextView = self.tintMaskTextView { + self.tintMaskTextView = nil + tintMaskTextView.removeFromSuperview() + } + } + + self.iconImageView.image = image + self.iconImageView.tintColor = theme.chat.inputPanel.inputControlColor + self.iconImageView.alpha = alpha + + self.tintMaskIconImageView.image = self.iconImageView.image + self.tintMaskIconImageView.tintColor = .black + self.tintMaskIconImageView.alpha = self.iconImageView.alpha + + self.accessibilityLabel = accessibilityLabel + + self.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.layer.removeAnimation(forKey: "opacity") + strongSelf.alpha = 0.4 + strongSelf.layer.allowsGroupOpacity = true + } else { + strongSelf.alpha = 1.0 + strongSelf.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + strongSelf.layer.allowsGroupOpacity = false + } + } + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + guard let result = super.hitTest(point, with: event) else { + return nil + } + return result + } + + func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { + self.theme = theme + self.strings = strings + + let (image, text, accessibilityLabel, alpha, _) = AccessoryItemIconButton.imageAndInsets(item: item, theme: theme, strings: strings) + + self.width = AccessoryItemIconButton.calculateWidth(item: item, image: image, text: text, strings: strings) + + if let text { + self.textView?.attributedText = NSAttributedString(string: text, font: accessoryButtonFont, textColor: theme.chat.inputPanel.inputControlColor) + self.tintMaskTextView?.attributedText = NSAttributedString(string: text, font: accessoryButtonFont, textColor: .black) + } + + self.iconImageView.image = image + self.iconImageView.tintColor = theme.chat.inputPanel.inputControlColor + self.iconImageView.alpha = alpha + + self.tintMaskIconImageView.image = self.iconImageView.image + self.tintMaskIconImageView.tintColor = .black + self.tintMaskIconImageView.alpha = self.iconImageView.alpha + + self.accessibilityLabel = accessibilityLabel + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private static func imageAndInsets(item: ChatTextInputAccessoryItem, theme: PresentationTheme, strings: PresentationStrings) -> (UIImage?, String?, String, CGFloat, UIEdgeInsets) { + switch item { + case let .input(isEnabled, inputMode), let .botInput(isEnabled, inputMode): + switch inputMode { + case .keyboard: + return (PresentationResourcesChat.chatInputTextFieldKeyboardImage(theme), nil, strings.VoiceOver_Keyboard, 1.0, UIEdgeInsets()) + case .stickers, .emoji: + return (PresentationResourcesChat.chatInputTextFieldStickersImage(theme), nil, strings.VoiceOver_Stickers, isEnabled ? 1.0 : 0.4, UIEdgeInsets()) + case .bot: + return (PresentationResourcesChat.chatInputTextFieldInputButtonsImage(theme), nil, strings.VoiceOver_BotKeyboard, 1.0, UIEdgeInsets()) + } + case .commands: + return (PresentationResourcesChat.chatInputTextFieldCommandsImage(theme), nil, strings.VoiceOver_BotCommands, 1.0, UIEdgeInsets()) + case let .silentPost(value): + if value { + return (PresentationResourcesChat.chatInputTextFieldSilentPostOnImage(theme), nil, strings.VoiceOver_SilentPostOn, 1.0, UIEdgeInsets()) + } else { + return (PresentationResourcesChat.chatInputTextFieldSilentPostOffImage(theme), nil, strings.VoiceOver_SilentPostOff, 1.0, UIEdgeInsets()) + } + case .suggestPost: + return (PresentationResourcesChat.chatInputTextFieldSuggestPostImage(theme), nil, strings.VoiceOver_SuggestPost, 1.0, UIEdgeInsets()) + case let .messageAutoremoveTimeout(timeout): + if let timeout = timeout { + return (nil, shortTimeIntervalString(strings: strings, value: timeout), strings.VoiceOver_SelfDestructTimerOn(timeIntervalString(strings: strings, value: timeout)).string, 1.0, UIEdgeInsets()) + } else { + return (PresentationResourcesChat.chatInputTextFieldTimerImage(theme), nil, strings.VoiceOver_SelfDestructTimerOff, 1.0, UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)) + } + case .scheduledMessages: + return (PresentationResourcesChat.chatInputTextFieldScheduleImage(theme), nil, strings.VoiceOver_ScheduledMessages, 1.0, UIEdgeInsets()) + case .gift: + return (PresentationResourcesChat.chatInputTextFieldGiftImage(theme), nil, strings.VoiceOver_GiftPremium, 1.0, UIEdgeInsets()) + } + } + + private static func calculateWidth(item: ChatTextInputAccessoryItem, image: UIImage?, text: String?, strings: PresentationStrings) -> CGFloat { + switch item { + case .input, .botInput, .silentPost, .commands, .scheduledMessages, .gift, .suggestPost: + return 32.0 + case let .messageAutoremoveTimeout(timeout): + var imageWidth = (image?.size.width ?? 0.0) + CGFloat(8.0) + if let _ = timeout, let text = text { + imageWidth = ceil((text as NSString).size(withAttributes: [.font: accessoryButtonFont]).width) + 10.0 + } + + return max(imageWidth, 24.0) + } + } + + func updateLayout(item: ChatTextInputAccessoryItem, size: CGSize) { + let previousItem = self.item + self.item = item + + let (updatedImage, text, _, _, _) = AccessoryItemIconButton.imageAndInsets(item: item, theme: self.theme, strings: self.strings) + + if let image = self.iconImageView.image { + self.iconImageView.image = updatedImage + self.tintMaskIconImageView.image = updatedImage + + let bottomInset: CGFloat = 0.0 + let imageFrame = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0) - bottomInset), size: image.size) + self.iconImageView.frame = imageFrame + self.tintMaskIconImageView.frame = imageFrame + + if let animationView = self.animationView { + let width = AccessoryItemIconButton.calculateWidth(item: item, image: image, text: "", strings: self.strings) + + let animationFrame = CGRect(origin: CGPoint(x: floor((size.width - width) / 2.0), y: floor((size.height - width) / 2.0) - bottomInset), size: CGSize(width: width, height: width)) + + let animationName: String + var animationMode: LottieAnimationComponent.AnimationItem.Mode = .still(position: .end) + + if case let .silentPost(muted) = item { + if case let .silentPost(previousMuted) = previousItem { + if muted { + animationName = "input_anim_channelMute" + } else { + animationName = "input_anim_channelUnmute" + } + if muted != previousMuted { + animationMode = .animating(loop: false) + } + } else { + animationName = "input_anim_channelMute" + } + } else { + var previousInputMode: ChatTextInputAccessoryItem.InputMode? + var inputMode: ChatTextInputAccessoryItem.InputMode? + + switch previousItem { + case let .input(_, itemInputMode), let .botInput(_, itemInputMode): + previousInputMode = itemInputMode + default: + break + } + switch item { + case let .input(_, itemInputMode), let .botInput(_, itemInputMode): + inputMode = itemInputMode + default: + break + } + + if let inputMode = inputMode { + switch inputMode { + case .keyboard: + if let previousInputMode = previousInputMode { + if case .stickers = previousInputMode { + animationName = "input_anim_stickerToKey" + animationMode = .animating(loop: false) + } else if case .emoji = previousInputMode { + animationName = "input_anim_smileToKey" + animationMode = .animating(loop: false) + } else if case .bot = previousInputMode { + animationName = "input_anim_botToKey" + animationMode = .animating(loop: false) + } else { + animationName = "input_anim_stickerToKey" + } + } else { + animationName = "input_anim_stickerToKey" + } + case .stickers: + if let previousInputMode = previousInputMode { + if case .keyboard = previousInputMode { + animationName = "input_anim_keyToSticker" + animationMode = .animating(loop: false) + } else if case .emoji = previousInputMode { + animationName = "input_anim_smileToSticker" + animationMode = .animating(loop: false) + } else { + animationName = "input_anim_keyToSticker" + } + } else { + animationName = "input_anim_keyToSticker" + } + case .emoji: + if let previousInputMode = previousInputMode { + if case .keyboard = previousInputMode { + animationName = "input_anim_keyToSmile" + animationMode = .animating(loop: false) + } else if case .stickers = previousInputMode { + animationName = "input_anim_stickerToSmile" + animationMode = .animating(loop: false) + } else { + animationName = "input_anim_keyToSmile" + } + } else { + animationName = "input_anim_keyToSmile" + } + case .bot: + if let previousInputMode = previousInputMode { + if case .keyboard = previousInputMode { + animationName = "input_anim_keyToBot" + animationMode = .animating(loop: false) + } else { + animationName = "input_anim_keyToBot" + } + } else { + animationName = "input_anim_keyToBot" + } + } + } else { + animationName = "" + } + } + + let animationSize = animationView.update( + transition: .immediate, + component: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent(name: animationName), + color: self.theme.chat.inputPanel.inputControlColor + )), + environment: {}, + containerSize: animationFrame.size + ) + if let view = animationView.view as? LottieComponent.View { + view.isUserInteractionEnabled = false + if view.superview == nil { + view.output = self.tintMaskAnimationView + self.addSubview(view) + if let tintMaskAnimationView = self.tintMaskAnimationView { + self.tintMask.addSubview(tintMaskAnimationView) + } + } + let animationFrameValue = CGRect(origin: CGPoint(x: animationFrame.minX + floor((animationFrame.width - animationSize.width) / 2.0), y: animationFrame.minY + floor((animationFrame.height - animationSize.height) / 2.0)), size: animationSize) + view.frame = animationFrameValue + if let tintMaskAnimationView = self.tintMaskAnimationView { + tintMaskAnimationView.frame = animationFrameValue + } + + if case .animating = animationMode { + view.playOnce() + } + } + } + } + + if let text { + self.textView?.attributedText = NSAttributedString(string: text, font: accessoryButtonFont, textColor: theme.chat.inputPanel.inputControlColor) + self.tintMaskTextView?.attributedText = NSAttributedString(string: text, font: accessoryButtonFont, textColor: .black) + } + + if let textView = self.textView, let tintMaskTextView = self.tintMaskTextView { + let textSize = textView.updateLayout(CGSize(width: 100.0, height: 100.0)) + let _ = tintMaskTextView.updateLayout(CGSize(width: 100.0, height: 100.0)) + + let textFrame = CGRect(origin: CGPoint(x: floor((size.width - textSize.width) * 0.5), y: floor((size.height - textSize.height) * 0.5)), size: textSize) + textView.frame = textFrame + tintMaskTextView.frame = textFrame + } + } + + var buttonWidth: CGFloat { + return self.width + } +} diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/BoostSlowModeButton.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/BoostSlowModeButton.swift new file mode 100644 index 0000000000..a3c2700e73 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/BoostSlowModeButton.swift @@ -0,0 +1,130 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import TelegramPresentationData +import SwiftSignalKit +import ChatPresentationInterfaceState +import AnimatedCountLabelNode +import TelegramStringFormatting + +private func generateClearImage(color: UIColor) -> UIImage? { + return generateImage(CGSize(width: 17.0, height: 17.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(color.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + context.setBlendMode(.copy) + context.setStrokeColor(UIColor.clear.cgColor) + context.setLineCap(.round) + context.setLineWidth(1.66) + context.move(to: CGPoint(x: 6.0, y: 6.0)) + context.addLine(to: CGPoint(x: 11.0, y: 11.0)) + context.strokePath() + context.move(to: CGPoint(x: size.width - 6.0, y: 6.0)) + context.addLine(to: CGPoint(x: size.width - 11.0, y: 11.0)) + context.strokePath() + }) +} + + +final class BoostSlowModeButton: HighlightTrackingButtonNode { + let containerNode: ASDisplayNode + let backgroundNode: ASImageNode + let textNode: ImmediateAnimatedCountLabelNode + let iconNode: ASImageNode + + private var updateTimer: SwiftSignalKit.Timer? + + var requestUpdate: () -> Void = {} + + override init(pointerStyle: PointerStyle? = nil) { + self.containerNode = ASDisplayNode() + + self.backgroundNode = ASImageNode() + self.backgroundNode.displaysAsynchronously = false + self.backgroundNode.clipsToBounds = true + self.backgroundNode.image = generateGradientImage(size: CGSize(width: 100.0, height: 2.0), scale: 1.0, colors: [UIColor(rgb: 0x9076ff), UIColor(rgb: 0xbc6de8)], locations: [0.0, 1.0], direction: .horizontal) + + self.iconNode = ASImageNode() + self.iconNode.displaysAsynchronously = false + self.iconNode.image = generateClearImage(color: .white) + + self.textNode = ImmediateAnimatedCountLabelNode() + self.textNode.alwaysOneDirection = true + self.textNode.isUserInteractionEnabled = false + + super.init(pointerStyle: pointerStyle) + + self.addSubnode(self.containerNode) + self.containerNode.addSubnode(self.backgroundNode) + self.containerNode.addSubnode(self.iconNode) + self.containerNode.addSubnode(self.textNode) + + self.highligthedChanged = { [weak self] highlighted in + if let self { + if highlighted { + self.containerNode.layer.animateScale(from: 1.0, to: 0.75, duration: 0.4, removeOnCompletion: false) + } else if let presentationLayer = self.containerNode.layer.presentation() { + self.containerNode.layer.animateScale(from: CGFloat((presentationLayer.value(forKeyPath: "transform.scale.y") as? NSNumber)?.floatValue ?? 1.0), to: 1.0, duration: 0.25, removeOnCompletion: false) + } + } + } + } + + func update(size: CGSize, interfaceState: ChatPresentationInterfaceState) -> CGSize { + var text = "" + if let slowmodeState = interfaceState.slowmodeState { + let relativeTimestamp: CGFloat + switch slowmodeState.variant { + case let .timestamp(validUntilTimestamp): + let timestamp = CGFloat(Date().timeIntervalSince1970) + relativeTimestamp = CGFloat(validUntilTimestamp) - timestamp + case .pendingMessages: + relativeTimestamp = CGFloat(slowmodeState.timeout) + } + + self.updateTimer?.invalidate() + + if relativeTimestamp >= 0.0 { + text = stringForDuration(Int32(relativeTimestamp)) + + self.updateTimer = SwiftSignalKit.Timer(timeout: 1.0 / 60.0, repeat: false, completion: { [weak self] in + self?.requestUpdate() + }, queue: .mainQueue()) + self.updateTimer?.start() + } + } else { + self.updateTimer?.invalidate() + self.updateTimer = nil + } + + let font = Font.with(size: 15.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]) + let textColor = UIColor.white + + var segments: [AnimatedCountLabelNode.Segment] = [] + var textCount = 0 + + for char in text { + if let intValue = Int(String(char)) { + segments.append(.number(intValue, NSAttributedString(string: String(char), font: font, textColor: textColor))) + } else { + segments.append(.text(textCount, NSAttributedString(string: String(char), font: font, textColor: textColor))) + textCount += 1 + } + } + self.textNode.segments = segments + + let textSize = self.textNode.updateLayout(size: CGSize(width: 200.0, height: 100.0), animated: true) + let totalSize = CGSize(width: textSize.width > 0.0 ? textSize.width + 38.0 : 33.0, height: 33.0) + + self.containerNode.bounds = CGRect(origin: .zero, size: totalSize) + self.containerNode.position = CGPoint(x: totalSize.width / 2.0, y: totalSize.height / 2.0) + self.backgroundNode.frame = CGRect(origin: .zero, size: totalSize) + self.backgroundNode.cornerRadius = totalSize.height / 2.0 + self.textNode.frame = CGRect(origin: CGPoint(x: 9.0, y: floorToScreenPixels((totalSize.height - textSize.height) / 2.0)), size: textSize) + if let icon = self.iconNode.image { + self.iconNode.frame = CGRect(origin: CGPoint(x: totalSize.width - icon.size.width - 7.0, y: floorToScreenPixels((totalSize.height - icon.size.height) / 2.0)), size: icon.size) + } + return totalSize + } +} diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift similarity index 81% rename from submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift rename to submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift index 5729398bea..6e2e2e1176 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift @@ -50,364 +50,16 @@ import PhotoResources import GlassBackgroundComponent import ComponentDisplayAdapters import ChatInputAccessoryPanel +import ChatTextInputSlowmodePlaceholderNode +import ChatTextInputActionButtonsNode +import ChatTextInputAudioRecordingTimeNode +import ChatTextInputAudioRecordingCancelIndicator +import ChatRecordingViewOnceButtonNode +import ChatRecordingPreviewInputPanelNode -private let accessoryButtonFont = Font.medium(14.0) private let counterFont = Font.with(size: 14.0, design: .regular, traits: [.monospacedNumbers]) -private final class AccessoryItemIconButton: HighlightTrackingButton, GlassBackgroundView.ContentView { - private var item: ChatTextInputAccessoryItem - private var theme: PresentationTheme - private var strings: PresentationStrings - private var width: CGFloat - private let iconImageView: UIImageView - private let tintMaskIconImageView: UIImageView - private var textView: ImmediateTextView? - private var tintMaskTextView: ImmediateTextView? - private var animationView: ComponentView? - private var tintMaskAnimationView: UIImageView? - - override static var layerClass: AnyClass { - return GlassBackgroundView.ContentLayer.self - } - - let tintMask = UIView() - - init(item: ChatTextInputAccessoryItem, theme: PresentationTheme, strings: PresentationStrings) { - self.item = item - self.theme = theme - self.strings = strings - - self.iconImageView = UIImageView() - self.tintMaskIconImageView = UIImageView() - - let (image, text, accessibilityLabel, alpha, _) = AccessoryItemIconButton.imageAndInsets(item: item, theme: theme, strings: strings) - - self.width = AccessoryItemIconButton.calculateWidth(item: item, image: image, text: text, strings: strings) - - super.init(frame: CGRect()) - - (self.layer as? GlassBackgroundView.ContentLayer)?.targetLayer = self.tintMask.layer - - self.isAccessibilityElement = true - self.accessibilityTraits = [.button] - - self.iconImageView.isUserInteractionEnabled = false - self.addSubview(self.iconImageView) - - self.tintMask.addSubview(self.tintMaskIconImageView) - - switch item { - case .input, .botInput, .silentPost: - self.iconImageView.isHidden = true - self.tintMaskIconImageView.isHidden = self.iconImageView.isHidden - self.animationView = ComponentView() - self.tintMaskAnimationView = UIImageView() - default: - break - } - - if let text { - if self.textView == nil { - let textView = ImmediateTextView() - self.textView = textView - self.addSubview(textView) - } - if self.tintMaskTextView == nil { - let tintMaskTextView = ImmediateTextView() - self.tintMaskTextView = tintMaskTextView - self.tintMask.addSubview(tintMaskTextView) - } - - self.textView?.attributedText = NSAttributedString(string: text, font: accessoryButtonFont, textColor: theme.chat.inputPanel.inputControlColor) - self.tintMaskTextView?.attributedText = NSAttributedString(string: text, font: accessoryButtonFont, textColor: .black) - } else { - if let textView = self.textView { - self.textView = nil - textView.removeFromSuperview() - } - if let tintMaskTextView = self.tintMaskTextView { - self.tintMaskTextView = nil - tintMaskTextView.removeFromSuperview() - } - } - - self.iconImageView.image = image - self.iconImageView.tintColor = theme.chat.inputPanel.inputControlColor - self.iconImageView.alpha = alpha - - self.tintMaskIconImageView.image = self.iconImageView.image - self.tintMaskIconImageView.tintColor = .black - self.tintMaskIconImageView.alpha = self.iconImageView.alpha - - self.accessibilityLabel = accessibilityLabel - - self.highligthedChanged = { [weak self] highlighted in - if let strongSelf = self { - if highlighted { - strongSelf.layer.removeAnimation(forKey: "opacity") - strongSelf.alpha = 0.4 - strongSelf.layer.allowsGroupOpacity = true - } else { - strongSelf.alpha = 1.0 - strongSelf.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) - strongSelf.layer.allowsGroupOpacity = false - } - } - } - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - guard let result = super.hitTest(point, with: event) else { - return nil - } - return result - } - - func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { - self.theme = theme - self.strings = strings - - let (image, text, accessibilityLabel, alpha, _) = AccessoryItemIconButton.imageAndInsets(item: item, theme: theme, strings: strings) - - self.width = AccessoryItemIconButton.calculateWidth(item: item, image: image, text: text, strings: strings) - - if let text { - self.textView?.attributedText = NSAttributedString(string: text, font: accessoryButtonFont, textColor: theme.chat.inputPanel.inputControlColor) - self.tintMaskTextView?.attributedText = NSAttributedString(string: text, font: accessoryButtonFont, textColor: .black) - } - - self.iconImageView.image = image - self.iconImageView.tintColor = theme.chat.inputPanel.inputControlColor - self.iconImageView.alpha = alpha - - self.tintMaskIconImageView.image = self.iconImageView.image - self.tintMaskIconImageView.tintColor = .black - self.tintMaskIconImageView.alpha = self.iconImageView.alpha - - self.accessibilityLabel = accessibilityLabel - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private static func imageAndInsets(item: ChatTextInputAccessoryItem, theme: PresentationTheme, strings: PresentationStrings) -> (UIImage?, String?, String, CGFloat, UIEdgeInsets) { - switch item { - case let .input(isEnabled, inputMode), let .botInput(isEnabled, inputMode): - switch inputMode { - case .keyboard: - return (PresentationResourcesChat.chatInputTextFieldKeyboardImage(theme), nil, strings.VoiceOver_Keyboard, 1.0, UIEdgeInsets()) - case .stickers, .emoji: - return (PresentationResourcesChat.chatInputTextFieldStickersImage(theme), nil, strings.VoiceOver_Stickers, isEnabled ? 1.0 : 0.4, UIEdgeInsets()) - case .bot: - return (PresentationResourcesChat.chatInputTextFieldInputButtonsImage(theme), nil, strings.VoiceOver_BotKeyboard, 1.0, UIEdgeInsets()) - } - case .commands: - return (PresentationResourcesChat.chatInputTextFieldCommandsImage(theme), nil, strings.VoiceOver_BotCommands, 1.0, UIEdgeInsets()) - case let .silentPost(value): - if value { - return (PresentationResourcesChat.chatInputTextFieldSilentPostOnImage(theme), nil, strings.VoiceOver_SilentPostOn, 1.0, UIEdgeInsets()) - } else { - return (PresentationResourcesChat.chatInputTextFieldSilentPostOffImage(theme), nil, strings.VoiceOver_SilentPostOff, 1.0, UIEdgeInsets()) - } - case .suggestPost: - return (PresentationResourcesChat.chatInputTextFieldSuggestPostImage(theme), nil, strings.VoiceOver_SuggestPost, 1.0, UIEdgeInsets()) - case let .messageAutoremoveTimeout(timeout): - if let timeout = timeout { - return (nil, shortTimeIntervalString(strings: strings, value: timeout), strings.VoiceOver_SelfDestructTimerOn(timeIntervalString(strings: strings, value: timeout)).string, 1.0, UIEdgeInsets()) - } else { - return (PresentationResourcesChat.chatInputTextFieldTimerImage(theme), nil, strings.VoiceOver_SelfDestructTimerOff, 1.0, UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)) - } - case .scheduledMessages: - return (PresentationResourcesChat.chatInputTextFieldScheduleImage(theme), nil, strings.VoiceOver_ScheduledMessages, 1.0, UIEdgeInsets()) - case .gift: - return (PresentationResourcesChat.chatInputTextFieldGiftImage(theme), nil, strings.VoiceOver_GiftPremium, 1.0, UIEdgeInsets()) - } - } - - private static func calculateWidth(item: ChatTextInputAccessoryItem, image: UIImage?, text: String?, strings: PresentationStrings) -> CGFloat { - switch item { - case .input, .botInput, .silentPost, .commands, .scheduledMessages, .gift, .suggestPost: - return 32.0 - case let .messageAutoremoveTimeout(timeout): - var imageWidth = (image?.size.width ?? 0.0) + CGFloat(8.0) - if let _ = timeout, let text = text { - imageWidth = ceil((text as NSString).size(withAttributes: [.font: accessoryButtonFont]).width) + 10.0 - } - - return max(imageWidth, 24.0) - } - } - - func updateLayout(item: ChatTextInputAccessoryItem, size: CGSize) { - let previousItem = self.item - self.item = item - - let (updatedImage, text, _, _, _) = AccessoryItemIconButton.imageAndInsets(item: item, theme: self.theme, strings: self.strings) - - if let image = self.iconImageView.image { - self.iconImageView.image = updatedImage - self.tintMaskIconImageView.image = updatedImage - - let bottomInset: CGFloat = 0.0 - let imageFrame = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0) - bottomInset), size: image.size) - self.iconImageView.frame = imageFrame - self.tintMaskIconImageView.frame = imageFrame - - if let animationView = self.animationView { - let width = AccessoryItemIconButton.calculateWidth(item: item, image: image, text: "", strings: self.strings) - - let animationFrame = CGRect(origin: CGPoint(x: floor((size.width - width) / 2.0), y: floor((size.height - width) / 2.0) - bottomInset), size: CGSize(width: width, height: width)) - - let animationName: String - var animationMode: LottieAnimationComponent.AnimationItem.Mode = .still(position: .end) - - if case let .silentPost(muted) = item { - if case let .silentPost(previousMuted) = previousItem { - if muted { - animationName = "input_anim_channelMute" - } else { - animationName = "input_anim_channelUnmute" - } - if muted != previousMuted { - animationMode = .animating(loop: false) - } - } else { - animationName = "input_anim_channelMute" - } - } else { - var previousInputMode: ChatTextInputAccessoryItem.InputMode? - var inputMode: ChatTextInputAccessoryItem.InputMode? - - switch previousItem { - case let .input(_, itemInputMode), let .botInput(_, itemInputMode): - previousInputMode = itemInputMode - default: - break - } - switch item { - case let .input(_, itemInputMode), let .botInput(_, itemInputMode): - inputMode = itemInputMode - default: - break - } - - if let inputMode = inputMode { - switch inputMode { - case .keyboard: - if let previousInputMode = previousInputMode { - if case .stickers = previousInputMode { - animationName = "input_anim_stickerToKey" - animationMode = .animating(loop: false) - } else if case .emoji = previousInputMode { - animationName = "input_anim_smileToKey" - animationMode = .animating(loop: false) - } else if case .bot = previousInputMode { - animationName = "input_anim_botToKey" - animationMode = .animating(loop: false) - } else { - animationName = "input_anim_stickerToKey" - } - } else { - animationName = "input_anim_stickerToKey" - } - case .stickers: - if let previousInputMode = previousInputMode { - if case .keyboard = previousInputMode { - animationName = "input_anim_keyToSticker" - animationMode = .animating(loop: false) - } else if case .emoji = previousInputMode { - animationName = "input_anim_smileToSticker" - animationMode = .animating(loop: false) - } else { - animationName = "input_anim_keyToSticker" - } - } else { - animationName = "input_anim_keyToSticker" - } - case .emoji: - if let previousInputMode = previousInputMode { - if case .keyboard = previousInputMode { - animationName = "input_anim_keyToSmile" - animationMode = .animating(loop: false) - } else if case .stickers = previousInputMode { - animationName = "input_anim_stickerToSmile" - animationMode = .animating(loop: false) - } else { - animationName = "input_anim_keyToSmile" - } - } else { - animationName = "input_anim_keyToSmile" - } - case .bot: - if let previousInputMode = previousInputMode { - if case .keyboard = previousInputMode { - animationName = "input_anim_keyToBot" - animationMode = .animating(loop: false) - } else { - animationName = "input_anim_keyToBot" - } - } else { - animationName = "input_anim_keyToBot" - } - } - } else { - animationName = "" - } - } - - let animationSize = animationView.update( - transition: .immediate, - component: AnyComponent(LottieComponent( - content: LottieComponent.AppBundleContent(name: animationName), - color: self.theme.chat.inputPanel.inputControlColor - )), - environment: {}, - containerSize: animationFrame.size - ) - if let view = animationView.view as? LottieComponent.View { - view.isUserInteractionEnabled = false - if view.superview == nil { - view.output = self.tintMaskAnimationView - self.addSubview(view) - if let tintMaskAnimationView = self.tintMaskAnimationView { - self.tintMask.addSubview(tintMaskAnimationView) - } - } - let animationFrameValue = CGRect(origin: CGPoint(x: animationFrame.minX + floor((animationFrame.width - animationSize.width) / 2.0), y: animationFrame.minY + floor((animationFrame.height - animationSize.height) / 2.0)), size: animationSize) - view.frame = animationFrameValue - if let tintMaskAnimationView = self.tintMaskAnimationView { - tintMaskAnimationView.frame = animationFrameValue - } - - if case .animating = animationMode { - view.playOnce() - } - } - } - } - - if let text { - self.textView?.attributedText = NSAttributedString(string: text, font: accessoryButtonFont, textColor: theme.chat.inputPanel.inputControlColor) - self.tintMaskTextView?.attributedText = NSAttributedString(string: text, font: accessoryButtonFont, textColor: .black) - } - - if let textView = self.textView, let tintMaskTextView = self.tintMaskTextView { - let textSize = textView.updateLayout(CGSize(width: 100.0, height: 100.0)) - let _ = tintMaskTextView.updateLayout(CGSize(width: 100.0, height: 100.0)) - - let textFrame = CGRect(origin: CGPoint(x: floor((size.width - textSize.width) * 0.5), y: floor((size.height - textSize.height) * 0.5)), size: textSize) - textView.frame = textFrame - tintMaskTextView.frame = textFrame - } - } - - var buttonWidth: CGFloat { - return self.width - } -} - -let chatTextInputMinFontSize: CGFloat = 5.0 +public let chatTextInputMinFontSize: CGFloat = 5.0 private let minInputFontSize = chatTextInputMinFontSize @@ -459,7 +111,7 @@ private func calculateTextFieldRealInsets(presentationInterfaceState: ChatPresen return UIEdgeInsets(top: 4.5 + top, left: 0.0, bottom: 5.5 + bottom, right: right) } -enum ChatTextInputPanelPasteData { +public enum ChatTextInputPanelPasteData { case images([UIImage]) case video(Data) case gif(Data) @@ -557,63 +209,63 @@ private func makeTextInputTheme(context: AccountContext, interfaceState: ChatPre ) } -class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, ChatInputTextNodeDelegate { - let clippingNode: ASDisplayNode - let textPlaceholderNode: ImmediateTextNodeWithEntities - let tintMaskTextPlaceholderNode: ImmediateTextNodeWithEntities +public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, ChatInputTextNodeDelegate { + public let clippingNode: ASDisplayNode + public let textPlaceholderNode: ImmediateTextNodeWithEntities + public let tintMaskTextPlaceholderNode: ImmediateTextNodeWithEntities - var textLockIconNode: ASImageNode? - var contextPlaceholderNode: TextNode? - var slowmodePlaceholderNode: ChatTextInputSlowmodePlaceholderNode? - let textInputContainerBackgroundView: GlassBackgroundView - let textInputContainer: ASDisplayNode - let textInputNodeClippingContainer: ASDisplayNode - let textInputSeparator: GlassBackgroundView.ContentColorView - var textInputNode: ChatInputTextNode? - var dustNode: InvisibleInkDustNode? - var customEmojiContainerView: CustomEmojiContainerView? + public var textLockIconNode: ASImageNode? + public var contextPlaceholderNode: TextNode? + public var slowmodePlaceholderNode: ChatTextInputSlowmodePlaceholderNode? + public let textInputContainerBackgroundView: GlassBackgroundView + public let textInputContainer: ASDisplayNode + public let textInputNodeClippingContainer: ASDisplayNode + public let textInputSeparator: GlassBackgroundView.ContentColorView + public var textInputNode: ChatInputTextNode? + public var dustNode: InvisibleInkDustNode? + public var customEmojiContainerView: CustomEmojiContainerView? - let textInputBackgroundNode: ASImageNode - var textInputBackgroundTapRecognizer: TouchDownGestureRecognizer? - let actionButtons: ChatTextInputActionButtonsNode + public let textInputBackgroundNode: ASImageNode + public var textInputBackgroundTapRecognizer: TouchDownGestureRecognizer? + public let actionButtons: ChatTextInputActionButtonsNode private let slowModeButton: BoostSlowModeButton - var mediaRecordingAccessibilityArea: AccessibilityAreaNode? + public var mediaRecordingAccessibilityArea: AccessibilityAreaNode? private let counterTextNode: ImmediateTextNode - let menuButton: HighlightTrackingButtonNode - private let menuButtonBackgroundNode: ASDisplayNode + public let menuButton: HighlightTrackingButtonNode + private let menuButtonBackgroundView: GlassBackgroundView private let menuButtonClippingNode: ASDisplayNode private let menuButtonIconNode: MenuIconNode private let menuButtonTextNode: ImmediateTextNode 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: HighlightTrackingButton - let attachmentButtonBackground: GlassBackgroundView - let attachmentButtonIcon: GlassBackgroundView.ContentImageView - let attachmentButtonDisabledNode: HighlightableButtonNode + public let attachmentButton: HighlightTrackingButton + public let attachmentButtonBackground: GlassBackgroundView + public let attachmentButtonIcon: GlassBackgroundView.ContentImageView + 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 audioRecordingDotView: UIImageView? + public var audioRecordingDotNodeDismissed = false + public var audioRecordingTimeNode: ChatTextInputAudioRecordingTimeNode? + public var audioRecordingCancelIndicator: ChatTextInputAudioRecordingCancelIndicator? - var viewOnce = false - let viewOnceButton: ChatRecordingViewOnceButtonNode + public var viewOnce = false + public let viewOnceButton: ChatRecordingViewOnceButtonNode private var accessoryPanel: (component: AnyComponentWithIdentity, view: ComponentView)? + private var mediaPreviewPanelNode: ChatRecordingPreviewInputPanelNodeImpl? private var accessoryItemButtons: [(ChatTextInputAccessoryItem, AccessoryItemIconButton)] = [] @@ -622,14 +274,15 @@ 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)? + public var textInputAccessoryPanel: ((_ context: AccountContext, _ chatPresentationInterfaceState: ChatPresentationInterfaceState, _ chatControllerInteraction: ChatControllerInteraction?, _ interfaceInteraction: ChatPanelInterfaceInteraction?) -> AnyComponentWithIdentity?)? - var updateActivity: () -> Void = { } + public var updateActivity: () -> Void = { } private var updatingInputState = false @@ -642,7 +295,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 @@ -652,7 +305,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) @@ -661,8 +314,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 { @@ -670,7 +323,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 @@ -678,18 +331,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 @@ -701,7 +354,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 { @@ -775,7 +428,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 @@ -783,7 +436,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } } - var text: String { + public var text: String { get { return self.textInputNode?.attributedText?.string ?? "" } set(value) { @@ -807,17 +460,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 @@ -881,8 +534,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch self.menuButton.clipsToBounds = true self.menuButton.cornerRadius = 16.0 self.menuButton.accessibilityLabel = presentationInterfaceState.strings.Conversation_InputMenu - self.menuButtonBackgroundNode = ASDisplayNode() - self.menuButtonBackgroundNode.isUserInteractionEnabled = false + self.menuButtonBackgroundView = GlassBackgroundView() + self.menuButtonBackgroundView.isUserInteractionEnabled = false self.menuButtonClippingNode = ASDisplayNode() self.menuButtonClippingNode.clipsToBounds = true self.menuButtonClippingNode.isUserInteractionEnabled = false @@ -1118,7 +771,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch self.textInputContainerBackgroundView.contentView.addSubview(self.textPlaceholderNode.view) self.textInputContainerBackgroundView.maskContentView.addSubview(self.tintMaskTextPlaceholderNode.view) - self.menuButton.addSubnode(self.menuButtonBackgroundNode) + self.menuButton.view.addSubview(self.menuButtonBackgroundView) self.menuButton.addSubnode(self.menuButtonClippingNode) self.menuButtonClippingNode.addSubnode(self.menuButtonTextNode) self.menuButton.addSubnode(self.menuButtonIconNode) @@ -1146,7 +799,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 } @@ -1155,7 +808,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() } @@ -1200,7 +853,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch self.currentEmojiSuggestion?.disposable.dispose() } - override func didLoad() { + override public func didLoad() { super.didLoad() if let viewForOverlayContent = self.viewForOverlayContent { @@ -1208,7 +861,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } } - func loadTextInputNodeIfNeeded() { + public func loadTextInputNodeIfNeeded() { if self.textInputNode == nil { self.loadTextInputNode() } @@ -1435,7 +1088,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 { @@ -1548,7 +1201,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 { @@ -1563,14 +1216,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( + override public func updateLayout( width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, @@ -1596,15 +1249,30 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } } + var accessoryPanel: AnyComponentWithIdentity? + if let context = self.context { + accessoryPanel = self.textInputAccessoryPanel?( + context, + interfaceState, + self.chatControllerInteraction, + self.interfaceInteraction + ) + } + var wasEditingMedia = false - if let interfaceState = self.presentationInterfaceState, let editMessageState = interfaceState.editMessageState { - if case let .media(value) = editMessageState.content { - wasEditingMedia = !value.isEmpty + var hadMediaDraft = false + if let interfaceState = self.presentationInterfaceState { + if let editMessageState = interfaceState.editMessageState { + if case let .media(value) = editMessageState.content { + wasEditingMedia = !value.isEmpty + } } + hadMediaDraft = interfaceState.interfaceState.mediaDraftState != nil } var isMediaEnabled = true var isEditingMedia = false + var hasMediaDraft = false if let editMessageState = interfaceState.editMessageState { if case let .media(value) = editMessageState.content { isEditingMedia = !value.isEmpty @@ -1620,6 +1288,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch isMediaEnabled = true } } + hasMediaDraft = interfaceState.interfaceState.mediaDraftState != nil var isRecording = false if let _ = interfaceState.inputTextPanelState.mediaRecordingState { @@ -1846,9 +1515,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch let buttonTitle: String if case let .webView(title, _) = interfaceState.botMenuButton { - buttonTitle = title + buttonTitle = title.uppercased() } else { - buttonTitle = interfaceState.strings.Conversation_InputMenu + buttonTitle = interfaceState.strings.Conversation_InputMenu.uppercased() } buttonTitleUpdated = self.menuButtonTextNode.attributedText != nil && self.menuButtonTextNode.attributedText?.string != buttonTitle @@ -1912,9 +1581,10 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch self.theme = interfaceState.theme - self.menuButtonBackgroundNode.backgroundColor = interfaceState.theme.chat.inputPanel.actionControlFillColor - - if isEditingMedia { + if interfaceState.interfaceState.mediaDraftState != nil { + self.attachmentButtonIcon.image = UIImage(bundleImageName: "Chat/Context Menu/Delete")?.withRenderingMode(.alwaysTemplate) + self.attachmentButtonIcon.tintColor = interfaceState.theme.chat.inputPanel.inputControlColor + } else if isEditingMedia { self.attachmentButtonIcon.image = PresentationResourcesChat.chatInputPanelEditAttachmentButtonImage(interfaceState.theme) self.attachmentButtonIcon.tintColor = interfaceState.theme.chat.inputPanel.inputControlColor } else { @@ -1942,8 +1612,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } } - if wasEditingMedia != isEditingMedia { - if isEditingMedia { + if wasEditingMedia != isEditingMedia || hadMediaDraft != hasMediaDraft { + if interfaceState.interfaceState.mediaDraftState != nil { + self.attachmentButtonIcon.image = UIImage(bundleImageName: "Chat/Context Menu/Delete")?.withRenderingMode(.alwaysTemplate) + self.attachmentButtonIcon.tintColor = interfaceState.theme.chat.inputPanel.inputControlColor + } else if isEditingMedia { self.attachmentButtonIcon.image = PresentationResourcesChat.chatInputPanelEditAttachmentButtonImage(interfaceState.theme) self.attachmentButtonIcon.tintColor = interfaceState.theme.chat.inputPanel.actionControlForegroundColor } else { @@ -2143,11 +1816,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } let leftMenuInset: CGFloat - let menuButtonHeight: CGFloat = 33.0 - let menuCollapsedButtonWidth: CGFloat = isSendAsButton ? menuButtonHeight : 38.0 + let menuButtonHeight: CGFloat = 40.0 + let menuCollapsedButtonWidth: CGFloat = isSendAsButton ? menuButtonHeight : 40.0 let menuButtonWidth = menuTextSize.width + 47.0 if hasMenuButton { - let menuButtonSpacing: CGFloat = 10.0 + let menuButtonSpacing: CGFloat = 6.0 if menuButtonExpanded { leftMenuInset = menuButtonWidth + menuButtonSpacing } else { @@ -2174,11 +1847,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch var textInputBackgroundWidthOffset: CGFloat = 0.0 var attachmentButtonX: CGFloat = hideOffset.x + leftInset + leftMenuInset + 8.0 - if !displayMediaButton { - attachmentButtonX = -40.0 - let inputFieldAdditionalWidth = 40.0 - 4.0 - leftInset -= inputFieldAdditionalWidth - textInputBackgroundWidthOffset += inputFieldAdditionalWidth + if !displayMediaButton || mediaRecordingState != nil { + attachmentButtonX = -48.0 } let baseWidth = width - leftInset - leftMenuInset - rightInset - rightSlowModeInset @@ -2195,17 +1865,18 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch menuButtonOriginY = panelHeight - minimalHeight + floorToScreenPixels((minimalHeight - menuButtonHeight) / 2.0) } - let menuButtonFrame = CGRect(x: leftInset + 10.0, y: menuButtonOriginY, width: menuButtonExpanded ? menuButtonWidth : menuCollapsedButtonWidth, height: menuButtonHeight) + let menuButtonFrame = CGRect(x: leftInset + 8.0, y: menuButtonOriginY, width: menuButtonExpanded ? menuButtonWidth : menuCollapsedButtonWidth, height: menuButtonHeight) transition.updateFrameAsPositionAndBounds(node: self.menuButton, frame: menuButtonFrame) - transition.updateFrame(node: self.menuButtonBackgroundNode, frame: CGRect(origin: CGPoint(), size: menuButtonFrame.size)) + transition.updateFrame(view: self.menuButtonBackgroundView, frame: CGRect(origin: CGPoint(), size: menuButtonFrame.size)) + self.menuButtonBackgroundView.update(size: menuButtonFrame.size, cornerRadius: menuButtonFrame.height * 0.5, isDark: interfaceState.theme.overallDarkAppearance, tintColor: interfaceState.theme.chat.inputPanel.actionControlFillColor, transition: ComponentTransition(transition)) transition.updateFrame(node: self.menuButtonClippingNode, frame: CGRect(origin: CGPoint(x: 19.0, y: 0.0), size: CGSize(width: menuButtonWidth - 19.0, height: menuButtonFrame.height))) var menuButtonTitleTransition = transition if buttonTitleUpdated { menuButtonTitleTransition = .immediate } - menuButtonTitleTransition.updateFrame(node: self.menuButtonTextNode, frame: CGRect(origin: CGPoint(x: 16.0, y: 7.0 - UIScreenPixel), size: menuTextSize)) + menuButtonTitleTransition.updateFrame(node: self.menuButtonTextNode, frame: CGRect(origin: CGPoint(x: 16.0, y: 10.0), size: menuTextSize)) transition.updateAlpha(node: self.menuButtonTextNode, alpha: menuButtonExpanded ? 1.0 : 0.0) - transition.updateFrame(node: self.menuButtonIconNode, frame: CGRect(x: isSendAsButton ? 1.0 + UIScreenPixel : (4.0 + UIScreenPixel), y: 1.0 + UIScreenPixel, width: 30.0, height: 30.0)) + transition.updateFrame(node: self.menuButtonIconNode, frame: CGRect(x: isSendAsButton ? 4.0 + UIScreenPixel : (4.0 + UIScreenPixel), y: isSendAsButton ? 5.0 : (5.0 - UIScreenPixel), width: 30.0, height: 30.0)) transition.updateFrame(node: self.sendAsAvatarButtonNode, frame: menuButtonFrame) transition.updateFrame(node: self.sendAsAvatarContainerNode, frame: CGRect(origin: CGPoint(), size: menuButtonFrame.size)) @@ -2239,14 +1910,40 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch self.actionButtons.micButton.updateMode(mode: interfaceState.interfaceState.mediaRecordingMode, animated: transition.isAnimated) - var hideMicButton = false - var audioRecordingItemsAlpha: CGFloat = 1 - if mediaRecordingState != nil || (interfaceState.interfaceState.mediaDraftState != nil && self.finishedTransitionToPreview != true) { - if interfaceState.interfaceState.mediaDraftState != nil { - self.finishedTransitionToPreview = false + var actionButtonsSize = CGSize(width: 40.0, height: 40.0) + if let presentationInterfaceState = self.presentationInterfaceState { + var showTitle = false + if !self.actionButtons.sendContainerNode.alpha.isZero { + if let _ = presentationInterfaceState.sendPaidMessageStars { + showTitle = true + } else if case let .customChatContents(customChatContents) = interfaceState.subject { + switch customChatContents.kind { + default: + break + } + } } - - audioRecordingItemsAlpha = 0 + actionButtonsSize = self.actionButtons.updateLayout(size: CGSize(width: 40.0, height: minimalHeight), isMediaInputExpanded: isMediaInputExpanded, showTitle: showTitle, currentMessageEffectId: presentationInterfaceState.interfaceState.sendMessageEffect, transition: transition, interfaceState: presentationInterfaceState) + } + + var textFieldInsets = self.textFieldInsets(metrics: metrics) + if actionButtonsSize.width > 40.0 { + textFieldInsets.right = actionButtonsSize.width - 2.0 + } + if additionalSideInsets.right > 0.0 { + textFieldInsets.right += additionalSideInsets.right / 3.0 + } + if mediaRecordingState != nil { + textFieldInsets.left = 8.0 + } + + var hideMicButton = false + var audioRecordingItemsAlpha: CGFloat = 1.0 + if interfaceState.interfaceState.mediaDraftState != nil { + audioRecordingItemsAlpha = 0.0 + } + if mediaRecordingState != nil { + audioRecordingItemsAlpha = 0.0 let audioRecordingInfoContainerNode: ASDisplayNode if let currentAudioRecordingInfoContainerNode = self.audioRecordingInfoContainerNode { @@ -2254,7 +1951,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } else { audioRecordingInfoContainerNode = ASDisplayNode() self.audioRecordingInfoContainerNode = audioRecordingInfoContainerNode - self.clippingNode.insertSubnode(audioRecordingInfoContainerNode, at: 0) + self.clippingNode.addSubnode(audioRecordingInfoContainerNode) } var animateTimeSlideIn = false @@ -2284,7 +1981,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch self?.tooltipController?.dismiss() }) self.audioRecordingCancelIndicator = audioRecordingCancelIndicator - self.clippingNode.insertSubnode(audioRecordingCancelIndicator, at: 0) + self.textInputContainerBackgroundView.contentView.addSubview(audioRecordingCancelIndicator) } let isLocked = mediaRecordingState?.isLocked ?? (interfaceState.interfaceState.mediaDraftState != nil) @@ -2325,9 +2022,6 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch if let textInputNode = self.textInputNode { transition.updateAlpha(node: textInputNode, alpha: 0.0) } - for (_, button) in self.accessoryItemButtons { - transition.updateAlpha(layer: button.layer, alpha: 0.0) - } let cancelTransformThreshold: CGFloat = 8.0 @@ -2335,8 +2029,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch let audioRecordingCancelIndicatorFrame = CGRect( origin: CGPoint( - x: leftInset + floor((baseWidth - audioRecordingCancelIndicator.bounds.size.width - indicatorTranslation) / 2.0), - y: panelHeight - minimalHeight + floor((minimalHeight - audioRecordingCancelIndicator.bounds.size.height) / 2.0)), + x: leftInset + floor((baseWidth - leftInset * 2.0 - 16.0 - audioRecordingCancelIndicator.bounds.size.width - indicatorTranslation) / 2.0), + y: (accessoryPanel != nil ? 52.0 : 0.0) + panelHeight - minimalHeight + floor((minimalHeight - audioRecordingCancelIndicator.bounds.size.height) / 2.0)), size: audioRecordingCancelIndicator.bounds.size) audioRecordingCancelIndicator.frame = audioRecordingCancelIndicatorFrame if self.actionButtons.micButton.cancelTranslation > cancelTransformThreshold { @@ -2383,104 +2077,76 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch size: CGSize(width: baseWidth, height: panelHeight) ) - audioRecordingTimeNode.frame = CGRect(origin: CGPoint(x: 40.0, y: panelHeight - minimalHeight + floor((minimalHeight - audioRecordingTimeSize.height) / 2.0)), size: audioRecordingTimeSize) + let audioRecordingTimeFrame = CGRect(origin: CGPoint(x: hideOffset.x + leftInset + leftMenuInset + 8.0 + 34.0, y: (accessoryPanel != nil ? 52.0 : 0.0) + panelHeight - minimalHeight + floor((minimalHeight - audioRecordingTimeSize.height) / 2.0) + 1.0 - UIScreenPixel), size: audioRecordingTimeSize) + if animateTimeSlideIn { - let position = audioRecordingTimeNode.layer.position - audioRecordingTimeNode.layer.animatePosition(from: CGPoint(x: position.x - 10.0, y: position.y), to: position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + var previousAudioRecordingTimeFrame = audioRecordingTimeFrame + previousAudioRecordingTimeFrame.origin.x = self.textInputContainer.frame.minX + 34.0 + audioRecordingTimeNode.frame = previousAudioRecordingTimeFrame + audioRecordingTimeNode.layer.animateAlpha(from: 0, to: 1, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) } - let dotFrame = CGRect(origin: CGPoint(x: leftInset + 2.0 - UIScreenPixel, y: audioRecordingTimeNode.frame.midY - 20), size: CGSize(width: 40.0, height: 40)) + transition.updateFrame(node: audioRecordingTimeNode, frame: audioRecordingTimeFrame) + + let dotFrame = CGRect(origin: CGPoint(x: hideOffset.x + leftInset + leftMenuInset + 8.0 + 16.0, y: audioRecordingTimeNode.frame.midY - 5.0), size: CGSize(width: 10.0, height: 10.0)) var animateDotAppearing = false - let audioRecordingDotNode: AnimationNode - if let currentAudioRecordingDotNode = self.audioRecordingDotNode, !currentAudioRecordingDotNode.didPlay { - audioRecordingDotNode = currentAudioRecordingDotNode + let audioRecordingDotView: UIImageView + if let current = self.audioRecordingDotView { + audioRecordingDotView = current + + transition.updatePosition(layer: audioRecordingDotView.layer, position: dotFrame.center) } else { - self.audioRecordingDotNode?.removeFromSupernode() - audioRecordingDotNode = AnimationNode(animation: "BinRed") + animateDotAppearing = true + audioRecordingDotView = UIImageView() + audioRecordingDotView.image = generateStretchableFilledCircleImage(diameter: 10.0, color: UIColor(rgb: 0xFF2D55)) - self.audioRecordingDotNode = audioRecordingDotNode - self.audioRecordingDotNodeDismissed = false - self.clippingNode.insertSubnode(audioRecordingDotNode, belowSubnode: self.menuButton) - audioRecordingDotNode.frame = dotFrame + self.audioRecordingDotView = audioRecordingDotView + self.clippingNode.view.insertSubview(audioRecordingDotView, belowSubview: self.menuButton.view) - self.animatingBinNode?.removeFromSupernode() - self.animatingBinNode = nil + let previousDotFrame = CGRect(origin: CGPoint(x: self.textInputContainer.frame.minX + 16.0, y: dotFrame.minY), size: dotFrame.size) + audioRecordingDotView.center = previousDotFrame.center + + transition.updatePosition(layer: audioRecordingDotView.layer, position: dotFrame.center) } - var resumingRecording = false - animateDotAppearing = transition.isAnimated && !hideInfo - if let mediaRecordingState = mediaRecordingState { - if case .waitingForPreview = mediaRecordingState { - self.recordingPaused = true - animateDotAppearing = false - } else { - if self.recordingPaused { - self.recordingPaused = false - resumingRecording = true - - if (audioRecordingDotNode.layer.animationKeys() ?? []).isEmpty { - animateDotAppearing = true - } - } - } - } - - audioRecordingDotNode.bounds = CGRect(origin: .zero, size: dotFrame.size) - audioRecordingDotNode.position = dotFrame.center + audioRecordingDotView.bounds = CGRect(origin: .zero, size: dotFrame.size) if animateDotAppearing { - Queue.mainQueue().justDispatch { - audioRecordingDotNode.layer.animateScale(from: 0.3, to: 1, duration: 0.15, delay: 0, removeOnCompletion: false) - - let animateDot = { [weak audioRecordingDotNode] in - if let audioRecordingDotNode, audioRecordingDotNode.layer.animation(forKey: "recording") == nil { - audioRecordingDotNode.layer.animateAlpha(from: CGFloat(audioRecordingDotNode.layer.presentation()?.opacity ?? 0), to: 1, duration: 0.15, delay: 0, completion: { [weak audioRecordingDotNode] finished in - if finished { - let animation = CAKeyframeAnimation(keyPath: "opacity") - animation.values = [1.0 as NSNumber, 1.0 as NSNumber, 0.0 as NSNumber] - animation.keyTimes = [0.0 as NSNumber, 0.4546 as NSNumber, 0.9091 as NSNumber, 1 as NSNumber] - animation.duration = 0.5 - animation.autoreverses = true - animation.repeatCount = Float.infinity - - audioRecordingDotNode?.layer.add(animation, forKey: "recording") - } - }) - } + audioRecordingDotView.layer.animateScale(from: 0.3, to: 1, duration: 0.15, delay: 0, removeOnCompletion: false) + + audioRecordingDotView.layer.animateAlpha(from: CGFloat(audioRecordingDotView.layer.presentation()?.opacity ?? 0), to: 1, duration: 0.15, delay: 0, completion: { [weak audioRecordingDotView] finished in + if finished { + let animation = CAKeyframeAnimation(keyPath: "opacity") + animation.values = [1.0 as NSNumber, 1.0 as NSNumber, 0.0 as NSNumber] + animation.keyTimes = [0.0 as NSNumber, 0.4546 as NSNumber, 0.9091 as NSNumber, 1 as NSNumber] + animation.duration = 0.5 + animation.autoreverses = true + animation.repeatCount = Float.infinity + + audioRecordingDotView?.layer.add(animation, forKey: "recording") } - - if resumingRecording { - animateDot() - } else { - audioRecordingTimeNode.started = { - animateDot() - } - } - } - self.attachmentButton.layer.animateAlpha(from: CGFloat(self.attachmentButton.layer.presentation()?.opacity ?? 1), to: 0, duration: 0.15, delay: 0, removeOnCompletion: false) - self.attachmentButton.layer.animateScale(from: 1, to: 0.3, duration: 0.15, delay: 0, removeOnCompletion: false) + }) } if hideInfo { - audioRecordingDotNode.layer.removeAllAnimations() - audioRecordingDotNode.layer.animateAlpha(from: CGFloat(audioRecordingDotNode.layer.presentation()?.opacity ?? 1), to: 0, duration: 0.15, delay: 0, removeOnCompletion: false) + audioRecordingDotView.layer.removeAllAnimations() + audioRecordingDotView.layer.animateAlpha(from: CGFloat(audioRecordingDotView.layer.presentation()?.opacity ?? 1), to: 0, duration: 0.15, delay: 0, removeOnCompletion: false) audioRecordingTimeNode.layer.animateAlpha(from: CGFloat(audioRecordingTimeNode.layer.presentation()?.opacity ?? 1), to: 0, duration: 0.15, delay: 0, removeOnCompletion: false) audioRecordingCancelIndicator.layer.animateAlpha(from: CGFloat(audioRecordingCancelIndicator.layer.presentation()?.opacity ?? 1), to: 0, duration: 0.15, delay: 0, removeOnCompletion: false) } - } else { + } else if self.audioRecordingInfoContainerNode != nil { self.finishedTransitionToPreview = nil - var update = self.actionButtons.micButton.audioRecorder != nil || self.actionButtons.micButton.videoRecordingStatus != nil self.actionButtons.micButton.audioRecorder = nil self.actionButtons.micButton.videoRecordingStatus = nil transition.updateAlpha(layer: self.textInputBackgroundNode.layer, alpha: 1.0) if let textInputNode = self.textInputNode { - transition.updateAlpha(node: textInputNode, alpha: 1.0) + transition.updateAlpha(node: textInputNode, alpha: audioRecordingItemsAlpha) } for (_, button) in self.accessoryItemButtons { - transition.updateAlpha(layer: button.layer, alpha: 1.0) + transition.updateAlpha(layer: button.layer, alpha: audioRecordingItemsAlpha) } if let audioRecordingInfoContainerNode = self.audioRecordingInfoContainerNode { @@ -2490,63 +2156,36 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } } - if let audioRecordingDotNode = self.audioRecordingDotNode { - let dismissDotNode = { [weak audioRecordingDotNode, weak self] in - guard let audioRecordingDotNode = audioRecordingDotNode, audioRecordingDotNode === self?.audioRecordingDotNode else { return } - - self?.audioRecordingDotNode = nil - - audioRecordingDotNode.layer.animateScale(from: 1.0, to: 0.3, duration: 0.15, delay: 0.0, removeOnCompletion: false) - audioRecordingDotNode.layer.animateAlpha(from: CGFloat(audioRecordingDotNode.layer.presentation()?.opacity ?? 1), to: 0.0, duration: 0.15, delay: 0.0, removeOnCompletion: false) { [weak audioRecordingDotNode] _ in - audioRecordingDotNode?.removeFromSupernode() - } - - self?.attachmentButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: 0.0, removeOnCompletion: false) - self?.attachmentButton.layer.animateScale(from: 0.3, to: 1.0, duration: 0.15, delay: 0.0, removeOnCompletion: false) - } + if let audioRecordingDotView = self.audioRecordingDotView { + self.audioRecordingDotView = nil - if update && !self.audioRecordingDotNodeDismissed { - audioRecordingDotNode.layer.removeAllAnimations() - } + var dotFrame = audioRecordingDotView.bounds.size.centered(around: audioRecordingDotView.center) + dotFrame.origin.x = hideOffset.x + leftInset + textFieldInsets.left + 16.0 + transition.updatePosition(layer: audioRecordingDotView.layer, position: dotFrame.center) - if self.isMediaDeleted { - if self.prevInputPanelNode is ChatRecordingPreviewInputPanelNode { - self.audioRecordingDotNode?.removeFromSupernode() - self.audioRecordingDotNode = nil - } else { - if !self.audioRecordingDotNodeDismissed { - audioRecordingDotNode.layer.removeAllAnimations() - } - audioRecordingDotNode.completion = dismissDotNode - audioRecordingDotNode.play() - update = true - } - } else { - dismissDotNode() - } - - if update && !self.audioRecordingDotNodeDismissed { - self.audioRecordingDotNode?.layer.animatePosition(from: CGPoint(), to: CGPoint(x: leftMenuInset, y: 0.0), duration: 0.15, removeOnCompletion: false, additive: true) - self.audioRecordingDotNodeDismissed = true + audioRecordingDotView.layer.animateScale(from: 1.0, to: 0.3, duration: 0.15, delay: 0.0, removeOnCompletion: false) + audioRecordingDotView.layer.animateAlpha(from: CGFloat(audioRecordingDotView.layer.presentation()?.opacity ?? 1), to: 0.0, duration: 0.15, delay: 0.0, removeOnCompletion: false) { [weak audioRecordingDotView] _ in + audioRecordingDotView?.removeFromSuperview() } } if let audioRecordingTimeNode = self.audioRecordingTimeNode { self.audioRecordingTimeNode = nil - let timePosition = audioRecordingTimeNode.position - transition.updatePosition(node: audioRecordingTimeNode, position: CGPoint(x: timePosition.x - audioRecordingTimeNode.bounds.width / 2.0, y: timePosition.y)) - transition.updateTransformScale(node: audioRecordingTimeNode, scale: 0.1) + var audioRecordingTimeFrame = audioRecordingTimeNode.bounds.size.centered(around: audioRecordingTimeNode.position) + audioRecordingTimeFrame.origin.x = hideOffset.x + leftInset + textFieldInsets.left + 34.0 + transition.updatePosition(layer: audioRecordingTimeNode.layer, position: audioRecordingTimeFrame.center) + transition.updateTransformScale(node: audioRecordingTimeNode, scale: CGPoint(x: 0.5, y: 0.5)) } if let audioRecordingCancelIndicator = self.audioRecordingCancelIndicator { self.audioRecordingCancelIndicator = nil if transition.isAnimated { - audioRecordingCancelIndicator.layer.animateAlpha(from: audioRecordingCancelIndicator.alpha, to: 0.0, duration: 0.25, completion: { [weak audioRecordingCancelIndicator] _ in - audioRecordingCancelIndicator?.removeFromSupernode() + audioRecordingCancelIndicator.layer.animateAlpha(from: audioRecordingCancelIndicator.alpha, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak audioRecordingCancelIndicator] _ in + audioRecordingCancelIndicator?.removeFromSuperview() }) } else { - audioRecordingCancelIndicator.removeFromSupernode() + audioRecordingCancelIndicator.removeFromSuperview() } } } @@ -2572,42 +2211,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch textInputViewRealInsets = calculateTextFieldRealInsets(presentationInterfaceState: presentationInterfaceState, accessoryButtonsWidth: accessoryButtonsWidth) } - var actionButtonsSize = CGSize(width: 40.0, height: 40.0) - if let presentationInterfaceState = self.presentationInterfaceState { - var showTitle = false - if !self.actionButtons.sendContainerNode.alpha.isZero { - if let _ = presentationInterfaceState.sendPaidMessageStars { - showTitle = true - } else if case let .customChatContents(customChatContents) = interfaceState.subject { - switch customChatContents.kind { - default: - break - } - } - } - actionButtonsSize = self.actionButtons.updateLayout(size: CGSize(width: 40.0, height: minimalHeight), isMediaInputExpanded: isMediaInputExpanded, showTitle: showTitle, currentMessageEffectId: presentationInterfaceState.interfaceState.sendMessageEffect, transition: transition, interfaceState: presentationInterfaceState) - } - - var textFieldInsets = self.textFieldInsets(metrics: metrics) - if actionButtonsSize.width > 40.0 { - textFieldInsets.right = actionButtonsSize.width - 2.0 - } - if additionalSideInsets.right > 0.0 { - textFieldInsets.right += additionalSideInsets.right / 3.0 - } - var contentHeight: CGFloat = 0.0 - var accessoryPanel: AnyComponentWithIdentity? - if let context = self.context { - accessoryPanel = textInputAccessoryPanel( - context: context, - chatPresentationInterfaceState: interfaceState, - chatControllerInteraction: self.chatControllerInteraction, - interfaceInteraction: self.interfaceInteraction - ) - } - let alphaTransitionIn: ContainedViewLayoutTransition = transition.isAnimated ? ContainedViewLayoutTransition.animated(duration: 0.15, curve: .easeInOut) : .immediate let alphaTransitionOut: ContainedViewLayoutTransition = transition.isAnimated ? ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) : .immediate @@ -2682,19 +2287,57 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch contentHeight += accessoryPanelSize.height } + if let _ = interfaceState.interfaceState.mediaDraftState { + let mediaPreviewPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: textInputWidth, height: 40.0)) + var mediaPreviewPanelTransition = transition + + let mediaPreviewPanelNode: ChatRecordingPreviewInputPanelNodeImpl + if let current = self.mediaPreviewPanelNode { + mediaPreviewPanelNode = current + } else { + mediaPreviewPanelTransition = .immediate + mediaPreviewPanelNode = ChatRecordingPreviewInputPanelNodeImpl(theme: interfaceState.theme) + mediaPreviewPanelNode.context = self.context + mediaPreviewPanelNode.chatControllerInteraction = self.chatControllerInteraction + mediaPreviewPanelNode.interfaceInteraction = self.interfaceInteraction + self.mediaPreviewPanelNode = mediaPreviewPanelNode + mediaPreviewPanelNode.alpha = 0.0 + mediaPreviewPanelNode.frame = mediaPreviewPanelFrame + + self.textInputContainer.addSubnode(mediaPreviewPanelNode) + mediaPreviewPanelNode.tintMaskView.alpha = 0.0 + mediaPreviewPanelNode.tintMaskView.frame = mediaPreviewPanelFrame + self.textInputContainerBackgroundView.maskContentView.addSubview(mediaPreviewPanelNode.tintMaskView) + } + transition.updateAlpha(node: mediaPreviewPanelNode, alpha: 1.0) + transition.updateFrame(node: mediaPreviewPanelNode, frame: mediaPreviewPanelFrame) + transition.updateAlpha(layer: mediaPreviewPanelNode.tintMaskView.layer, alpha: 1.0) + transition.updateFrame(view: mediaPreviewPanelNode.tintMaskView, frame: mediaPreviewPanelFrame) + + let _ = mediaPreviewPanelNode.updateLayout(width: mediaPreviewPanelFrame.width, leftInset: 0.0, rightInset: 0.0, bottomInset: 0.0, additionalSideInsets: UIEdgeInsets(), maxHeight: 40.0, isSecondary: false, transition: mediaPreviewPanelTransition, interfaceState: interfaceState, metrics: metrics, isMediaInputExpanded: false) + } else if let mediaPreviewPanelNode = self.mediaPreviewPanelNode { + self.mediaPreviewPanelNode = nil + transition.updateAlpha(node: mediaPreviewPanelNode, alpha: 0.0, completion: { [weak mediaPreviewPanelNode] _ in + mediaPreviewPanelNode?.view.removeFromSuperview() + }) + let mediaPreviewPanelNodeTintMaskView = mediaPreviewPanelNode.tintMaskView + transition.updateAlpha(layer: mediaPreviewPanelNodeTintMaskView.layer, alpha: 0.0, completion: { [weak mediaPreviewPanelNodeTintMaskView] _ in + mediaPreviewPanelNodeTintMaskView?.removeFromSuperview() + }) + } + let textFieldTopContentOffset = contentHeight contentHeight += textInputHeight contentHeight += textFieldInsets.bottom - let textInputFrame = CGRect(x: hideOffset.x + leftInset + textFieldInsets.left, y: hideOffset.y + textFieldInsets.top, width: textInputWidth, height: contentHeight) + let textInputContainerBackgroundFrame = CGRect(x: hideOffset.x + leftInset + textFieldInsets.left, y: hideOffset.y + textFieldInsets.top, width: textInputWidth, height: contentHeight) + let textInputFrame = textInputContainerBackgroundFrame - transition.updateFrame(node: self.textInputContainer, frame: textInputFrame) - transition.updateFrame(view: self.textInputContainerBackgroundView, frame: CGRect(origin: CGPoint(), size: textInputFrame.size)) + transition.updateFrame(node: self.textInputContainer, frame: textInputContainerBackgroundFrame) + transition.updateFrame(view: self.textInputContainerBackgroundView, frame: CGRect(origin: CGPoint(), size: textInputContainerBackgroundFrame.size)) - self.textInputContainerBackgroundView.update(size: textInputFrame.size, cornerRadius: floor(minimalInputHeight * 0.5), isDark: interfaceState.theme.overallDarkAppearance, tintColor: interfaceState.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.65), transition: ComponentTransition(transition)) - - transition.updateAlpha(node: self.textInputContainer, alpha: audioRecordingItemsAlpha) + self.textInputContainerBackgroundView.update(size: textInputContainerBackgroundFrame.size, cornerRadius: floor(minimalInputHeight * 0.5), isDark: interfaceState.theme.overallDarkAppearance, tintColor: interfaceState.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.65), transition: ComponentTransition(transition)) if let removedAccessoryPanelView { if let removedAccessoryPanelView = removedAccessoryPanelView as? ChatInputAccessoryPanelView { @@ -2743,7 +2386,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch contextPlaceholderNode.displaysAsynchronously = false contextPlaceholderNode.isUserInteractionEnabled = false self.contextPlaceholderNode = contextPlaceholderNode - self.clippingNode.insertSubnode(contextPlaceholderNode, aboveSubnode: self.textPlaceholderNode) + self.textInputContainerBackgroundView.contentView.insertSubview(contextPlaceholderNode.view, aboveSubview: self.textPlaceholderNode.view) self.textInputContainerBackgroundView.contentView.insertSubview(contextPlaceholderNode.view, aboveSubview: self.textPlaceholderNode.view) } @@ -2794,7 +2437,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch self.slowmodePlaceholderNode?.isHidden = true } - var nextButtonTopRight = CGPoint(x: textInputFrame.width - accessoryButtonInset - rightSlowModeInset, y: textInputFrame.height - minimalInputHeight) + var nextButtonTopRight = CGPoint(x: textInputContainerBackgroundFrame.width - accessoryButtonInset - rightSlowModeInset, y: textInputContainerBackgroundFrame.height - minimalInputHeight) for (item, button) in self.accessoryItemButtons.reversed() { let buttonSize = CGSize(width: button.buttonWidth, height: minimalInputHeight) button.updateLayout(item: item, size: buttonSize) @@ -2804,12 +2447,13 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch button.frame = buttonFrame.offsetBy(dx: -additionalOffset, dy: 0.0) transition.updateFrame(layer: button.layer, frame: buttonFrame) if animatedTransition { - button.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + button.layer.animateAlpha(from: 0.0, to: audioRecordingItemsAlpha, duration: 0.25) button.layer.animateScale(from: 0.2, to: 1.0, duration: 0.25) } } else { transition.updateFrame(layer: button.layer, frame: buttonFrame) } + transition.updateAlpha(layer: button.layer, alpha: audioRecordingItemsAlpha) nextButtonTopRight.x -= buttonSize.width nextButtonTopRight.x -= accessoryButtonSpacing } @@ -2917,7 +2561,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } } - let actionButtonsFrame = CGRect(origin: CGPoint(x: hideOffset.x + width - rightInset - 8.0 - actionButtonsSize.width + composeButtonsOffset, y: textInputFrame.maxY - actionButtonsSize.height), size: actionButtonsSize) + let actionButtonsFrame = CGRect(origin: CGPoint(x: hideOffset.x + width - rightInset - 8.0 - actionButtonsSize.width + composeButtonsOffset, y: textInputContainerBackgroundFrame.maxY - actionButtonsSize.height), size: actionButtonsSize) transition.updateFrame(node: self.actionButtons, frame: actionButtonsFrame) if let (rect, containerSize) = self.absoluteRect { self.actionButtons.updateAbsoluteRect(CGRect(x: rect.origin.x + actionButtonsFrame.origin.x, y: rect.origin.y + actionButtonsFrame.origin.y, width: actionButtonsFrame.width, height: actionButtonsFrame.height), within: containerSize, transition: transition) @@ -3061,128 +2705,6 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch viewOnceIsVisible = isLocked } } - - if let prevInputPanelNode = self.prevInputPanelNode { - prevInputPanelNode.frame = CGRect(origin: .zero, size: prevInputPanelNode.frame.size) - } - if let prevPreviewInputPanelNode = self.prevInputPanelNode as? ChatRecordingPreviewInputPanelNode { - self.prevInputPanelNode = nil - - if !prevPreviewInputPanelNode.viewOnceButton.isHidden { - self.viewOnce = prevPreviewInputPanelNode.viewOnce - self.viewOnceButton.update(isSelected: prevPreviewInputPanelNode.viewOnce, animated: false) - self.viewOnceButton.layer.animatePosition(from: prevPreviewInputPanelNode.viewOnceButton.position, to: self.viewOnceButton.position, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in - }) - } - - let animateOutPreviewButton: (ASDisplayNode) -> Void = { button in - if button.alpha > 0.0 { - if let snapshotView = button.view.snapshotContentTree() { - snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in - snapshotView.removeFromSuperview() - }) - snapshotView.layer.animateScale(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) - self.viewForOverlayContent?.addSubview(snapshotView) - } - } - } - - animateOutPreviewButton(prevPreviewInputPanelNode.viewOnceButton) - animateOutPreviewButton(prevPreviewInputPanelNode.recordMoreButton) - - prevPreviewInputPanelNode.gestureRecognizer?.isEnabled = false - prevPreviewInputPanelNode.isUserInteractionEnabled = false - - if self.isMediaDeleted { - func animatePosition(for previewLayer: CALayer) { - previewLayer.animatePosition( - from: previewLayer.position, - to: CGPoint(x: leftMenuInset.isZero ? previewLayer.position.x - 20 : leftMenuInset + previewLayer.frame.width / 2.0, y: previewLayer.position.y), - duration: 0.15 - ) - } - - animatePosition(for: prevPreviewInputPanelNode.waveformBackgroundNode.layer) - animatePosition(for: prevPreviewInputPanelNode.waveformScrubberNode.layer) - animatePosition(for: prevPreviewInputPanelNode.playButtonNode.layer) - animatePosition(for: prevPreviewInputPanelNode.trimView.layer) - if let view = prevPreviewInputPanelNode.scrubber.view { - animatePosition(for: view.layer) - } - } - - func animateAlpha(for previewLayer: CALayer) { - previewLayer.animateAlpha( - from: 1.0, - to: 0.0, - duration: 0.15, - removeOnCompletion: false - ) - } - animateAlpha(for: prevPreviewInputPanelNode.waveformBackgroundNode.layer) - animateAlpha(for: prevPreviewInputPanelNode.waveformScrubberNode.layer) - animateAlpha(for: prevPreviewInputPanelNode.playButtonNode.layer) - animateAlpha(for: prevPreviewInputPanelNode.trimView.layer) - if let view = prevPreviewInputPanelNode.scrubber.view { - animateAlpha(for: view.layer) - } - - let binNode = prevPreviewInputPanelNode.binNode - self.animatingBinNode = binNode - let dismissBin = { [weak self, weak prevPreviewInputPanelNode, weak binNode] in - if binNode?.supernode != nil { - prevPreviewInputPanelNode?.deleteButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, delay: 0.0, removeOnCompletion: false) { [weak prevPreviewInputPanelNode] _ in - if prevPreviewInputPanelNode?.supernode === self { - prevPreviewInputPanelNode?.removeFromSupernode() - } - } - prevPreviewInputPanelNode?.deleteButton.layer.animateScale(from: 1.0, to: 0.3, duration: 0.15, delay: 0.0, removeOnCompletion: false) - - if isRecording { - self?.attachmentButton.layer.animateAlpha(from: 0.0, to: 0, duration: 0.01, delay: 0.0, removeOnCompletion: false) - self?.attachmentButton.layer.animateScale(from: 1, to: 0.3, duration: 0.01, delay: 0.0, removeOnCompletion: false) - } else { - self?.attachmentButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: 0.0, removeOnCompletion: false) - self?.attachmentButton.layer.animateScale(from: 0.3, to: 1.0, duration: 0.15, delay: 0.0, removeOnCompletion: false) - } - } else if prevPreviewInputPanelNode?.supernode === self { - prevPreviewInputPanelNode?.removeFromSupernode() - } - } - - if self.isMediaDeleted { - Queue.mainQueue().after(0.5, { - self.isMediaDeleted = false - }) - } - - if self.isMediaDeleted && !isRecording { - self.attachmentButton.layer.animateAlpha(from: 0.0, to: 0, duration: 0.01, delay: 0.0, removeOnCompletion: false) - binNode.completion = dismissBin - binNode.play() - } else { - dismissBin() - } - - prevPreviewInputPanelNode.deleteButton.layer.animatePosition(from: CGPoint(), to: CGPoint(x: leftMenuInset, y: 0.0), duration: 0.15, removeOnCompletion: false, additive: true) - - prevPreviewInputPanelNode.sendButton.layer.animateScale(from: 1.0, to: 0.3, duration: 0.15, removeOnCompletion: false) - prevPreviewInputPanelNode.sendButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) - - self.actionButtons.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: 0, removeOnCompletion: false) - self.actionButtons.layer.animateScale(from: 0.3, to: 1.0, duration: 0.15, delay: 0, removeOnCompletion: false) - - if hasMenuButton { - if isSendAsButton { - - } else { - self.menuButton.alpha = 1.0 - self.menuButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: 0, removeOnCompletion: false) - self.menuButton.transform = CATransform3DIdentity - self.menuButton.layer.animateScale(from: 0.3, to: 1.0, duration: 0.15, delay: 0, removeOnCompletion: false) - } - } - } var clippingDelta: CGFloat = 0.0 if case let .media(_, _, focused) = interfaceState.inputMode, focused { @@ -3192,7 +2714,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch transition.updateSublayerTransformOffset(layer: self.clippingNode.layer, offset: CGPoint(x: 0.0, y: clippingDelta)) let viewOnceSize = self.viewOnceButton.update(theme: interfaceState.theme) - let viewOnceButtonFrame = CGRect(origin: CGPoint(x: width - rightInset - 40.0 - UIScreenPixel, y: -152.0), size: viewOnceSize) + let viewOnceButtonFrame = CGRect(origin: CGPoint(x: width - rightInset - 50.0 - UIScreenPixel, y: -152.0), size: viewOnceSize) self.viewOnceButton.bounds = CGRect(origin: .zero, size: viewOnceButtonFrame.size) transition.updatePosition(node: self.viewOnceButton, position: viewOnceButtonFrame.center) @@ -3261,11 +2783,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch parentController.present(tooltipController, in: .current) } - override 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 @@ -3285,7 +2803,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } } - @objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { + @objc public func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { self.chatInputTextNodeDidUpdateText() } @@ -3869,9 +3387,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? @@ -3892,32 +3407,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) + self.interfaceInteraction?.presentController(undoController, nil) } let copyEmoji: (TelegramMediaFile) -> Void = { file in var text = "." @@ -4069,6 +3574,10 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch keepSendButtonEnabled = true } } + + if presentationInterfaceState.interfaceState.mediaDraftState != nil { + keepSendButtonEnabled = true + } } var animateWithBounce = false @@ -4279,14 +3788,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() } @@ -4319,7 +3828,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) }) @@ -4339,11 +3848,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 } @@ -4368,12 +3877,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 } @@ -4406,14 +3915,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) @@ -4494,7 +4003,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: []) } @@ -4582,12 +4091,12 @@ 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) } private var currentSpeechHolder: SpeechSynthesizerHolder? - @objc func _accessibilitySpeak(_ sender: Any) { + @objc public func _accessibilitySpeak(_ sender: Any) { var text = "" self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in text = current.inputText.attributedSubstring(from: NSMakeRange(current.selectionRange.lowerBound, current.selectionRange.count)).string @@ -4611,53 +4120,53 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } } - @objc func _showTextStyleOptions(_ sender: Any) { + @objc public func _showTextStyleOptions(_ sender: Any) { if let textInputNode = self.textInputNode { self.inputMenu.format(view: textInputNode.view, rect: textInputNode.selectionRect.offsetBy(dx: 0.0, dy: -textInputNode.textView.contentOffset.y).insetBy(dx: 0.0, dy: -1.0)) } } - @objc func formatAttributesBold(_ sender: Any) { + @objc public func formatAttributesBold(_ sender: Any) { self.inputMenu.back() self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.bold, value: nil), inputMode) } } - @objc func formatAttributesItalic(_ sender: Any) { + @objc public func formatAttributesItalic(_ sender: Any) { self.inputMenu.back() self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.italic, value: nil), inputMode) } } - @objc func formatAttributesMonospace(_ sender: Any) { + @objc public func formatAttributesMonospace(_ sender: Any) { self.inputMenu.back() self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.monospace, value: nil), inputMode) } } - @objc func formatAttributesLink(_ sender: Any) { + @objc public func formatAttributesLink(_ sender: Any) { self.inputMenu.back() self.interfaceInteraction?.openLinkEditing() } - @objc func formatAttributesStrikethrough(_ sender: Any) { + @objc public func formatAttributesStrikethrough(_ sender: Any) { self.inputMenu.back() self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.strikethrough, value: nil), inputMode) } } - @objc func formatAttributesUnderline(_ sender: Any) { + @objc public func formatAttributesUnderline(_ sender: Any) { self.inputMenu.back() self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.underline, value: nil), inputMode) } } - @objc func formatAttributesQuote(_ sender: Any) { + @objc public func formatAttributesQuote(_ sender: Any) { self.inputMenu.back() self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in @@ -4665,7 +4174,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } } - @objc func formatAttributesCodeBlock(_ sender: Any) { + @objc public func formatAttributesCodeBlock(_ sender: Any) { self.inputMenu.back() self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in @@ -4673,7 +4182,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } } - @objc func formatAttributesSpoiler(_ sender: Any) { + @objc public func formatAttributesSpoiler(_ sender: Any) { self.inputMenu.back() var animated = false @@ -4692,7 +4201,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 } @@ -4732,11 +4241,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) @@ -4744,7 +4253,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch return false } - @objc func editableTextNodeShouldCopy(_ editableTextNode: ASEditableTextNode) -> Bool { + @objc public func editableTextNodeShouldCopy(_ editableTextNode: ASEditableTextNode) -> Bool { return self.chatInputTextNodeShouldCopy() } @@ -4760,7 +4269,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } } - func chatInputTextNodeShouldPaste() -> Bool { + public func chatInputTextNodeShouldPaste() -> Bool { let pasteboard = UIPasteboard.general var attributedString: NSAttributedString? @@ -4840,7 +4349,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch return true } - @objc func editableTextNodeShouldPaste(_ editableTextNode: ASEditableTextNode) -> Bool { + @objc public func editableTextNodeShouldPaste(_ editableTextNode: ASEditableTextNode) -> Bool { return self.chatInputTextNodeShouldPaste() } @@ -4855,8 +4364,13 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch return } } - - self.sendMessage() + + if let presentationInterfaceState = self.presentationInterfaceState, presentationInterfaceState.interfaceState.mediaDraftState != nil { + self.interfaceInteraction?.sendRecordedMedia(false, self.viewOnce) + self.viewOnce = false + } else { + self.sendMessage() + } } @objc func sendAsAvatarButtonPressed() { @@ -4890,7 +4404,12 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } @objc func attachmentButtonPressed() { - self.displayAttachmentMenu() + if let presentationInterfaceState = self.presentationInterfaceState, presentationInterfaceState.interfaceState.mediaDraftState != nil { + self.viewOnce = false + self.interfaceInteraction?.deleteRecordedMedia() + } else { + self.displayAttachmentMenu() + } } @objc func searchLayoutClearButtonPressed() { @@ -4919,21 +4438,21 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } } - @objc func textInputBackgroundViewTap(_ recognizer: UITapGestureRecognizer) { + @objc public func textInputBackgroundViewTap(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { self.ensureFocused() } } - 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 } @@ -4948,7 +4467,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } private var switching = false - func ensureFocusedOnTap() { + public func ensureFocusedOnTap() { if self.textInputNode == nil { self.loadTextInputNode() } @@ -4962,14 +4481,14 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } } - func backwardsDeleteText() { + public func backwardsDeleteText() { guard let textInputNode = self.textInputNode else { return } textInputNode.textView.deleteBackward() } - @objc func expandButtonPressed() { + @objc public func expandButtonPressed() { self.toggleExpandMediaInput?() /*self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in if case let .media(mode, expanded, focused) = state.inputMode { @@ -5025,7 +4544,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 @@ -5050,7 +4569,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 @@ -5059,21 +4578,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) @@ -5084,7 +4603,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) @@ -5093,7 +4612,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) @@ -5102,7 +4621,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) @@ -5111,7 +4630,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 textInputNode = self.textInputNode else { return nil } @@ -5132,7 +4651,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), @@ -5140,200 +4659,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch ) } - 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) }) - } -} - -private enum MenuIconNodeState: Equatable { - case menu - case app - case close -} - -private final class MenuIconNode: ManagedAnimationNode { - private let duration: Double = 0.33 - fileprivate var iconState: MenuIconNodeState = .menu - - init() { - super.init(size: CGSize(width: 30.0, height: 30.0)) - - self.trackTo(item: ManagedAnimationItem(source: .local("anim_menuclose"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01)) - } - - func enqueueState(_ state: MenuIconNodeState, animated: Bool) { - guard self.iconState != state else { - return - } - - let previousState = self.iconState - self.iconState = state - - switch previousState { - case .close: - switch state { - case .menu: - if animated { - self.trackTo(item: ManagedAnimationItem(source: .local("anim_closemenu"), frames: .range(startFrame: 0, endFrame: 20), duration: self.duration)) - } else { - self.trackTo(item: ManagedAnimationItem(source: .local("anim_menuclose"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01)) - } - case .app: - if animated { - self.trackTo(item: ManagedAnimationItem(source: .local("anim_webview"), frames: .range(startFrame: 0, endFrame: 22), duration: self.duration)) - } else { - self.trackTo(item: ManagedAnimationItem(source: .local("anim_webview"), frames: .range(startFrame: 22, endFrame: 22), duration: 0.01)) - } - case .close: - break - } - case .menu: - switch state { - case .close: - if animated { - self.trackTo(item: ManagedAnimationItem(source: .local("anim_menuclose"), frames: .range(startFrame: 0, endFrame: 20), duration: self.duration)) - } else { - self.trackTo(item: ManagedAnimationItem(source: .local("anim_menuclose"), frames: .range(startFrame: 20, endFrame: 20), duration: 0.01)) - } - case .app: - self.trackTo(item: ManagedAnimationItem(source: .local("anim_webview"), frames: .range(startFrame: 22, endFrame: 22), duration: 0.01)) - case .menu: - break - } - case .app: - switch state { - case .close: - if animated { - self.trackTo(item: ManagedAnimationItem(source: .local("anim_webview"), frames: .range(startFrame: 22, endFrame: 0), duration: self.duration)) - } else { - self.trackTo(item: ManagedAnimationItem(source: .local("anim_webview"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01)) - } - case .menu: - self.trackTo(item: ManagedAnimationItem(source: .local("anim_menuclose"), frames: .range(startFrame: 0, endFrame: 20), duration: 0.01)) - case .app: - break - } - } - } -} - -private func generateClearImage(color: UIColor) -> UIImage? { - return generateImage(CGSize(width: 17.0, height: 17.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(color.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) - context.setBlendMode(.copy) - context.setStrokeColor(UIColor.clear.cgColor) - context.setLineCap(.round) - context.setLineWidth(1.66) - context.move(to: CGPoint(x: 6.0, y: 6.0)) - context.addLine(to: CGPoint(x: 11.0, y: 11.0)) - context.strokePath() - context.move(to: CGPoint(x: size.width - 6.0, y: 6.0)) - context.addLine(to: CGPoint(x: size.width - 11.0, y: 11.0)) - context.strokePath() - }) -} - - -private final class BoostSlowModeButton: HighlightTrackingButtonNode { - let containerNode: ASDisplayNode - let backgroundNode: ASImageNode - let textNode: ImmediateAnimatedCountLabelNode - let iconNode: ASImageNode - - private var updateTimer: SwiftSignalKit.Timer? - - var requestUpdate: () -> Void = {} - - override init(pointerStyle: PointerStyle? = nil) { - self.containerNode = ASDisplayNode() - - self.backgroundNode = ASImageNode() - self.backgroundNode.displaysAsynchronously = false - self.backgroundNode.clipsToBounds = true - self.backgroundNode.image = generateGradientImage(size: CGSize(width: 100.0, height: 2.0), scale: 1.0, colors: [UIColor(rgb: 0x9076ff), UIColor(rgb: 0xbc6de8)], locations: [0.0, 1.0], direction: .horizontal) - - self.iconNode = ASImageNode() - self.iconNode.displaysAsynchronously = false - self.iconNode.image = generateClearImage(color: .white) - - self.textNode = ImmediateAnimatedCountLabelNode() - self.textNode.alwaysOneDirection = true - self.textNode.isUserInteractionEnabled = false - - super.init(pointerStyle: pointerStyle) - - self.addSubnode(self.containerNode) - self.containerNode.addSubnode(self.backgroundNode) - self.containerNode.addSubnode(self.iconNode) - self.containerNode.addSubnode(self.textNode) - - self.highligthedChanged = { [weak self] highlighted in - if let self { - if highlighted { - self.containerNode.layer.animateScale(from: 1.0, to: 0.75, duration: 0.4, removeOnCompletion: false) - } else if let presentationLayer = self.containerNode.layer.presentation() { - self.containerNode.layer.animateScale(from: CGFloat((presentationLayer.value(forKeyPath: "transform.scale.y") as? NSNumber)?.floatValue ?? 1.0), to: 1.0, duration: 0.25, removeOnCompletion: false) - } - } - } - } - - func update(size: CGSize, interfaceState: ChatPresentationInterfaceState) -> CGSize { - var text = "" - if let slowmodeState = interfaceState.slowmodeState { - let relativeTimestamp: CGFloat - switch slowmodeState.variant { - case let .timestamp(validUntilTimestamp): - let timestamp = CGFloat(Date().timeIntervalSince1970) - relativeTimestamp = CGFloat(validUntilTimestamp) - timestamp - case .pendingMessages: - relativeTimestamp = CGFloat(slowmodeState.timeout) - } - - self.updateTimer?.invalidate() - - if relativeTimestamp >= 0.0 { - text = stringForDuration(Int32(relativeTimestamp)) - - self.updateTimer = SwiftSignalKit.Timer(timeout: 1.0 / 60.0, repeat: false, completion: { [weak self] in - self?.requestUpdate() - }, queue: .mainQueue()) - self.updateTimer?.start() - } - } else { - self.updateTimer?.invalidate() - self.updateTimer = nil - } - - let font = Font.with(size: 15.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]) - let textColor = UIColor.white - - var segments: [AnimatedCountLabelNode.Segment] = [] - var textCount = 0 - - for char in text { - if let intValue = Int(String(char)) { - segments.append(.number(intValue, NSAttributedString(string: String(char), font: font, textColor: textColor))) - } else { - segments.append(.text(textCount, NSAttributedString(string: String(char), font: font, textColor: textColor))) - textCount += 1 - } - } - self.textNode.segments = segments - - let textSize = self.textNode.updateLayout(size: CGSize(width: 200.0, height: 100.0), animated: true) - let totalSize = CGSize(width: textSize.width > 0.0 ? textSize.width + 38.0 : 33.0, height: 33.0) - - self.containerNode.bounds = CGRect(origin: .zero, size: totalSize) - self.containerNode.position = CGPoint(x: totalSize.width / 2.0, y: totalSize.height / 2.0) - self.backgroundNode.frame = CGRect(origin: .zero, size: totalSize) - self.backgroundNode.cornerRadius = totalSize.height / 2.0 - self.textNode.frame = CGRect(origin: CGPoint(x: 9.0, y: floorToScreenPixels((totalSize.height - textSize.height) / 2.0)), size: textSize) - if let icon = self.iconNode.image { - self.iconNode.frame = CGRect(origin: CGPoint(x: totalSize.width - icon.size.width - 7.0, y: floorToScreenPixels((totalSize.height - icon.size.height) / 2.0)), size: icon.size) - } - return totalSize + public func makeAttachmentMenuTransition(accessoryPanelNode: ASDisplayNode?) -> AttachmentController.InputPanelTransition { + return AttachmentController.InputPanelTransition(inputNode: self, accessoryPanelNode: accessoryPanelNode, menuButtonNode: self.menuButton, menuButtonBackgroundView: self.menuButtonBackgroundView, menuIconNode: self.menuButtonIconNode, menuTextNode: self.menuButtonTextNode, prepareForDismiss: { self.menuButtonIconNode.enqueueState(.app, animated: false) }) } } diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/MenuIconNode.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/MenuIconNode.swift new file mode 100644 index 0000000000..83866a5da2 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/MenuIconNode.swift @@ -0,0 +1,78 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import TelegramPresentationData +import ManagedAnimationNode + +enum MenuIconNodeState: Equatable { + case menu + case app + case close +} + +final class MenuIconNode: ManagedAnimationNode { + private let duration: Double = 0.33 + var iconState: MenuIconNodeState = .menu + + init() { + super.init(size: CGSize(width: 30.0, height: 30.0)) + + self.trackTo(item: ManagedAnimationItem(source: .local("anim_menuclose"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01)) + } + + func enqueueState(_ state: MenuIconNodeState, animated: Bool) { + guard self.iconState != state else { + return + } + + let previousState = self.iconState + self.iconState = state + + switch previousState { + case .close: + switch state { + case .menu: + if animated { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_closemenu"), frames: .range(startFrame: 0, endFrame: 20), duration: self.duration)) + } else { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_menuclose"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01)) + } + case .app: + if animated { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_webview"), frames: .range(startFrame: 0, endFrame: 22), duration: self.duration)) + } else { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_webview"), frames: .range(startFrame: 22, endFrame: 22), duration: 0.01)) + } + case .close: + break + } + case .menu: + switch state { + case .close: + if animated { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_menuclose"), frames: .range(startFrame: 0, endFrame: 20), duration: self.duration)) + } else { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_menuclose"), frames: .range(startFrame: 20, endFrame: 20), duration: 0.01)) + } + case .app: + self.trackTo(item: ManagedAnimationItem(source: .local("anim_webview"), frames: .range(startFrame: 22, endFrame: 22), duration: 0.01)) + case .menu: + break + } + case .app: + switch state { + case .close: + if animated { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_webview"), frames: .range(startFrame: 22, endFrame: 0), duration: self.duration)) + } else { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_webview"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01)) + } + case .menu: + self.trackTo(item: ManagedAnimationItem(source: .local("anim_menuclose"), frames: .range(startFrame: 0, endFrame: 20), duration: 0.01)) + case .app: + break + } + } + } +} 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/ChatTextInputMediaRecordingButton/BUILD b/submodules/TelegramUI/Components/ChatTextInputMediaRecordingButton/BUILD index 09ef65c475..20b691619b 100644 --- a/submodules/TelegramUI/Components/ChatTextInputMediaRecordingButton/BUILD +++ b/submodules/TelegramUI/Components/ChatTextInputMediaRecordingButton/BUILD @@ -24,6 +24,8 @@ swift_library( "//submodules/Components/LottieAnimationComponent", "//submodules/TelegramUI/Components/LottieComponent", "//submodules/TelegramUI/Components/LegacyInstantVideoController", + "//submodules/TelegramUI/Components/GlassBackgroundComponent", + "//submodules/Components/ComponentDisplayAdapters", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/ChatTextInputMediaRecordingButton/Sources/ChatTextInputMediaRecordingButton.swift b/submodules/TelegramUI/Components/ChatTextInputMediaRecordingButton/Sources/ChatTextInputMediaRecordingButton.swift index 2d263109cb..0ca1dff8b9 100644 --- a/submodules/TelegramUI/Components/ChatTextInputMediaRecordingButton/Sources/ChatTextInputMediaRecordingButton.swift +++ b/submodules/TelegramUI/Components/ChatTextInputMediaRecordingButton/Sources/ChatTextInputMediaRecordingButton.swift @@ -14,6 +14,8 @@ import ComponentFlow import LottieAnimationComponent import LottieComponent import LegacyInstantVideoController +import GlassBackgroundComponent +import ComponentDisplayAdapters private let offsetThreshold: CGFloat = 10.0 private let dismissOffsetThreshold: CGFloat = 70.0 @@ -453,17 +455,23 @@ public final class ChatTextInputMediaRecordingButton: TGModernConversationInputM self.updateAnimation(previousMode: self.mode) self.pallete = legacyInputMicPalette(from: theme) - self.micDecorationValue?.setColor( self.theme.chat.inputPanel.actionControlFillColor) + self.micDecorationValue?.setColor(self.theme.chat.inputPanel.actionControlFillColor) (self.micLockValue as? LockView)?.updateTheme(theme) } - public override func createLockPanelView() -> UIView! { + public override func createLockPanelView() -> (UIView & TGModernConversationInputMicButtonLockPanelView)! { + let isDark: Bool + let tintColor: UIColor if self.hidesOnLock { - let view = WrapperBlurrredBackgroundView(frame: CGRect(origin: .zero, size: CGSize(width: 40.0, height: 72.0))) - return view + isDark = false + tintColor = UIColor(white: 0.0, alpha: 0.5) } else { - return super.createLockPanelView() + isDark = self.theme.overallDarkAppearance + tintColor = self.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.65) } + + let view = WrapperBlurrredBackgroundView(size: CGSize(width: 40.0, height: 72.0), isDark: isDark, tintColor: tintColor) + return view } public func cancelRecording() { @@ -614,16 +622,22 @@ public final class ChatTextInputMediaRecordingButton: TGModernConversationInputM } } -private class WrapperBlurrredBackgroundView: UIView { - let view: BlurredBackgroundView +private class WrapperBlurrredBackgroundView: UIView, TGModernConversationInputMicButtonLockPanelView { + let isDark: Bool + let glassTintColor: UIColor - override init(frame: CGRect) { - let view = BlurredBackgroundView(color: UIColor(white: 0.0, alpha: 0.5), enableBlur: true) - view.frame = CGRect(origin: .zero, size: frame.size) - view.update(size: frame.size, cornerRadius: frame.width / 2.0, transition: .immediate) + let view: GlassBackgroundView + + init(size: CGSize, isDark: Bool, tintColor: UIColor) { + self.isDark = isDark + self.glassTintColor = tintColor + + let view = GlassBackgroundView() + view.frame = CGRect(origin: CGPoint(), size: size) + view.update(size: size, cornerRadius: min(size.width, size.height) * 0.5, isDark: self.isDark, tintColor: self.glassTintColor, transition: .immediate) self.view = view - super.init(frame: frame) + super.init(frame: CGRect(origin: CGPoint(), size: size)) self.addSubview(view) } @@ -637,7 +651,14 @@ private class WrapperBlurrredBackgroundView: UIView { return super.frame } set { super.frame = newValue - self.view.update(size: newValue.size, cornerRadius: newValue.width / 2.0, transition: .immediate) + self.view.frame = CGRect(origin: CGPoint(), size: newValue.size) + self.view.update(size: newValue.size, cornerRadius: min(newValue.width, newValue.height) * 0.5, isDark: self.isDark, tintColor: self.glassTintColor, transition: .immediate) } } + + func update(_ size: CGSize) { + let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) + transition.updateFrame(view: self.view, frame: CGRect(origin: CGPoint(), size: size)) + self.view.update(size: size, cornerRadius: min(size.width, size.height) * 0.5, isDark: self.isDark, tintColor: self.glassTintColor, transition: ComponentTransition(transition)) + } } diff --git a/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift b/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift index 910841c6bb..edb72d1e7b 100644 --- a/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift +++ b/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift @@ -387,12 +387,16 @@ public final class GlassBackgroundView: UIView { if let nativeView = self.nativeView { let previousFrame = nativeView.frame - transition.containedViewLayoutTransition.animateView { + if transition.animation.isImmediate { nativeView.layer.cornerRadius = cornerRadius nativeView.frame = CGRect(origin: CGPoint(), size: size) + } else { + transition.containedViewLayoutTransition.animateView { + nativeView.layer.cornerRadius = cornerRadius + nativeView.frame = CGRect(origin: CGPoint(), size: size) + } + nativeView.layer.animateFrame(from: previousFrame, to: CGRect(origin: CGPoint(), size: size), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) } - - nativeView.layer.animateFrame(from: previousFrame, to: CGRect(origin: CGPoint(), size: size), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) } if let backgroundNode = self.backgroundNode { backgroundNode.updateColor(color: .clear, forceKeepBlur: tintColor.alpha != 1.0, transition: transition.containedViewLayoutTransition) diff --git a/submodules/TelegramUI/Components/MediaScrubberComponent/Sources/MediaScrubberComponent.swift b/submodules/TelegramUI/Components/MediaScrubberComponent/Sources/MediaScrubberComponent.swift index 1df1b22a8e..1c843521e7 100644 --- a/submodules/TelegramUI/Components/MediaScrubberComponent/Sources/MediaScrubberComponent.swift +++ b/submodules/TelegramUI/Components/MediaScrubberComponent/Sources/MediaScrubberComponent.swift @@ -1826,7 +1826,8 @@ public class TrimView: UIView { endPosition: Double, position: Double, minDuration: Double, - maxDuration: Double + maxDuration: Double, + isBorderless: Bool )? public func update( @@ -1840,10 +1841,13 @@ public class TrimView: UIView { position: Double, minDuration: Double, maxDuration: Double, + isBorderless: Bool = false, transition: ComponentTransition ) -> (leftHandleFrame: CGRect, rightHandleFrame: CGRect) { let isFirstTime = self.params == nil - self.params = (scrubberSize, duration, startPosition, endPosition, position, minDuration, maxDuration) + self.params = (scrubberSize, duration, startPosition, endPosition, position, minDuration, maxDuration, isBorderless) + + self.borderView.isHidden = isBorderless let effectiveHandleWidth: CGFloat let fullTrackHeight: CGFloat @@ -1891,7 +1895,7 @@ public class TrimView: UIView { case .videoMessage: effectiveHandleWidth = 16.0 fullTrackHeight = 33.0 - capsuleOffset = 8.0 + capsuleOffset = 10.0 color = theme.chat.inputPanel.panelControlAccentColor highlightColor = theme.chat.inputPanel.panelControlAccentColor @@ -1918,8 +1922,9 @@ public class TrimView: UIView { capsuleOffset = 8.0 color = theme.chat.inputPanel.panelControlAccentColor highlightColor = theme.chat.inputPanel.panelControlAccentColor + self.borderView.isHidden = true - self.zoneView.backgroundColor = UIColor(white: 1.0, alpha: 0.4) + self.zoneView.backgroundColor = .clear if isFirstTime { self.borderView.image = generateImage(CGSize(width: 3.0, height: fullTrackHeight), rotatedContext: { size, context in @@ -1941,6 +1946,9 @@ public class TrimView: UIView { self.leftHandleView.image = handleImage self.rightHandleView.image = handleImage + self.leftHandleView.image = nil + self.rightHandleView.image = nil + self.leftCapsuleView.backgroundColor = .white self.rightCapsuleView.backgroundColor = .white } @@ -1972,7 +1980,7 @@ public class TrimView: UIView { rightHandleFrame.origin.x = min(rightHandleFrame.origin.x, totalWidth - visualInsets.right - effectiveHandleWidth) transition.setFrame(view: self.rightHandleView, frame: rightHandleFrame) - let capsuleSize = CGSize(width: 2.0, height: 11.0) + let capsuleSize = CGSize(width: 3.0, height: 12.0) transition.setFrame(view: self.leftCapsuleView, frame: CGRect(origin: CGPoint(x: capsuleOffset, y: floorToScreenPixels((leftHandleFrame.height - capsuleSize.height) / 2.0)), size: capsuleSize)) transition.setFrame(view: self.rightCapsuleView, frame: CGRect(origin: CGPoint(x: capsuleOffset, y: floorToScreenPixels((leftHandleFrame.height - capsuleSize.height) / 2.0)), size: capsuleSize)) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index a0bb3a3b5f..79d0318081 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -442,6 +442,8 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode { }, dismissUrlPreview: { }, dismissForwardMessages: { }, dismissSuggestPost: { + }, 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 b5380ef304..84cc9ef8f1 100644 --- a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift @@ -829,6 +829,8 @@ final class PeerSelectionControllerNode: ASDisplayNode { }, dismissUrlPreview: { }, dismissForwardMessages: { }, dismissSuggestPost: { + }, 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 833a4d8dde..0629b6608a 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -3932,7 +3932,7 @@ extension ChatControllerImpl { strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() - let contextController = ContextController(presentationData: strongSelf.presentationData, source: .reference(ChatControllerContextReferenceContentSource(controller: strongSelf, sourceView: node.view, insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomInset, right: 0.0))), items: .single(ContextController.Items(content: .list(items))), gesture: gesture, workaroundUseLegacyImplementation: true) + let contextController = ContextController(presentationData: strongSelf.presentationData, source: .reference(ChatControllerContextReferenceContentSource(controller: strongSelf, sourceView: node.view, insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomInset, right: 0.0), contentInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0))), items: .single(ContextController.Items(content: .list(items))), gesture: gesture, workaroundUseLegacyImplementation: true) contextController.dismissed = { [weak self] in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(interactive: true, { @@ -4364,6 +4364,16 @@ extension ChatControllerImpl { } return state }) + }, 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 cb5425cf54..1c4fcaae6b 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -48,6 +48,7 @@ import SpaceWarpView import ChatSideTopicsPanel import GlassBackgroundComponent import ChatThemeScreen +import ChatTextInputPanelNode final class VideoNavigationControllerDropContentItem: NavigationControllerDropContentItem { let itemNode: OverlayMediaItemNode @@ -236,6 +237,8 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { private var leftPanelContainer: ChatControllerTitlePanelNodeContainer private(set) var leftPanel: (component: AnyComponentWithIdentity, view: ComponentView)? + private var bottomBackgroundEdgeEffectNode: WallpaperEdgeEffectNode? + private var inputPanelBackgroundBlurMask: UIImageView? private var inputPanelBackgroundBlurView: VariableBlurView? @@ -855,6 +858,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { self.textInputPanelNode = ChatTextInputPanelNode(context: context, presentationInterfaceState: chatPresentationInterfaceState, presentationContext: ChatPresentationContext(context: context, backgroundNode: backgroundNode), presentController: { [weak self] controller in self?.interfaceInteraction?.presentController(controller, nil) }) + self.textInputPanelNode?.textInputAccessoryPanel = textInputAccessoryPanel self.textInputPanelNode?.storedInputLanguage = chatPresentationInterfaceState.interfaceState.inputLanguage self.textInputPanelNode?.updateHeight = { [weak self] animated in if let strongSelf = self, let _ = strongSelf.inputPanelNode as? ChatTextInputPanelNode, !strongSelf.ignoreUpdateHeight { @@ -2203,6 +2207,32 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } } + if "".isEmpty { + var bottomBackgroundEdgeEffectNode: WallpaperEdgeEffectNode? + if let current = self.bottomBackgroundEdgeEffectNode { + bottomBackgroundEdgeEffectNode = current + } else { + if let value = self.backgroundNode.makeEdgeEffectNode() { + bottomBackgroundEdgeEffectNode = value + self.bottomBackgroundEdgeEffectNode = value + self.historyNodeContainer.view.superview?.insertSubview(value.view, aboveSubview: self.historyNodeContainer.view) + } + } + + if let bottomBackgroundEdgeEffectNode { + var blurFrame = inputBackgroundFrame + blurFrame.origin.y -= 26.0 + blurFrame.size.height += 100.0 + transition.updateFrame(node: bottomBackgroundEdgeEffectNode, frame: blurFrame) + bottomBackgroundEdgeEffectNode.update( + rect: blurFrame, + edge: WallpaperEdgeEffectEdge(edge: .bottom, size: 80.0), + containerSize: wallpaperBounds.size, + transition: transition + ) + } + } + if !"".isEmpty { let blurView: VariableBlurView let blurMask: UIImageView @@ -3931,9 +3961,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } func sendButtonFrame() -> CGRect? { - if let mediaPreviewNode = self.inputPanelNode as? ChatRecordingPreviewInputPanelNode { - return mediaPreviewNode.convert(mediaPreviewNode.sendButton.frame, to: self) - } else if let frame = self.textInputPanelNode?.actionButtons.frame { + if let frame = self.textInputPanelNode?.actionButtons.frame { return self.textInputPanelNode?.convert(frame, to: self) } else { return nil @@ -3974,10 +4002,6 @@ 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 { - return recordingPreviewPanelNode.frameForInputActionButton().flatMap { - return $0.offsetBy(dx: recordingPreviewPanelNode.frame.minX, dy: recordingPreviewPanelNode.frame.minY) - } } return nil } @@ -4722,7 +4746,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 8db0456530..33cff427ec 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift @@ -9,6 +9,7 @@ import ChatBotStartInputPanelNode import ChatChannelSubscriberInputPanelNode import ChatMessageSelectionInputPanelNode import ChatControllerInteraction +import ChatTextInputPanelNode func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, currentPanel: ChatInputPanelNode?, currentSecondaryPanel: ChatInputPanelNode?, textInputPanelNode: ChatTextInputPanelNode?, chatControllerInteraction: ChatControllerInteraction?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> (primary: ChatInputPanelNode?, secondary: ChatInputPanelNode?) { if let renderedPeer = chatPresentationInterfaceState.renderedPeer, renderedPeer.peer?.restrictionText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) != nil { @@ -418,12 +419,11 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState } } - var displayBotStartPanel = false - var isScheduledMessages = false if case .scheduledMessages = chatPresentationInterfaceState.subject { isScheduledMessages = true } + var displayBotStartPanel = false if !isScheduledMessages { if let _ = chatPresentationInterfaceState.botStartPayload { @@ -436,33 +436,9 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState } } } + let _ = displayBotStartPanel - if displayBotStartPanel, !"".isEmpty { - if let currentPanel = (currentPanel as? ChatBotStartInputPanelNode) ?? (currentSecondaryPanel as? ChatBotStartInputPanelNode) { - currentPanel.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) - return (currentPanel, nil) - } else { - let panel = ChatBotStartInputPanelNode(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) - panel.context = context - panel.chatControllerInteraction = chatControllerInteraction - panel.interfaceInteraction = interfaceInteraction - return (panel, nil) - } - } else { - if let _ = chatPresentationInterfaceState.interfaceState.mediaDraftState { - if let currentPanel = (currentPanel as? ChatRecordingPreviewInputPanelNode) ?? (currentSecondaryPanel as? ChatRecordingPreviewInputPanelNode) { - return (currentPanel, nil) - } else { - let panel = ChatRecordingPreviewInputPanelNode(theme: chatPresentationInterfaceState.theme) - panel.context = context - panel.chatControllerInteraction = chatControllerInteraction - panel.interfaceInteraction = interfaceInteraction - return (panel, nil) - } - } - - displayInputTextPanel = true - } + displayInputTextPanel = true } if case let .customChatContents(customChatContents) = chatPresentationInterfaceState.subject { @@ -504,7 +480,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState let panel = ChatTextInputPanelNode(context: context, presentationInterfaceState: chatPresentationInterfaceState, presentationContext: nil, presentController: { [weak interfaceInteraction] controller in interfaceInteraction?.presentController(controller, nil) }) - + panel.textInputAccessoryPanel = textInputAccessoryPanel panel.chatControllerInteraction = chatControllerInteraction panel.interfaceInteraction = interfaceInteraction panel.context = context diff --git a/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift deleted file mode 100644 index 13204771c7..0000000000 --- a/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift +++ /dev/null @@ -1,1017 +0,0 @@ -import Foundation -import UIKit -import AsyncDisplayKit -import Display -import TelegramCore -import Postbox -import SwiftSignalKit -import TelegramPresentationData -import UniversalMediaPlayer -import AppBundle -import ContextUI -import AnimationUI -import ManagedAnimationNode -import ChatPresentationInterfaceState -import ChatSendButtonRadialStatusNode -import AudioWaveformNode -import ChatInputPanelNode -import TooltipUI -import TelegramNotices -import ComponentFlow -import MediaScrubberComponent -import AnimatedCountLabelNode - -#if SWIFT_PACKAGE -extension AudioWaveformNode: CustomMediaPlayerScrubbingForegroundNode { -} -#else -extension AudioWaveformNode: @retroactive CustomMediaPlayerScrubbingForegroundNode { -} -#endif - -final class ChatRecordingPreviewViewForOverlayContent: UIView, ChatInputPanelViewForOverlayContent { - let ignoreHit: (UIView, CGPoint) -> Bool - - init(ignoreHit: @escaping (UIView, CGPoint) -> Bool) { - self.ignoreHit = ignoreHit - - super.init(frame: CGRect()) - } - - required init(coder: NSCoder) { - preconditionFailure() - } - - func maybeDismissContent(point: CGPoint) { - for subview in self.subviews.reversed() { - if let _ = subview.hitTest(self.convert(point, to: subview), with: nil) { - return - } - } - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - for subview in self.subviews.reversed() { - if let result = subview.hitTest(self.convert(point, to: subview), with: event) { - return result - } - } - - if event == nil || self.ignoreHit(self, point) { - return nil - } - - return nil - } -} - -final class PlayButtonNode: ASDisplayNode { - let backgroundNode: ASDisplayNode - let playButton: HighlightableButtonNode - fileprivate let playPauseIconNode: PlayPauseIconNode - let durationLabel: MediaPlayerTimeTextNode - - var pressed: () -> Void = {} - - init(theme: PresentationTheme) { - self.backgroundNode = ASDisplayNode() - self.backgroundNode.clipsToBounds = true - self.backgroundNode.backgroundColor = theme.chat.inputPanel.actionControlFillColor - self.backgroundNode.cornerRadius = 11.0 - self.backgroundNode.displaysAsynchronously = false - - self.playButton = HighlightableButtonNode() - self.playButton.displaysAsynchronously = false - - self.playPauseIconNode = PlayPauseIconNode() - self.playPauseIconNode.enqueueState(.play, animated: false) - self.playPauseIconNode.customColor = theme.chat.inputPanel.actionControlForegroundColor - - self.durationLabel = MediaPlayerTimeTextNode(textColor: theme.chat.inputPanel.actionControlForegroundColor, textFont: Font.with(size: 13.0, weight: .semibold, traits: .monospacedNumbers)) - self.durationLabel.alignment = .right - self.durationLabel.mode = .normal - self.durationLabel.showDurationIfNotStarted = true - - super.init() - - self.addSubnode(self.backgroundNode) - self.addSubnode(self.playButton) - self.backgroundNode.addSubnode(self.playPauseIconNode) - self.backgroundNode.addSubnode(self.durationLabel) - - self.playButton.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) - } - - override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { - return self.backgroundNode.frame.contains(point) - } - - @objc private func buttonPressed() { - self.pressed() - } - - func update(size: CGSize, transition: ContainedViewLayoutTransition) { - var buttonSize = CGSize(width: 63.0, height: 22.0) - if size.width < 70.0 { - buttonSize.width = 27.0 - } - - transition.updateFrame(node: self.backgroundNode, frame: buttonSize.centered(in: CGRect(origin: .zero, size: size))) - - self.playPauseIconNode.frame = CGRect(origin: CGPoint(x: 3.0, y: 1.0 - UIScreenPixel), size: CGSize(width: 21.0, height: 21.0)) - - transition.updateFrame(node: self.durationLabel, frame: CGRect(origin: CGPoint(x: 18.0, y: 3.0), size: CGSize(width: 35.0, height: 20.0))) - transition.updateAlpha(node: self.durationLabel, alpha: buttonSize.width > 27.0 ? 1.0 : 0.0) - - self.playButton.frame = CGRect(origin: .zero, size: size) - } -} - -final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { - let deleteButton: HighlightableButtonNode - let binNode: AnimationNode - let sendButton: HighlightTrackingButtonNode - let sendBackgroundNode: ASDisplayNode - let sendIconNode: ASImageNode - let textNode: ImmediateAnimatedCountLabelNode - - private var sendButtonRadialStatusNode: ChatSendButtonRadialStatusNode? - private let waveformButton: ASButtonNode - let waveformBackgroundNode: ASImageNode - - let trimView: TrimView - let playButtonNode: PlayButtonNode - - let scrubber = ComponentView() - - var viewOnce = false - let viewOnceButton: ChatRecordingViewOnceButtonNode - let recordMoreButton: ChatRecordingViewOnceButtonNode - - private let waveformNode: AudioWaveformNode - private let waveformForegroundNode: AudioWaveformNode - let waveformScrubberNode: MediaPlayerScrubbingNode - - private var presentationInterfaceState: ChatPresentationInterfaceState? - - private var mediaPlayer: MediaPlayer? - - private var statusValue: MediaPlayerStatus? - private let statusDisposable = MetaDisposable() - private var scrubbingDisposable: Disposable? - - private var positionTimer: SwiftSignalKit.Timer? - - private(set) var gestureRecognizer: ContextGesture? - - init(theme: PresentationTheme) { - self.deleteButton = HighlightableButtonNode() - self.deleteButton.displaysAsynchronously = false - - self.binNode = AnimationNode( - animation: "BinBlue", - colors: [ - "Cap11.Cap2.Обводка 1": theme.chat.inputPanel.panelControlAccentColor, - "Bin 5.Bin.Обводка 1": theme.chat.inputPanel.panelControlAccentColor, - "Cap12.Cap1.Обводка 1": theme.chat.inputPanel.panelControlAccentColor, - "Line15.Line1.Обводка 1": theme.chat.inputPanel.panelControlAccentColor, - "Line13.Line3.Обводка 1": theme.chat.inputPanel.panelControlAccentColor, - "Line14.Line2.Обводка 1": theme.chat.inputPanel.panelControlAccentColor, - "Line13.Обводка 1": theme.chat.inputPanel.panelControlAccentColor, - ] - ) - - self.sendButton = HighlightTrackingButtonNode() - self.sendButton.displaysAsynchronously = false - self.sendButton.isExclusiveTouch = true - - self.sendBackgroundNode = ASDisplayNode() - self.sendBackgroundNode.backgroundColor = theme.chat.inputPanel.actionControlFillColor - - self.sendIconNode = ASImageNode() - self.sendIconNode.displaysAsynchronously = false - self.sendIconNode.image = PresentationResourcesChat.chatInputPanelSendIconImage(theme) - - self.textNode = ImmediateAnimatedCountLabelNode() - self.textNode.isUserInteractionEnabled = false - - 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.waveformButton = ASButtonNode() - self.waveformButton.accessibilityTraits.insert(.startsMediaSession) - - self.waveformNode = AudioWaveformNode() - self.waveformNode.isLayerBacked = true - self.waveformForegroundNode = AudioWaveformNode() - self.waveformForegroundNode.isLayerBacked = true - - self.waveformScrubberNode = MediaPlayerScrubbingNode(content: .custom(backgroundNode: self.waveformNode, foregroundContentNode: self.waveformForegroundNode)) - - self.trimView = TrimView(frame: .zero) - self.trimView.isHollow = true - self.playButtonNode = PlayButtonNode(theme: theme) - - super.init() - - self.viewForOverlayContent = ChatRecordingPreviewViewForOverlayContent( - ignoreHit: { [weak self] view, point in - guard let strongSelf = self else { - return false - } - if strongSelf.view.hitTest(view.convert(point, to: strongSelf.view), with: nil) != nil { - return true - } - if view.convert(point, to: strongSelf.view).y > strongSelf.view.bounds.maxY { - return true - } - return false - } - ) - - 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.waveformScrubberNode) - //self.addSubnode(self.waveformButton) - - self.view.addSubview(self.trimView) - self.addSubnode(self.playButtonNode) - - self.sendButton.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) - } - } - } - - self.playButtonNode.pressed = { [weak self] in - guard let self else { - return - } - self.waveformPressed() - } - - self.waveformScrubberNode.seek = { [weak self] timestamp in - guard let self else { - return - } - var timestamp = timestamp - if let recordedMediaPreview = self.presentationInterfaceState?.interfaceState.mediaDraftState, case let .audio(audio) = recordedMediaPreview, let trimRange = audio.trimRange { - timestamp = max(trimRange.lowerBound, min(timestamp, trimRange.upperBound)) - } - self.mediaPlayer?.seek(timestamp: timestamp) - } - - self.scrubbingDisposable = (self.waveformScrubberNode.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) - }) - - self.deleteButton.addTarget(self, action: #selector(self.deletePressed), forControlEvents: [.touchUpInside]) - self.sendButton.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]) - - self.waveformButton.addTarget(self, action: #selector(self.waveformPressed), forControlEvents: .touchUpInside) - } - - deinit { - self.mediaPlayer?.pause() - self.statusDisposable.dispose() - self.scrubbingDisposable?.dispose() - self.positionTimer?.invalidate() - } - - override func didLoad() { - super.didLoad() - - let gestureRecognizer = ContextGesture(target: nil, action: nil) - self.sendButton.view.addGestureRecognizer(gestureRecognizer) - self.gestureRecognizer = gestureRecognizer - gestureRecognizer.shouldBegin = { [weak self] _ in - if let self, self.viewOnce { - return false - } - return true - } - gestureRecognizer.activated = { [weak self] gesture, _ in - guard let strongSelf = self else { - return - } - strongSelf.interfaceInteraction?.displaySendMessageOptions(strongSelf.sendButton, gesture) - } - - if let viewForOverlayContent = self.viewForOverlayContent { - viewForOverlayContent.addSubnode(self.viewOnceButton) - viewForOverlayContent.addSubnode(self.recordMoreButton) - } - - self.view.disablesInteractiveTransitionGestureRecognizer = true - } - - private func ensureHasTimer() { - if self.positionTimer == nil { - let timer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in - self?.checkPosition() - }, queue: Queue.mainQueue()) - self.positionTimer = timer - timer.start() - } - } - - func checkPosition() { - guard let statusValue = self.statusValue, let recordedMediaPreview = self.presentationInterfaceState?.interfaceState.mediaDraftState, case let .audio(audio) = recordedMediaPreview, let trimRange = audio.trimRange, let mediaPlayer = self.mediaPlayer else { - return - } - let timestampSeconds: Double - if !statusValue.generationTimestamp.isZero { - timestampSeconds = statusValue.timestamp + (CACurrentMediaTime() - statusValue.generationTimestamp) - } else { - timestampSeconds = statusValue.timestamp - } - if timestampSeconds >= trimRange.upperBound { - mediaPlayer.seek(timestamp: trimRange.lowerBound, play: false) - } - } - - private func stopTimer() { - self.positionTimer?.invalidate() - self.positionTimer = nil - } - - private func maybePresentViewOnceTooltip() { - guard let context = self.context else { - return - } - let _ = (ApplicationSpecificNotice.getVoiceMessagesPlayOnceSuggestion(accountManager: context.sharedContext.accountManager) - |> deliverOnMainQueue).startStandalone(next: { [weak self] counter in - guard let self, let interfaceState = self.presentationInterfaceState else { - return - } - if counter >= 3 { - return - } - - Queue.mainQueue().after(0.3) { - self.displayViewOnceTooltip(text: interfaceState.strings.Chat_TapToPlayVoiceMessageOnceTooltip, hasIcon: true) - } - - let _ = ApplicationSpecificNotice.incrementVoiceMessagesPlayOnceSuggestion(accountManager: context.sharedContext.accountManager).startStandalone() - }) - } - - 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 { - var isFirstTime = false - if self.presentationInterfaceState == nil { - isFirstTime = true - } - - var innerSize = CGSize(width: 44.0, height: 44.0) - if let sendPaidMessageStars = interfaceState.sendPaidMessageStars { - self.sendIconNode.alpha = 0.0 - self.textNode.isHidden = false - - var amount = sendPaidMessageStars.value - if let forwardedCount = interfaceState.interfaceState.forwardMessageIds?.count, forwardedCount > 0 { - amount = sendPaidMessageStars.value * Int64(forwardedCount) - if interfaceState.interfaceState.effectiveInputState.inputText.length > 0 { - amount += sendPaidMessageStars.value - } - } - - let text = "\(amount)" - let font = Font.with(size: 17.0, design: .round, weight: .semibold, traits: .monospacedNumbers) - let badgeString = NSMutableAttributedString(string: "⭐️ ", font: font, textColor: interfaceState.theme.chat.inputPanel.actionControlForegroundColor) - if let range = badgeString.string.range(of: "⭐️") { - badgeString.addAttribute(.attachment, value: PresentationResourcesChat.chatPlaceholderStarIcon(interfaceState.theme)!, range: NSRange(range, in: badgeString.string)) - badgeString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: badgeString.string)) - } - var segments: [AnimatedCountLabelNode.Segment] = [] - segments.append(.text(0, badgeString)) - for char in text { - if let intValue = Int(String(char)) { - segments.append(.number(intValue, NSAttributedString(string: String(char), font: font, textColor: interfaceState.theme.chat.inputPanel.actionControlForegroundColor))) - } - } - self.textNode.segments = segments - - let textSize = self.textNode.updateLayout(size: CGSize(width: 100.0, height: 100.0), animated: transition.isAnimated) - let buttonInset: CGFloat = 14.0 - innerSize.width = textSize.width + buttonInset * 2.0 - transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: 12.0, y: floorToScreenPixels((innerSize.height - textSize.height) / 2.0)), size: textSize)) - } else { - self.sendIconNode.alpha = 1.0 - 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)) - 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) - self.sendBackgroundNode.cornerRadius = backgroundSize.height / 2.0 - - if let icon = self.sendIconNode.image { - transition.updateFrame(node: self.sendIconNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((innerSize.width - icon.size.width) / 2.0), y: floorToScreenPixels((innerSize.height - icon.size.height) / 2.0)), size: icon.size)) - } - - let waveformBackgroundFrame = CGRect(origin: CGPoint(x: leftInset + 45.0, y: 7.0 - UIScreenPixel), size: CGSize(width: width - leftInset - rightInset - 45.0 - innerSize.width - 1.0, height: 33.0)) - - if self.presentationInterfaceState != interfaceState { - var updateWaveform = false - if self.presentationInterfaceState?.interfaceState.mediaDraftState != interfaceState.interfaceState.mediaDraftState { - updateWaveform = true - } - if self.presentationInterfaceState?.strings !== interfaceState.strings { - self.deleteButton.accessibilityLabel = interfaceState.strings.VoiceOver_MessageContextDelete - self.sendButton.accessibilityLabel = interfaceState.strings.VoiceOver_MessageContextSend - self.waveformButton.accessibilityLabel = interfaceState.strings.VoiceOver_Chat_RecordPreviewVoiceMessage - } - - self.presentationInterfaceState = interfaceState - - if let recordedMediaPreview = interfaceState.interfaceState.mediaDraftState, let context = self.context { - switch recordedMediaPreview { - case let .audio(audio): - self.waveformButton.isHidden = false - self.waveformBackgroundNode.isHidden = false - self.waveformForegroundNode.isHidden = false - self.waveformScrubberNode.isHidden = false - self.playButtonNode.isHidden = false - - if let view = self.scrubber.view, view.superview != nil { - view.removeFromSuperview() - } - - if updateWaveform { - self.waveformNode.setup(color: interfaceState.theme.chat.inputPanel.actionControlForegroundColor.withAlphaComponent(0.5), gravity: .center, waveform: audio.waveform) - self.waveformForegroundNode.setup(color: interfaceState.theme.chat.inputPanel.actionControlForegroundColor, gravity: .center, waveform: audio.waveform) - if self.mediaPlayer != nil { - self.mediaPlayer?.pause() - } - let mediaManager = context.sharedContext.mediaManager - let mediaPlayer = MediaPlayer(audioSessionManager: mediaManager.audioSession, postbox: context.account.postbox, userLocation: .other, userContentType: .audio, resourceReference: .standalone(resource: audio.resource), streamable: .none, video: false, preferSoftwareDecoding: false, enableSound: true, fetchAutomatically: true) - mediaPlayer.actionAtEnd = .action { [weak self] in - guard let self else { - return - } - Queue.mainQueue().async { - guard let interfaceState = self.presentationInterfaceState else { - return - } - var timestamp: Double = 0.0 - if let recordedMediaPreview = interfaceState.interfaceState.mediaDraftState, case let .audio(audio) = recordedMediaPreview, let trimRange = audio.trimRange { - timestamp = trimRange.lowerBound - } - self.mediaPlayer?.seek(timestamp: timestamp, play: false) - } - } - 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.statusDisposable.set((mediaPlayer.status - |> deliverOnMainQueue).startStrict(next: { [weak self] status in - if let self { - switch status.status { - case .playing, .buffering(_, true, _, _): - self.statusValue = status - if let recordedMediaPreview = self.presentationInterfaceState?.interfaceState.mediaDraftState, case let .audio(audio) = recordedMediaPreview, let _ = audio.trimRange { - self.ensureHasTimer() - } - self.playButtonNode.playPauseIconNode.enqueueState(.pause, animated: true) - default: - self.statusValue = nil - self.stopTimer() - self.playButtonNode.playPauseIconNode.enqueueState(.play, animated: true) - } - } - })) - } - - let minDuration = max(1.0, 56.0 * audio.duration / waveformBackgroundFrame.size.width) - let (leftHandleFrame, rightHandleFrame) = self.trimView.update( - style: .voiceMessage, - theme: interfaceState.theme, - visualInsets: .zero, - scrubberSize: waveformBackgroundFrame.size, - duration: audio.duration, - startPosition: audio.trimRange?.lowerBound ?? 0.0, - endPosition: audio.trimRange?.upperBound ?? Double(audio.duration), - position: 0.0, - minDuration: minDuration, - maxDuration: Double(audio.duration), - transition: .immediate - ) - self.trimView.trimUpdated = { [weak self] start, end, updatedEnd, apply in - if let self { - self.mediaPlayer?.pause() - self.interfaceInteraction?.updateRecordingTrimRange(start, end, updatedEnd, apply) - if apply { - if !updatedEnd { - self.mediaPlayer?.seek(timestamp: start, play: true) - } else { - self.mediaPlayer?.seek(timestamp: max(0.0, end - 1.0), play: true) - } - self.playButtonNode.durationLabel.isScrubbing = false - Queue.mainQueue().after(0.1) { - self.waveformForegroundNode.alpha = 1.0 - } - } else { - self.playButtonNode.durationLabel.isScrubbing = true - self.waveformForegroundNode.alpha = 0.0 - } - - let startFraction = start / Double(audio.duration) - let endFraction = end / Double(audio.duration) - self.waveformForegroundNode.trimRange = startFraction ..< endFraction - } - } - self.trimView.frame = waveformBackgroundFrame - self.trimView.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)) - case let .video(video): - self.waveformButton.isHidden = true - self.waveformBackgroundNode.isHidden = true - self.waveformForegroundNode.isHidden = true - self.waveformScrubberNode.isHidden = true - self.playButtonNode.isHidden = true - - let scrubberSize = self.scrubber.update( - transition: .immediate, - component: AnyComponent( - MediaScrubberComponent( - context: context, - style: .videoMessage, - theme: interfaceState.theme, - generationTimestamp: 0, - position: 0, - minDuration: 1.0, - maxDuration: 60.0, - isPlaying: false, - tracks: [ - MediaScrubberComponent.Track( - id: 0, - content: .video(frames: video.frames, framesUpdateTimestamp: video.framesUpdateTimestamp), - duration: Double(video.duration), - trimRange: video.trimRange, - offset: nil, - isMain: true - ) - ], - isCollage: false, - positionUpdated: { _, _ in }, - trackTrimUpdated: { [weak self] _, start, end, updatedEnd, apply in - if let self { - self.interfaceInteraction?.updateRecordingTrimRange(start, end, updatedEnd, apply) - } - }, - trackOffsetUpdated: { _, _, _ in }, - trackLongPressed: { _, _ in } - ) - ), - environment: {}, - forceUpdate: false, - containerSize: CGSize(width: min(424.0, width - leftInset - rightInset - 45.0 - innerSize.width - 1.0), height: 33.0) - ) - - if let view = self.scrubber.view { - if view.superview == nil { - self.view.addSubview(view) - } - view.bounds = CGRect(origin: .zero, size: scrubberSize) - } - } - } - } - - if let view = self.scrubber.view { - view.frame = CGRect(origin: CGPoint(x: min(width - innerSize.width - view.bounds.width, max(leftInset + 45.0, floorToScreenPixels((width - view.bounds.width) / 2.0))), y: 7.0 - UIScreenPixel), size: view.bounds.size) - } - - 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))) - - self.binNode.frame = self.deleteButton.bounds - - var viewOnceOffset: CGFloat = 0.0 - if interfaceState.interfaceState.replyMessageSubject != nil { - viewOnceOffset = -35.0 - } - - let viewOnceSize = self.viewOnceButton.update(theme: interfaceState.theme) - let viewOnceButtonFrame = CGRect(origin: CGPoint(x: width - rightInset - 44.0 - UIScreenPixel, y: -64.0 - 53.0 + viewOnceOffset), size: viewOnceSize) - transition.updateFrame(node: self.viewOnceButton, frame: viewOnceButtonFrame) - - let recordMoreSize = self.recordMoreButton.update(theme: interfaceState.theme) - let recordMoreButtonFrame = CGRect(origin: CGPoint(x: width - rightInset - 44.0 - UIScreenPixel, y: -64.0 + viewOnceOffset), size: recordMoreSize) - transition.updateFrame(node: self.recordMoreButton, frame: recordMoreButtonFrame) - - var isScheduledMessages = false - if case .scheduledMessages = interfaceState.subject { - isScheduledMessages = true - } - - if let slowmodeState = interfaceState.slowmodeState, !isScheduledMessages { - let sendButtonRadialStatusNode: ChatSendButtonRadialStatusNode - if let current = self.sendButtonRadialStatusNode { - sendButtonRadialStatusNode = current - } else { - sendButtonRadialStatusNode = ChatSendButtonRadialStatusNode(color: interfaceState.theme.chat.inputPanel.panelControlAccentColor) - sendButtonRadialStatusNode.alpha = self.sendButton.alpha - self.sendButtonRadialStatusNode = sendButtonRadialStatusNode - self.addSubnode(sendButtonRadialStatusNode) - } - - transition.updateSublayerTransformScale(layer: self.sendButton.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.slowmodeState = slowmodeState - } else { - if let sendButtonRadialStatusNode = self.sendButtonRadialStatusNode { - self.sendButtonRadialStatusNode = nil - sendButtonRadialStatusNode.removeFromSupernode() - } - 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.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))) - - prevInputPanelNode?.frame = CGRect(origin: .zero, size: CGSize(width: width, height: panelHeight)) - if let prevTextInputPanelNode = self.prevInputPanelNode as? ChatTextInputPanelNode { - self.prevInputPanelNode = nil - - self.viewOnceButton.isHidden = prevTextInputPanelNode.viewOnceButton.isHidden - self.viewOnce = prevTextInputPanelNode.viewOnce - self.viewOnceButton.update(isSelected: self.viewOnce, animated: false) - - prevTextInputPanelNode.viewOnceButton.isHidden = true - prevTextInputPanelNode.viewOnce = false - - self.recordMoreButton.isEnabled = false - self.viewOnceButton.layer.animatePosition(from: prevTextInputPanelNode.viewOnceButton.position, to: self.viewOnceButton.position, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in - prevTextInputPanelNode.viewOnceButton.isHidden = false - prevTextInputPanelNode.viewOnceButton.update(isSelected: false, animated: false) - - Queue.mainQueue().after(0.3) { - self.recordMoreButton.isEnabled = true - } - }) - - self.recordMoreButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - self.recordMoreButton.layer.animateScale(from: 0.1, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) - - if let audioRecordingDotNode = prevTextInputPanelNode.audioRecordingDotNode { - let startAlpha = CGFloat(audioRecordingDotNode.layer.presentation()?.opacity ?? 1.0) - audioRecordingDotNode.layer.removeAllAnimations() - audioRecordingDotNode.layer.animateScale(from: 1.0, to: 0.3, duration: 0.15, removeOnCompletion: false) - audioRecordingDotNode.layer.animateAlpha(from: startAlpha, to: 0.0, duration: 0.15, removeOnCompletion: false) - } - - if let audioRecordingTimeNode = prevTextInputPanelNode.audioRecordingTimeNode { - audioRecordingTimeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) - audioRecordingTimeNode.layer.animateScale(from: 1.0, to: 0.3, duration: 0.15, removeOnCompletion: false) - let timePosition = audioRecordingTimeNode.position - audioRecordingTimeNode.layer.animatePosition(from: timePosition, to: CGPoint(x: timePosition.x - 20, y: timePosition.y), duration: 0.15, removeOnCompletion: false) - } - - if let audioRecordingCancelIndicator = prevTextInputPanelNode.audioRecordingCancelIndicator { - audioRecordingCancelIndicator.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) - } - - prevTextInputPanelNode.actionButtons.micButton.animateOut(true) - - if let view = self.scrubber.view { - view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) - 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.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.trimView.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.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), - to: waveformBackgroundFrame, - duration: 0.2, - delay: 0.12, - timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, - removeOnCompletion: false - ) { [weak self, weak prevTextInputPanelNode] finished in - if prevTextInputPanelNode?.supernode === self { - prevTextInputPanelNode?.removeFromSupernode() - prevTextInputPanelNode?.finishedTransitionToPreview = true - prevTextInputPanelNode?.requestLayout() - } - } - } - - if isFirstTime, !self.viewOnceButton.isHidden { - self.maybePresentViewOnceTooltip() - } - - return panelHeight - } - - override func canHandleTransition(from prevInputPanelNode: ChatInputPanelNode?) -> Bool { - return prevInputPanelNode is ChatTextInputPanelNode - } - - @objc func deletePressed() { - self.viewOnce = false - self.tooltipController?.dismiss() - - self.mediaPlayer?.pause() - self.interfaceInteraction?.deleteRecordedMedia() - } - - @objc func sendPressed() { - self.tooltipController?.dismiss() - - self.interfaceInteraction?.sendRecordedMedia(false, self.viewOnce) - - self.viewOnce = false - } - - private weak var tooltipController: TooltipScreen? - @objc private func viewOncePressed() { - guard let context = self.context, let interfaceState = self.presentationInterfaceState else { - return - } - self.viewOnce = !self.viewOnce - - self.viewOnceButton.update(isSelected: self.viewOnce, animated: true) - - self.tooltipController?.dismiss() - if self.viewOnce { - self.displayViewOnceTooltip(text: interfaceState.strings.Chat_PlayVoiceMessageOnceTooltip, hasIcon: true) - - let _ = ApplicationSpecificNotice.incrementVoiceMessagesPlayOnceSuggestion(accountManager: context.sharedContext.accountManager, count: 3).startStandalone() - } - } - - @objc private func recordMorePressed() { - self.tooltipController?.dismiss() - - self.interfaceInteraction?.resumeMediaRecording() - } - - private func displayViewOnceTooltip(text: String, hasIcon: Bool) { - guard let context = self.context, let parentController = self.interfaceInteraction?.chatController() else { - return - } - - let absoluteFrame = self.viewOnceButton.view.convert(self.viewOnceButton.bounds, to: parentController.view) - let location = CGRect(origin: CGPoint(x: absoluteFrame.midX - 20.0, y: absoluteFrame.midY), size: CGSize()) - - let tooltipController = TooltipScreen( - account: context.account, - sharedContext: context.sharedContext, - text: .markdown(text: text), - balancedTextLayout: true, - constrainWidth: 240.0, - style: .customBlur(UIColor(rgb: 0x18181a), 0.0), - arrowStyle: .small, - icon: hasIcon ? .animation(name: "anim_autoremove_on", delay: 0.1, tintColor: nil) : nil, - location: .point(location, .right), - displayDuration: .default, - inset: 8.0, - cornerRadius: 8.0, - shouldDismissOnTouch: { _, _ in - return .ignore - } - ) - self.tooltipController = tooltipController - - parentController.present(tooltipController, in: .current) - } - - @objc func waveformPressed() { - guard let mediaPlayer = self.mediaPlayer else { - return - } - if let recordedMediaPreview = self.presentationInterfaceState?.interfaceState.mediaDraftState, case let .audio(audio) = recordedMediaPreview, let trimRange = audio.trimRange { - let _ = (mediaPlayer.status - |> map(Optional.init) - |> timeout(0.3, queue: Queue.mainQueue(), alternate: .single(nil)) - |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] status in - guard let self, let mediaPlayer = self.mediaPlayer else { - return - } - if let status { - if case .playing = status.status { - mediaPlayer.pause() - } else if status.timestamp <= trimRange.lowerBound { - mediaPlayer.seek(timestamp: trimRange.lowerBound, play: true) - } else { - mediaPlayer.play() - } - } else { - mediaPlayer.seek(timestamp: trimRange.lowerBound, play: true) - } - }) - } else { - mediaPlayer.togglePlayPause() - } - } - - override func minimalHeight(interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { - return defaultHeight(metrics: metrics) - } - - func frameForInputActionButton() -> CGRect? { - return self.sendButton.frame - } -} - -private enum PlayPauseIconNodeState: Equatable { - case play - case pause -} - -private final class PlayPauseIconNode: ManagedAnimationNode { - private let duration: Double = 0.35 - private var iconState: PlayPauseIconNodeState = .pause - - init() { - super.init(size: CGSize(width: 21.0, height: 21.0)) - - self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 41), duration: 0.01)) - } - - func enqueueState(_ state: PlayPauseIconNodeState, animated: Bool) { - guard self.iconState != state else { - return - } - - let previousState = self.iconState - self.iconState = state - - switch previousState { - case .pause: - switch state { - case .play: - if animated { - self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 83), duration: self.duration)) - } else { - self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01)) - } - case .pause: - break - } - case .play: - switch state { - case .pause: - if animated { - self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 41), duration: self.duration)) - } else { - self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 41), duration: 0.01)) - } - case .play: - break - } - } - } -} - - -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 - } -} diff --git a/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift b/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift index 4bfe3ee995..199f3b6c77 100644 --- a/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift +++ b/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift @@ -81,6 +81,25 @@ public enum WallpaperDisplayMode { } } +public struct WallpaperEdgeEffectEdge: Equatable { + public enum Edge { + case top + case bottom + } + + public var edge: Edge + public var size: CGFloat + + public init(edge: Edge, size: CGFloat) { + self.edge = edge + self.size = size + } +} + +public protocol WallpaperEdgeEffectNode: ASDisplayNode { + func update(rect: CGRect, edge: WallpaperEdgeEffectEdge, containerSize: CGSize, transition: ContainedViewLayoutTransition) +} + public protocol WallpaperBackgroundNode: ASDisplayNode { var isReady: Signal { get } var rotation: CGFloat { get set } @@ -99,6 +118,8 @@ public protocol WallpaperBackgroundNode: ASDisplayNode { func hasExtraBubbleBackground() -> Bool func makeDimmedNode() -> ASDisplayNode? + + func makeEdgeEffectNode() -> WallpaperEdgeEffectNode? } private final class EffectImageLayer: SimpleLayer, GradientBackgroundPatternOverlayLayer { @@ -493,7 +514,7 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou if needsGradientBackground, let gradientBackgroundNode = gradientBackgroundSource { if self.gradientWallpaperNode == nil { - let gradientWallpaperNode = GradientBackgroundNode.CloneNode(parentNode: gradientBackgroundNode) + let gradientWallpaperNode = GradientBackgroundNode.CloneNode(parentNode: gradientBackgroundNode, isDimmed: true) gradientWallpaperNode.frame = self.bounds self.gradientWallpaperNode = gradientWallpaperNode self.insertSubnode(gradientWallpaperNode, at: 0) @@ -765,6 +786,8 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou private var modelStickerNode: DefaultAnimatedStickerNodeImpl? + fileprivate let edgeEffectNodes = SparseBag>() + private var isSettingUpWallpaper: Bool = false private struct CachedValidPatternImage { @@ -1188,6 +1211,12 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou self.contentNode.alpha = 1.0 self.patternImageLayer.backgroundColor = nil } + + for edgeEffectNode in self.edgeEffectNodes { + if let edgeEffectNode = edgeEffectNode.value { + edgeEffectNode.updatePattern(isInverted: invertPattern) + } + } default: self.patternImageDisposable.set(nil) self.symbolImageDisposable.set(nil) @@ -1198,6 +1227,12 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou self.backgroundColor = nil self.gradientBackgroundNode?.contentView.alpha = 1.0 self.contentNode.alpha = 1.0 + + for edgeEffectNode in self.edgeEffectNodes { + if let edgeEffectNode = edgeEffectNode.value { + edgeEffectNode.updatePattern(isInverted: false) + } + } } } @@ -1670,11 +1705,149 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou public func makeDimmedNode() -> ASDisplayNode? { if let gradientBackgroundNode = self.gradientBackgroundNode { - return GradientBackgroundNode.CloneNode(parentNode: gradientBackgroundNode) + return GradientBackgroundNode.CloneNode(parentNode: gradientBackgroundNode, isDimmed: true) } else { return nil } } + + public func makeEdgeEffectNode() -> WallpaperEdgeEffectNode? { + if let gradientBackgroundNode = self.gradientBackgroundNode { + let node = WallpaperEdgeEffectNodeImpl(parentNode: self) + node.cloneNode = GradientBackgroundNode.CloneNode(parentNode: gradientBackgroundNode, isDimmed: false) + return node + } else { + return nil + } + } +} + +private final class WallpaperEdgeEffectNodeImpl: ASDisplayNode, WallpaperEdgeEffectNode { + var cloneNode: GradientBackgroundNode.CloneNode? { + didSet { + if self.cloneNode !== oldValue { + if let cloneNode = self.cloneNode { + self.containerNode.insertSubnode(cloneNode, at: 0) + + if let params = self.params { + self.updateImpl(rect: params.rect, edge: params.edge, containerSize: params.containerSize, transition: .immediate) + } + } + } + } + } + + private struct Params: Equatable { + let rect: CGRect + let edge: WallpaperEdgeEffectEdge + let containerSize: CGSize + + init(rect: CGRect, edge: WallpaperEdgeEffectEdge, containerSize: CGSize) { + self.rect = rect + self.edge = edge + self.containerSize = containerSize + } + } + + private let containerNode: ASDisplayNode + private let containerMaskingNode: ASDisplayNode + private let overlayNode: ASDisplayNode + private let maskView: UIImageView + + private weak var parentNode: WallpaperBackgroundNodeImpl? + private var index: Int? + private var params: Params? + + private var isInverted: Bool = false + + init(parentNode: WallpaperBackgroundNodeImpl) { + self.parentNode = parentNode + + self.containerNode = ASDisplayNode() + self.containerNode.anchorPoint = CGPoint() + self.containerNode.clipsToBounds = true + + self.containerMaskingNode = ASDisplayNode() + self.containerMaskingNode.addSubnode(self.containerNode) + + self.overlayNode = ASDisplayNode() + + self.maskView = UIImageView() + + super.init() + + self.addSubnode(self.containerMaskingNode) + self.containerMaskingNode.view.mask = self.maskView + + self.containerNode.addSubnode(self.overlayNode) + + self.index = parentNode.edgeEffectNodes.add(Weak(self)) + } + + deinit { + if let index = self.index, let parentNode = self.parentNode { + parentNode.edgeEffectNodes.remove(index) + } + } + + func updatePattern(isInverted: Bool) { + if self.isInverted != isInverted { + self.isInverted = isInverted + + self.overlayNode.backgroundColor = isInverted ? .black : .clear + } + } + + func update(rect: CGRect, edge: WallpaperEdgeEffectEdge, containerSize: CGSize, transition: ContainedViewLayoutTransition) { + let params = Params(rect: rect, edge: edge, containerSize: containerSize) + if self.params != params { + self.params = params + self.updateImpl(rect: params.rect, edge: params.edge, containerSize: params.containerSize, transition: transition) + } + } + + private func updateImpl(rect: CGRect, edge: WallpaperEdgeEffectEdge, containerSize: CGSize, transition: ContainedViewLayoutTransition) { + transition.updateFrame(node: self.containerMaskingNode, frame: CGRect(origin: CGPoint(), size: rect.size)) + transition.updateBounds(node: self.containerNode, bounds: CGRect(origin: CGPoint(x: rect.minX, y: rect.minY), size: rect.size)) + + if self.maskView.image?.size.height != edge.size { + let baseGradientAlpha: CGFloat = 0.75 + let numSteps = 8 + let firstStep = 1 + let firstLocation = 0.0 + let colors: [UIColor] = (0 ..< numSteps).map { i in + if i < firstStep { + return UIColor(white: 1.0, alpha: 1.0) + } else { + let step: CGFloat = CGFloat(i - firstStep) / CGFloat(numSteps - firstStep - 1) + let value: CGFloat = bezierPoint(0.42, 0.0, 0.58, 1.0, step) + return UIColor(white: 1.0, alpha: baseGradientAlpha * value) + } + } + let locations: [CGFloat] = (0 ..< numSteps).map { i in + if i < firstStep { + return 0.0 + } else { + let step: CGFloat = CGFloat(i - firstStep) / CGFloat(numSteps - firstStep - 1) + return (firstLocation + (1.0 - firstLocation) * step) + } + } + + self.maskView.image = generateGradientImage( + size: CGSize(width: 8.0, height: edge.size), + colors: colors, + locations: locations + )?.stretchableImage(withLeftCapWidth: 0, topCapHeight: Int(edge.size)) + } + + transition.updateFrame(view: self.maskView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: rect.size)) + + transition.updateFrame(node: self.overlayNode, frame: CGRect(origin: CGPoint(), size: containerSize)) + + if let cloneNode = self.cloneNode { + transition.updateFrame(node: cloneNode, frame: CGRect(origin: CGPoint(), size: containerSize)) + } + } } private protocol WallpaperComponentView: AnyObject {