From c69c578e1d0786a3ce716ec11e19e752080ffb4e Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 5 Jul 2022 17:20:56 +0200 Subject: [PATCH 1/2] Fix typing statuses --- submodules/ChatListUI/Sources/Node/ChatListNode.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index 515dbd6d2b..ef7a2a5ecc 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -1312,6 +1312,8 @@ public final class ChatListNode: ListView { switch activity { case .interactingWithEmoji: return true + case .speakingInGroupCall: + return true default: return false } @@ -1343,9 +1345,9 @@ public final class ChatListNode: ListView { return engine.data.get(EngineDataMap( activitiesByPeerId.keys.filter { key in if case .global = key.category { - return false - } else { return true + } else { + return false } }.map { key in return TelegramEngine.EngineData.Item.Peer.Peer(id: key.peerId) From e99cefa2d695d00acf23f42dd05794a09f97bea6 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 5 Jul 2022 19:16:06 +0200 Subject: [PATCH 2/2] Ongoing work on the updated entity input --- .../AttachmentTextInputPanelNode.swift | 8 +- .../Sources/AttachmentPanel.swift | 2 +- .../Sources/ChatTextInputPanelState.swift | 31 +- .../Source/Base/Transition.swift | 79 +++- .../Source/Components/Button.swift | 79 +++- .../Source/Host/ComponentHostView.swift | 15 +- .../Sources/LottieAnimationComponent.swift | 3 - .../Sources/PagerComponent.swift | 110 ++++- .../Display/Source/ContextGesture.swift | 2 +- submodules/Display/Source/PortalView.swift | 2 + submodules/Display/Source/TextNode.swift | 4 + .../Pasteboard/Sources/Pasteboard.swift | 3 + .../ShimmerEffect/Sources/ShimmerEffect.swift | 66 +++ .../Sources/StickerShimmerEffectNode.swift | 4 +- submodules/TelegramUI/BUILD | 2 + .../Sources/ChatInputPanelContainer.swift | 70 ++- .../Components/EntityKeyboard/BUILD | 1 + .../Sources/EmojiPagerContentComponent.swift | 375 ++++++++++++--- .../Sources/EntityKeyboard.swift | 114 ++++- ...tyKeyboardTopContainerPanelComponent.swift | 69 ++- .../EntityKeyboardTopPanelComponent.swift | 434 ++++++++++++++++-- .../Sources/GifPagerContentComponent.swift | 1 + .../Sources/MultiAnimationRenderer.swift | 24 + .../Contents.json | 12 + .../EntityInputGlobeIcon.imageset/planet.pdf | 255 ++++++++++ .../Animations/anim_smiletosticker.json | 1 + .../Animations/anim_stickertosmile.json | 1 + .../Sources/ChatBotStartInputPanelNode.swift | 2 +- .../Sources/ChatButtonKeyboardInputNode.swift | 2 +- .../ChatChannelSubscriberInputPanelNode.swift | 2 +- .../Sources/ChatControllerNode.swift | 47 +- .../Sources/ChatEntityKeyboardInputNode.swift | 54 ++- .../ChatFeedNavigationInputPanelNode.swift | 2 +- .../TelegramUI/Sources/ChatInputNode.swift | 7 +- .../Sources/ChatInputPanelNode.swift | 2 +- .../Sources/ChatInterfaceInputContexts.swift | 5 +- .../Sources/ChatMediaInputNode.swift | 36 +- .../ChatMessageReportInputPanelNode.swift | 2 +- .../ChatMessageSelectionInputPanelNode.swift | 14 +- .../ChatMessageTextBubbleContentNode.swift | 3 +- .../ChatRecordingPreviewInputPanelNode.swift | 2 +- .../ChatRestrictedInputPanelNode.swift | 2 +- .../Sources/ChatSearchInputPanelNode.swift | 10 +- .../ChatTextInputActionButtonsNode.swift | 7 +- .../Sources/ChatTextInputPanelNode.swift | 166 ++++--- .../Sources/ChatUnblockInputPanelNode.swift | 2 +- .../Sources/DeleteChatInputPanelNode.swift | 2 +- .../Sources/PeerInfo/PeerInfoScreen.swift | 2 +- .../Sources/PeerSelectionControllerNode.swift | 2 +- ...retChatHandshakeStatusInputPanelNode.swift | 2 +- .../Sources/StringWithAppliedEntities.swift | 2 + 51 files changed, 1827 insertions(+), 317 deletions(-) create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputGlobeIcon.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputGlobeIcon.imageset/planet.pdf create mode 100644 submodules/TelegramUI/Resources/Animations/anim_smiletosticker.json create mode 100644 submodules/TelegramUI/Resources/Animations/anim_stickertosmile.json diff --git a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift index c0afa5705a..55d5d295a4 100644 --- a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift +++ b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift @@ -352,7 +352,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS guard let presentationInterfaceState = self.presentationInterfaceState else { return 0.0 } - return self.updateLayout(width: size.width, leftInset: sideInset, rightInset: sideInset, bottomInset: 0.0, additionalSideInsets: UIEdgeInsets(), maxHeight: size.height, isSecondary: false, transition: .immediate, interfaceState: presentationInterfaceState, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact)) + return self.updateLayout(width: size.width, leftInset: sideInset, rightInset: sideInset, bottomInset: 0.0, additionalSideInsets: UIEdgeInsets(), maxHeight: size.height, isSecondary: false, transition: .immediate, interfaceState: presentationInterfaceState, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), isMediaInputExpanded: false) } public func setCaption(_ caption: NSAttributedString?) { @@ -496,7 +496,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS return minimalHeight } - public func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { + 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 hadLayout = self.validLayout != nil let previousAdditionalSideInsets = self.validLayout?.3 self.validLayout = (width, leftInset, rightInset, additionalSideInsets, maxHeight, metrics, isSecondary) @@ -1110,7 +1110,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS self.focusUpdated?(true) if self.isCaption, let (width, leftInset, rightInset, additionalSideInsets, maxHeight, metrics, isSecondary) = self.validLayout, let presentationInterfaceState = self.presentationInterfaceState { - let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: 0.0, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: .animated(duration: 0.3, curve: .easeInOut), interfaceState: presentationInterfaceState, metrics: metrics) + let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: 0.0, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: .animated(duration: 0.3, curve: .easeInOut), interfaceState: presentationInterfaceState, metrics: metrics, isMediaInputExpanded: false) } } @@ -1121,7 +1121,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS self.focusUpdated?(false) if self.isCaption, let (width, leftInset, rightInset, additionalSideInsets, maxHeight, metrics, isSecondary) = self.validLayout, let presentationInterfaceState = self.presentationInterfaceState { - let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: 0.0, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: .animated(duration: 0.3, curve: .easeInOut), interfaceState: presentationInterfaceState, metrics: metrics) + let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: 0.0, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: .animated(duration: 0.3, curve: .easeInOut), interfaceState: presentationInterfaceState, metrics: metrics, isMediaInputExpanded: false) } } diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index 0cb84a5b20..547c91d193 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -1098,7 +1098,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate { if textInputPanelNode.frame.width.isZero { panelTransition = .immediate } - let panelHeight = textInputPanelNode.updateLayout(width: layout.size.width, leftInset: insets.left + layout.safeInsets.left, rightInset: insets.right + layout.safeInsets.right, bottomInset: 0.0, additionalSideInsets: UIEdgeInsets(), maxHeight: layout.size.height / 2.0, isSecondary: false, transition: panelTransition, interfaceState: self.presentationInterfaceState, metrics: layout.metrics) + let panelHeight = textInputPanelNode.updateLayout(width: layout.size.width, leftInset: insets.left + layout.safeInsets.left, rightInset: insets.right + layout.safeInsets.right, bottomInset: 0.0, additionalSideInsets: UIEdgeInsets(), maxHeight: layout.size.height / 2.0, isSecondary: false, transition: panelTransition, interfaceState: self.presentationInterfaceState, metrics: layout.metrics, isMediaInputExpanded: false) let panelFrame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: panelHeight) if textInputPanelNode.frame.width.isZero { textInputPanelNode.frame = panelFrame diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatTextInputPanelState.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatTextInputPanelState.swift index 2e50a789fa..f973517907 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatTextInputPanelState.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatTextInputPanelState.swift @@ -3,13 +3,42 @@ import AccountContext import SwiftSignalKit public enum ChatTextInputAccessoryItem: Equatable { + public enum Key: Hashable { + case keyboard + case stickers + case inputButtons + case commands + case silentPost + case messageAutoremoveTimeout + case scheduledMessages + } + case keyboard - case stickers(Bool) + case stickers(isEnabled: Bool, isEmoji: Bool) case inputButtons case commands case silentPost(Bool) case messageAutoremoveTimeout(Int32?) case scheduledMessages + + public var key: Key { + switch self { + case .keyboard: + return .keyboard + case .stickers: + return .stickers + case .inputButtons: + return .inputButtons + case .commands: + return .commands + case .silentPost: + return .silentPost + case .messageAutoremoveTimeout: + return .messageAutoremoveTimeout + case .scheduledMessages: + return .scheduledMessages + } + } } public final class InstantVideoControllerRecordingStatus { diff --git a/submodules/ComponentFlow/Source/Base/Transition.swift b/submodules/ComponentFlow/Source/Base/Transition.swift index ecfc9ec761..a44933d09d 100644 --- a/submodules/ComponentFlow/Source/Base/Transition.swift +++ b/submodules/ComponentFlow/Source/Base/Transition.swift @@ -128,6 +128,16 @@ private extension Transition.Animation.Curve { } } +public extension Transition.Animation { + var isImmediate: Bool { + if case .none = self { + return true + } else { + return false + } + } +} + public struct Transition { public enum Animation { public enum Curve { @@ -245,6 +255,61 @@ public struct Transition { } } + public func setBounds(layer: CALayer, bounds: CGRect, completion: ((Bool) -> Void)? = nil) { + if layer.bounds == bounds { + completion?(true) + return + } + switch self.animation { + case .none: + layer.bounds = bounds + layer.removeAnimation(forKey: "bounds") + completion?(true) + case .curve: + let previousBounds = layer.presentation()?.bounds ?? layer.bounds + layer.bounds = bounds + + self.animateBounds(layer: layer, from: previousBounds, to: layer.bounds, completion: completion) + } + } + + public func setPosition(layer: CALayer, position: CGPoint, completion: ((Bool) -> Void)? = nil) { + if layer.position == position { + completion?(true) + return + } + switch self.animation { + case .none: + layer.position = position + layer.removeAnimation(forKey: "position") + completion?(true) + case .curve: + let previousPosition = layer.presentation()?.position ?? layer.position + layer.position = position + + self.animatePosition(layer: layer, from: previousPosition, to: layer.position, completion: completion) + } + } + + public func attachAnimation(view: UIView, completion: @escaping (Bool) -> Void) { + switch self.animation { + case .none: + completion(true) + case let .curve(duration, curve): + view.layer.animate( + from: 0.0 as NSNumber, + to: 1.0 as NSNumber, + keyPath: "attached\(UInt32.random(in: 0 ... UInt32.max))", + duration: duration, + delay: 0.0, + curve: curve, + removeOnCompletion: true, + additive: true, + completion: completion + ) + } + } + public func setAlpha(view: UIView, alpha: CGFloat, completion: ((Bool) -> Void)? = nil) { if view.alpha == alpha { completion?(true) @@ -356,11 +421,19 @@ public struct Transition { } public func animatePosition(view: UIView, from fromValue: CGPoint, to toValue: CGPoint, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { + self.animatePosition(layer: view.layer, from: fromValue, to: toValue, additive: additive, completion: completion) + } + + public func animateBounds(view: UIView, from fromValue: CGRect, to toValue: CGRect, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { + self.animateBounds(layer: view.layer, from: fromValue, to: toValue, additive: additive, completion: completion) + } + + public func animatePosition(layer: CALayer, from fromValue: CGPoint, to toValue: CGPoint, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { switch self.animation { case .none: completion?(true) case let .curve(duration, curve): - view.layer.animate( + layer.animate( from: NSValue(cgPoint: fromValue), to: NSValue(cgPoint: toValue), keyPath: "position", @@ -374,12 +447,12 @@ public struct Transition { } } - public func animateBounds(view: UIView, from fromValue: CGRect, to toValue: CGRect, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { + public func animateBounds(layer: CALayer, from fromValue: CGRect, to toValue: CGRect, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { switch self.animation { case .none: break case let .curve(duration, curve): - view.layer.animate( + layer.animate( from: NSValue(cgRect: fromValue), to: NSValue(cgRect: toValue), keyPath: "bounds", diff --git a/submodules/ComponentFlow/Source/Components/Button.swift b/submodules/ComponentFlow/Source/Components/Button.swift index 078558a048..27f749aa5f 100644 --- a/submodules/ComponentFlow/Source/Components/Button.swift +++ b/submodules/ComponentFlow/Source/Components/Button.swift @@ -7,6 +7,7 @@ public final class Button: Component { public let tag: AnyObject? public let automaticHighlight: Bool public let action: () -> Void + public let holdAction: (() -> Void)? convenience public init( content: AnyComponent, @@ -17,7 +18,8 @@ public final class Button: Component { minSize: nil, tag: nil, automaticHighlight: true, - action: action + action: action, + holdAction: nil ) } @@ -26,13 +28,15 @@ public final class Button: Component { minSize: CGSize? = nil, tag: AnyObject? = nil, automaticHighlight: Bool = true, - action: @escaping () -> Void + action: @escaping () -> Void, + holdAction: (() -> Void)? ) { self.content = content self.minSize = minSize self.tag = tag self.automaticHighlight = automaticHighlight self.action = action + self.holdAction = holdAction } public func minSize(_ minSize: CGSize?) -> Button { @@ -41,7 +45,19 @@ public final class Button: Component { minSize: minSize, tag: self.tag, automaticHighlight: self.automaticHighlight, - action: self.action + action: self.action, + holdAction: self.holdAction + ) + } + + public func withHoldAction(_ holdAction: (() -> Void)?) -> Button { + return Button( + content: self.content, + minSize: self.minSize, + tag: self.tag, + automaticHighlight: self.automaticHighlight, + action: self.action, + holdAction: holdAction ) } @@ -51,7 +67,8 @@ public final class Button: Component { minSize: self.minSize, tag: tag, automaticHighlight: self.automaticHighlight, - action: self.action + action: self.action, + holdAction: self.holdAction ) } @@ -86,6 +103,9 @@ public final class Button: Component { } } + private var holdActionTriggerred: Bool = false + private var holdActionTimer: Timer? + override init(frame: CGRect) { self.contentView = ComponentHostView() self.contentView.isUserInteractionEnabled = false @@ -101,6 +121,10 @@ public final class Button: Component { fatalError("init(coder:) has not been implemented") } + deinit { + self.holdActionTimer?.invalidate() + } + public func matches(tag: Any) -> Bool { if let component = self.component, let componentTag = component.tag { let tag = tag as AnyObject @@ -112,24 +136,69 @@ public final class Button: Component { } @objc private func pressed() { - self.component?.action() + if self.holdActionTriggerred { + self.holdActionTriggerred = false + } else { + self.component?.action() + } } override public func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { self.currentIsHighlighted = true + self.holdActionTriggerred = false + + if self.component?.holdAction != nil { + self.holdActionTriggerred = true + self.component?.action() + + self.holdActionTimer?.invalidate() + if #available(iOS 10.0, *) { + let holdActionTimer = Timer(timeInterval: 1.0, repeats: false, block: { [weak self] _ in + guard let strongSelf = self else { + return + } + strongSelf.holdActionTimer?.invalidate() + strongSelf.component?.holdAction?() + strongSelf.beginExecuteHoldActionTimer() + }) + self.holdActionTimer = holdActionTimer + RunLoop.main.add(holdActionTimer, forMode: .common) + } + } + return super.beginTracking(touch, with: event) } + private func beginExecuteHoldActionTimer() { + self.holdActionTimer?.invalidate() + if #available(iOS 10.0, *) { + let holdActionTimer = Timer(timeInterval: 0.2, repeats: true, block: { [weak self] _ in + guard let strongSelf = self else { + return + } + strongSelf.component?.holdAction?() + }) + self.holdActionTimer = holdActionTimer + RunLoop.main.add(holdActionTimer, forMode: .common) + } + } + override public func endTracking(_ touch: UITouch?, with event: UIEvent?) { self.currentIsHighlighted = false + self.holdActionTimer?.invalidate() + self.holdActionTimer = nil + super.endTracking(touch, with: event) } override public func cancelTracking(with event: UIEvent?) { self.currentIsHighlighted = false + self.holdActionTimer?.invalidate() + self.holdActionTimer = nil + super.cancelTracking(with: event) } diff --git a/submodules/ComponentFlow/Source/Host/ComponentHostView.swift b/submodules/ComponentFlow/Source/Host/ComponentHostView.swift index bd29143893..0e27198b92 100644 --- a/submodules/ComponentFlow/Source/Host/ComponentHostView.swift +++ b/submodules/ComponentFlow/Source/Host/ComponentHostView.swift @@ -106,8 +106,21 @@ public final class ComponentHostView: UIView { } override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.alpha.isZero { + return nil + } + for view in self.subviews.reversed() { + if let result = view.hitTest(self.convert(point, to: view), with: event), result.isUserInteractionEnabled { + return result + } + } + let result = super.hitTest(point, with: event) - return result + if result != self { + return result + } else { + return nil + } } public func findTaggedView(tag: Any) -> UIView? { diff --git a/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift b/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift index c3887fa3ac..cad54a967a 100644 --- a/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift +++ b/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift @@ -157,9 +157,6 @@ public final class LottieAnimationComponent: Component { for (key, value) in component.animation.colors { view.setValueProvider(ColorValueProvider(value.lottieColorValue), keypath: AnimationKeypath(keypath: "\(key).Color")) - /*let colorCallback = LOTColorValueCallback(color: value.cgColor) - self.colorCallbacks.append(colorCallback) - view.setValueDelegate(colorCallback, for: LOTKeypath(string: "\(key).Color"))*/ } self.animationView = view diff --git a/submodules/Components/PagerComponent/Sources/PagerComponent.swift b/submodules/Components/PagerComponent/Sources/PagerComponent.swift index 040f784ca4..a122249858 100644 --- a/submodules/Components/PagerComponent/Sources/PagerComponent.swift +++ b/submodules/Components/PagerComponent/Sources/PagerComponent.swift @@ -9,11 +9,15 @@ public protocol PagerExpandableScrollView: UIScrollView { public protocol PagerPanGestureRecognizer: UIGestureRecognizer { } +open class PagerExternalTopPanelContainer: SparseContainerView { +} + public final class PagerComponentChildEnvironment: Equatable { public struct ContentScrollingUpdate { public var relativeOffset: CGFloat public var absoluteOffsetToTopEdge: CGFloat? public var absoluteOffsetToBottomEdge: CGFloat? + public var isReset: Bool public var isInteracting: Bool public var transition: Transition @@ -21,12 +25,14 @@ public final class PagerComponentChildEnvironment: Equatable { relativeOffset: CGFloat, absoluteOffsetToTopEdge: CGFloat?, absoluteOffsetToBottomEdge: CGFloat?, + isReset: Bool, isInteracting: Bool, transition: Transition ) { self.relativeOffset = relativeOffset self.absoluteOffsetToTopEdge = absoluteOffsetToTopEdge self.absoluteOffsetToBottomEdge = absoluteOffsetToBottomEdge + self.isReset = isReset self.isInteracting = isInteracting self.transition = transition } @@ -61,6 +67,7 @@ public final class PagerComponentPanelEnvironment: Equatabl public let activeContentId: AnyHashable? public let navigateToContentId: (AnyHashable) -> Void public let visibilityFractionUpdated: ActionSlot<(CGFloat, Transition)> + public let isExpandedUpdated: (Bool, Transition) -> Void init( contentOffset: CGFloat, @@ -70,7 +77,8 @@ public final class PagerComponentPanelEnvironment: Equatabl contentAccessoryRightButtons: [AnyComponentWithIdentity], activeContentId: AnyHashable?, navigateToContentId: @escaping (AnyHashable) -> Void, - visibilityFractionUpdated: ActionSlot<(CGFloat, Transition)> + visibilityFractionUpdated: ActionSlot<(CGFloat, Transition)>, + isExpandedUpdated: @escaping (Bool, Transition) -> Void ) { self.contentOffset = contentOffset self.contentTopPanels = contentTopPanels @@ -80,6 +88,7 @@ public final class PagerComponentPanelEnvironment: Equatabl self.activeContentId = activeContentId self.navigateToContentId = navigateToContentId self.visibilityFractionUpdated = visibilityFractionUpdated + self.isExpandedUpdated = isExpandedUpdated } public static func ==(lhs: PagerComponentPanelEnvironment, rhs: PagerComponentPanelEnvironment) -> Bool { @@ -122,6 +131,12 @@ public final class PagerComponentViewTag { } } +public enum PagerComponentPanelHideBehavior { + case hideOnScroll + case show + case hide +} + public final class PagerComponent: Component { public typealias EnvironmentType = ChildEnvironmentType @@ -134,10 +149,11 @@ public final class PagerComponent? public let topPanel: AnyComponent>? - public let externalTopPanelContainer: UIView? + public let externalTopPanelContainer: PagerExternalTopPanelContainer? public let bottomPanel: AnyComponent>? public let panelStateUpdated: ((PagerComponentPanelState, Transition) -> Void)? - public let hidePanels: Bool + public let isTopPanelExpandedUpdated: (Bool, Transition) -> Void + public let panelHideBehavior: PagerComponentPanelHideBehavior public init( contentInsets: UIEdgeInsets, @@ -149,10 +165,11 @@ public final class PagerComponent?, topPanel: AnyComponent>?, - externalTopPanelContainer: UIView?, + externalTopPanelContainer: PagerExternalTopPanelContainer?, bottomPanel: AnyComponent>?, panelStateUpdated: ((PagerComponentPanelState, Transition) -> Void)?, - hidePanels: Bool + isTopPanelExpandedUpdated: @escaping (Bool, Transition) -> Void, + panelHideBehavior: PagerComponentPanelHideBehavior ) { self.contentInsets = contentInsets self.contents = contents @@ -166,7 +183,8 @@ public final class PagerComponent Bool { @@ -197,7 +215,7 @@ public final class PagerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let previousPanelHideBehavior = self.component?.panelHideBehavior + + var panelStateTransition = transition + if let previousPanelHideBehavior = previousPanelHideBehavior, previousPanelHideBehavior != component.panelHideBehavior, panelStateTransition.animation.isImmediate { + panelStateTransition = Transition(animation: .curve(duration: 0.3, curve: .spring)) + } + self.component = component self.state = state @@ -345,7 +372,9 @@ public final class PagerComponent> - var topPanelTransition = transition + var topPanelTransition = panelStateTransition if let current = self.topPanelView { topPanelView = current } else { topPanelTransition = .immediate topPanelView = ComponentHostView>() - topPanelView.clipsToBounds = true self.topPanelView = topPanelView } @@ -381,7 +409,13 @@ public final class PagerComponent> - var bottomPanelTransition = transition + var bottomPanelTransition = panelStateTransition if let current = self.bottomPanelView { bottomPanelView = current } else { @@ -449,7 +488,9 @@ public final class PagerComponent - var contentBackgroundTransition = transition + var contentBackgroundTransition = panelStateTransition if let current = self.contentBackgroundView { contentBackgroundView = current } else { @@ -633,7 +680,7 @@ public final class PagerComponent View { diff --git a/submodules/Display/Source/ContextGesture.swift b/submodules/Display/Source/ContextGesture.swift index 7a2a1b0de0..217df908aa 100644 --- a/submodules/Display/Source/ContextGesture.swift +++ b/submodules/Display/Source/ContextGesture.swift @@ -36,7 +36,7 @@ public func cancelParentGestures(view: UIView, ignore: [UIGestureRecognizer] = [ node.highligthedChanged(false) } if let superview = view.superview { - cancelParentGestures(view: superview) + cancelParentGestures(view: superview, ignore: ignore) } } diff --git a/submodules/Display/Source/PortalView.swift b/submodules/Display/Source/PortalView.swift index aa8c401fed..d5a58cb6c6 100644 --- a/submodules/Display/Source/PortalView.swift +++ b/submodules/Display/Source/PortalView.swift @@ -16,6 +16,8 @@ public class PortalView { if let portalSuperview = self.view.superview, let index = portalSuperview.subviews.firstIndex(of: self.view) { portalSuperview.insertSubview(self.view, at: index) + } else if let portalSuperlayer = self.view.layer.superlayer, let index = portalSuperlayer.sublayers?.firstIndex(of: self.view.layer) { + portalSuperlayer.insertSublayer(self.view.layer, at: UInt32(index)) } } } diff --git a/submodules/Display/Source/TextNode.swift b/submodules/Display/Source/TextNode.swift index a5b9e8c6bc..9bc8efeef9 100644 --- a/submodules/Display/Source/TextNode.swift +++ b/submodules/Display/Source/TextNode.swift @@ -1435,6 +1435,10 @@ public class TextNode: ASDisplayNode { for run in glyphRuns { let run = run as! CTRun let glyphCount = CTRunGetGlyphCount(run) + let attributes = CTRunGetAttributes(run) as NSDictionary + if attributes["Attribute__EmbeddedItem"] != nil { + continue + } CTRunDraw(run, context, CFRangeMake(0, glyphCount)) } } diff --git a/submodules/Pasteboard/Sources/Pasteboard.swift b/submodules/Pasteboard/Sources/Pasteboard.swift index 203d275a49..855805a504 100644 --- a/submodules/Pasteboard/Sources/Pasteboard.swift +++ b/submodules/Pasteboard/Sources/Pasteboard.swift @@ -51,6 +51,9 @@ private func chatInputStateString(attributedString: NSAttributedString) -> NSAtt if let _ = attributes[.underlineStyle] { string.addAttribute(ChatTextInputAttributes.underline, value: true as NSNumber, range: range) } + if let value = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute { + string.addAttribute(ChatTextInputAttributes.customEmoji, value: value, range: range) + } }) return string } diff --git a/submodules/ShimmerEffect/Sources/ShimmerEffect.swift b/submodules/ShimmerEffect/Sources/ShimmerEffect.swift index e2827e4ea2..311a5ac2c7 100644 --- a/submodules/ShimmerEffect/Sources/ShimmerEffect.swift +++ b/submodules/ShimmerEffect/Sources/ShimmerEffect.swift @@ -430,3 +430,69 @@ public final class ShimmerEffectNode: ASDisplayNode { self.effectNode.frame = CGRect(origin: CGPoint(), size: size) } } + +public final class StandaloneShimmerEffect { + private var image: UIImage? + + private var background: UIColor? + private var foreground: UIColor? + + public var layer: CALayer? { + didSet { + if self.layer !== oldValue { + self.updateLayer() + } + } + } + + public init() { + } + + public func update(background: UIColor, foreground: UIColor) { + if self.background == background && self.foreground == foreground { + return + } + self.background = background + self.foreground = foreground + + self.image = generateImage(CGSize(width: 1.0, height: 320.0), opaque: false, scale: 1.0, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(background.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + + context.clip(to: CGRect(origin: CGPoint(), size: size)) + + let transparentColor = foreground.withAlphaComponent(0.0).cgColor + let peakColor = foreground.cgColor + + var locations: [CGFloat] = [0.0, 0.5, 1.0] + let colors: [CGColor] = [transparentColor, peakColor, transparentColor] + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + }) + + self.updateLayer() + } + + public func updateLayer() { + guard let layer = self.layer, let image = self.image else { + return + } + + layer.contents = image.cgImage + + if layer.animation(forKey: "shimmer") == nil { + let animation = CABasicAnimation(keyPath: "contentsRect.origin.y") + animation.fromValue = 1.0 as NSNumber + animation.toValue = -1.0 as NSNumber + animation.isAdditive = true + animation.repeatCount = .infinity + animation.duration = 0.8 + animation.beginTime = layer.convertTime(1.0, from: nil) + layer.add(animation, forKey: "shimmer") + } + } +} diff --git a/submodules/ShimmerEffect/Sources/StickerShimmerEffectNode.swift b/submodules/ShimmerEffect/Sources/StickerShimmerEffectNode.swift index 61c29e57a6..2835c10c6d 100644 --- a/submodules/ShimmerEffect/Sources/StickerShimmerEffectNode.swift +++ b/submodules/ShimmerEffect/Sources/StickerShimmerEffectNode.swift @@ -21,8 +21,8 @@ private func decodeStickerThumbnailData(_ data: Data) -> String { return string } -public func generateStickerPlaceholderImage(data: Data?, size: CGSize, imageSize: CGSize, backgroundColor: UIColor?, foregroundColor: UIColor) -> UIImage? { - return generateImage(size, rotatedContext: { size, context in +public func generateStickerPlaceholderImage(data: Data?, size: CGSize, scale: CGFloat? = nil, imageSize: CGSize, backgroundColor: UIColor?, foregroundColor: UIColor) -> UIImage? { + return generateImage(size, scale: scale, rotatedContext: { size, context in if let backgroundColor = backgroundColor { context.setFillColor(backgroundColor.cgColor) context.setBlendMode(.copy) diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index c0eb480146..7fe179a22d 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -291,6 +291,8 @@ swift_library( "//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters", "//submodules/Media/ConvertOpusToAAC:ConvertOpusToAAC", "//submodules/Media/LocalAudioTranscription:LocalAudioTranscription", + "//submodules/Components/PagerComponent:PagerComponent", + "//submodules/Components/LottieAnimationComponent:LottieAnimationComponent", ] + select({ "@build_bazel_rules_apple//apple:ios_armv7": [], "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, diff --git a/submodules/TelegramUI/Components/ChatInputPanelContainer/Sources/ChatInputPanelContainer.swift b/submodules/TelegramUI/Components/ChatInputPanelContainer/Sources/ChatInputPanelContainer.swift index 3ecfc8b2e8..609dee148f 100644 --- a/submodules/TelegramUI/Components/ChatInputPanelContainer/Sources/ChatInputPanelContainer.swift +++ b/submodules/TelegramUI/Components/ChatInputPanelContainer/Sources/ChatInputPanelContainer.swift @@ -7,21 +7,18 @@ import PagerComponent private func traceScrollView(view: UIView, point: CGPoint) -> (UIScrollView?, Bool) { for subview in view.subviews.reversed() { let subviewPoint = view.convert(point, to: subview) - if subview.frame.contains(point) { + if subview.frame.contains(point) || subview is PagerExternalTopPanelContainer { let (result, shouldContinue) = traceScrollView(view: subview, point: subviewPoint) if let result = result { return (result, false) } else if subview.backgroundColor != nil { return (nil, false) - } else if !shouldContinue{ + } else if !shouldContinue { return (nil, false) } } } if let scrollView = view as? UIScrollView { - if scrollView is ListViewScroller || scrollView is GridNodeScrollerView { - return (nil, false) - } return (scrollView, false) } return (nil, true) @@ -31,6 +28,7 @@ private final class ExpansionPanRecognizer: UIGestureRecognizer, UIGestureRecogn enum LockDirection { case up case down + case any } var requiredLockDirection: LockDirection = .up @@ -83,14 +81,23 @@ private final class ExpansionPanRecognizer: UIGestureRecognizer, UIGestureRecogn var found = false let point = touch.location(in: self.view) - if let _ = view.hitTest(point, with: event) as? UIButton { - } else if let scrollView = traceScrollView(view: view, point: point).0 { - let contentOffset = scrollView.contentOffset - let contentInset = scrollView.contentInset - if contentOffset.y.isLessThanOrEqualTo(contentInset.top) { + + let hitView = view.hitTest(point, with: event) + + if let _ = hitView as? UIButton { + } else if let hitView = hitView, hitView.asyncdisplaykit_node is ASButtonNode { + } else { + if let scrollView = traceScrollView(view: view, point: point).0 { + if scrollView is ListViewScroller || scrollView is GridNodeScrollerView { + found = false + } else { + found = true + } + } else { found = true } } + if found { self.beginPosition = point } else { @@ -118,28 +125,46 @@ private final class ExpansionPanRecognizer: UIGestureRecognizer, UIGestureRecogn } var lockDirection: LockDirection? let point = touch.location(in: self.view) + let tracedView = view.hitTest(point, with: event) if let scrollView = traceScrollView(view: view, point: point).0 { - let contentOffset = scrollView.contentOffset - let contentInset = scrollView.contentInset - if contentOffset.y <= contentInset.top { - lockDirection = self.requiredLockDirection + if !(scrollView is PagerExpandableScrollView) { + lockDirection = .any + } else { + let contentOffset = scrollView.contentOffset + let contentInset = scrollView.contentInset + if contentOffset.y <= contentInset.top { + lockDirection = .down + } } + } else { + lockDirection = .any } if let lockDirection = lockDirection { if abs(translation.y) > 2.0 { switch lockDirection { case .up: if translation.y < 0.0 { + if let tracedView = tracedView { + cancelParentGestures(view: tracedView, ignore: [self]) + } self.state = .began } else { self.state = .failed } case .down: if translation.y > 0.0 { + if let tracedView = tracedView { + cancelParentGestures(view: tracedView, ignore: [self]) + } self.state = .began } else { self.state = .failed } + case .any: + if let tracedView = tracedView { + cancelParentGestures(view: tracedView, ignore: [self]) + } + self.state = .began } } } else { @@ -179,6 +204,7 @@ public final class ChatInputPanelContainer: SparseNode, UIScrollViewDelegate { private var scrollableDistance: CGFloat? public private(set) var initialExpansionFraction: CGFloat = 0.0 public private(set) var expansionFraction: CGFloat = 0.0 + public private(set) var stableIsExpanded: Bool = false override public init() { super.init() @@ -231,6 +257,8 @@ public final class ChatInputPanelContainer: SparseNode, UIScrollViewDelegate { } } + self.stableIsExpanded = self.expansionFraction == 1.0 + if let expansionRecognizer = self.expansionRecognizer { expansionRecognizer.requiredLockDirection = self.expansionFraction == 0.0 ? .up : .down } @@ -250,10 +278,24 @@ public final class ChatInputPanelContainer: SparseNode, UIScrollViewDelegate { public func expand() { self.expansionFraction = 1.0 self.expansionRecognizer?.requiredLockDirection = self.expansionFraction == 0.0 ? .up : .down + self.stableIsExpanded = self.expansionFraction == 1.0 } public func collapse() { self.expansionFraction = 0.0 self.expansionRecognizer?.requiredLockDirection = self.expansionFraction == 0.0 ? .up : .down + self.stableIsExpanded = self.expansionFraction == 1.0 + } + + public func toggleIfEnabled() { + if let expansionRecognizer = self.expansionRecognizer, expansionRecognizer.isEnabled { + if self.expansionFraction == 0.0 { + self.expansionFraction = 1.0 + } else { + self.expansionFraction = 0.0 + } + self.stableIsExpanded = self.expansionFraction == 1.0 + self.expansionUpdated?(.animated(duration: 0.4, curve: .spring)) + } } } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/BUILD b/submodules/TelegramUI/Components/EntityKeyboard/BUILD index 2c3681c3ec..353a3fa407 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/BUILD +++ b/submodules/TelegramUI/Components/EntityKeyboard/BUILD @@ -38,6 +38,7 @@ swift_library( "//submodules/PremiumUI:PremiumUI", "//submodules/StickerPackPreviewUI:StickerPackPreviewUI", "//submodules/UndoUI:UndoUI", + "//submodules/Components/MultilineTextComponent:MultilineTextComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index 12a0905961..37500a96b5 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -230,7 +230,8 @@ public final class EmojiPagerContentComponent: Component { var width: CGFloat var containerInsets: UIEdgeInsets var itemGroupLayouts: [ItemGroupLayout] - var itemSize: CGFloat + var nativeItemSize: CGFloat + let visibleItemSize: CGFloat var horizontalSpacing: CGFloat var verticalSpacing: CGFloat var verticalGroupSpacing: CGFloat @@ -241,14 +242,17 @@ public final class EmojiPagerContentComponent: Component { self.width = width self.containerInsets = containerInsets + let minItemsPerRow: Int let minSpacing: CGFloat switch itemLayoutType { case .compact: - self.itemSize = 36.0 + minItemsPerRow = 8 + self.nativeItemSize = 36.0 self.verticalSpacing = 9.0 minSpacing = 9.0 case .detailed: - self.itemSize = 76.0 + minItemsPerRow = 5 + self.nativeItemSize = 76.0 self.verticalSpacing = 2.0 minSpacing = 2.0 } @@ -257,8 +261,11 @@ public final class EmojiPagerContentComponent: Component { let itemHorizontalSpace = width - self.containerInsets.left - self.containerInsets.right - self.itemsPerRow = Int((itemHorizontalSpace + minSpacing) / (self.itemSize + minSpacing)) - self.horizontalSpacing = floor((itemHorizontalSpace - self.itemSize * CGFloat(self.itemsPerRow)) / CGFloat(self.itemsPerRow - 1)) + self.itemsPerRow = max(minItemsPerRow, Int((itemHorizontalSpace + minSpacing) / (self.nativeItemSize + minSpacing))) + + self.visibleItemSize = floor((itemHorizontalSpace - CGFloat(self.itemsPerRow - 1) * minSpacing) / CGFloat(self.itemsPerRow)) + + self.horizontalSpacing = floor((itemHorizontalSpace - self.visibleItemSize * CGFloat(self.itemsPerRow)) / CGFloat(self.itemsPerRow - 1)) var verticalGroupOrigin: CGFloat = self.containerInsets.top self.itemGroupLayouts = [] @@ -269,7 +276,7 @@ public final class EmojiPagerContentComponent: Component { } let numRowsInGroup = (itemGroup.itemCount + (self.itemsPerRow - 1)) / self.itemsPerRow - let groupContentSize = CGSize(width: width, height: itemTopOffset + CGFloat(numRowsInGroup) * self.itemSize + CGFloat(max(0, numRowsInGroup - 1)) * self.verticalSpacing) + let groupContentSize = CGSize(width: width, height: itemTopOffset + CGFloat(numRowsInGroup) * self.visibleItemSize + CGFloat(max(0, numRowsInGroup - 1)) * self.verticalSpacing) self.itemGroupLayouts.append(ItemGroupLayout( frame: CGRect(origin: CGPoint(x: 0.0, y: verticalGroupOrigin), size: groupContentSize), id: itemGroup.id, @@ -290,12 +297,12 @@ public final class EmojiPagerContentComponent: Component { return CGRect( origin: CGPoint( - x: self.containerInsets.left + CGFloat(column) * (self.itemSize + self.horizontalSpacing), - y: groupLayout.frame.minY + groupLayout.itemTopOffset + CGFloat(row) * (self.itemSize + self.verticalSpacing) + x: self.containerInsets.left + CGFloat(column) * (self.visibleItemSize + self.horizontalSpacing), + y: groupLayout.frame.minY + groupLayout.itemTopOffset + CGFloat(row) * (self.visibleItemSize + self.verticalSpacing) ), size: CGSize( - width: self.itemSize, - height: self.itemSize + width: self.visibleItemSize, + height: self.visibleItemSize ) ) } @@ -310,9 +317,9 @@ public final class EmojiPagerContentComponent: Component { continue } let offsetRect = rect.offsetBy(dx: -self.containerInsets.left, dy: -group.frame.minY - group.itemTopOffset) - var minVisibleRow = Int(floor((offsetRect.minY - self.verticalSpacing) / (self.itemSize + self.verticalSpacing))) + var minVisibleRow = Int(floor((offsetRect.minY - self.verticalSpacing) / (self.visibleItemSize + self.verticalSpacing))) minVisibleRow = max(0, minVisibleRow) - let maxVisibleRow = Int(ceil((offsetRect.maxY - self.verticalSpacing) / (self.itemSize + self.verticalSpacing))) + let maxVisibleRow = Int(ceil((offsetRect.maxY - self.verticalSpacing) / (self.visibleItemSize + self.verticalSpacing))) let minVisibleIndex = minVisibleRow * self.itemsPerRow let maxVisibleIndex = min(group.itemCount - 1, (maxVisibleRow + 1) * self.itemsPerRow - 1) @@ -330,6 +337,60 @@ public final class EmojiPagerContentComponent: Component { } } + final class ItemPlaceholderView: UIView { + private let shimmerView: PortalSourceView? + private var placeholderView: PortalView? + private let placeholderMaskLayer: SimpleLayer + + init( + context: AccountContext, + file: TelegramMediaFile, + shimmerView: PortalSourceView?, + color: UIColor?, + size: CGSize + ) { + self.shimmerView = shimmerView + self.placeholderView = PortalView() + self.placeholderMaskLayer = SimpleLayer() + + super.init(frame: CGRect()) + + if let placeholderView = self.placeholderView, let shimmerView = self.shimmerView { + placeholderView.view.clipsToBounds = true + placeholderView.view.layer.mask = self.placeholderMaskLayer + self.addSubview(placeholderView.view) + shimmerView.addPortal(view: placeholderView) + } + + Queue.concurrentDefaultQueue().async { [weak self] in + if let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: size, scale: min(2.0, UIScreenScale), imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: color ?? .black) { + Queue.mainQueue().async { + guard let strongSelf = self else { + return + } + + if let _ = color { + strongSelf.layer.contents = image.cgImage + } else { + strongSelf.placeholderMaskLayer.contents = image.cgImage + } + } + } + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(size: CGSize) { + if let placeholderView = self.placeholderView { + placeholderView.view.frame = CGRect(origin: CGPoint(), size: size) + } + self.placeholderMaskLayer.frame = CGRect(origin: CGPoint(), size: size) + } + } + final class ItemLayer: MultiAnimationRenderTarget { struct Key: Hashable { var groupId: AnyHashable @@ -353,7 +414,8 @@ public final class EmojiPagerContentComponent: Component { } } } - private var displayPlaceholder: Bool = false + private(set) var displayPlaceholder: Bool = false + let onUpdateDisplayPlaceholder: (Bool) -> Void init( item: Item, @@ -366,11 +428,13 @@ public final class EmojiPagerContentComponent: Component { placeholderColor: UIColor, blurredBadgeColor: UIColor, displayPremiumBadgeIfAvailable: Bool, - pointSize: CGSize + pointSize: CGSize, + onUpdateDisplayPlaceholder: @escaping (Bool) -> Void ) { self.item = item self.file = file self.placeholderColor = placeholderColor + self.onUpdateDisplayPlaceholder = onUpdateDisplayPlaceholder let scale = min(2.0, UIScreenScale) let pixelSize = CGSize(width: pointSize.width * scale, height: pointSize.height * scale) @@ -414,17 +478,21 @@ public final class EmojiPagerContentComponent: Component { if attemptSynchronousLoad { if !renderer.loadFirstFrameSynchronously(groupId: groupId, target: self, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize) { - self.displayPlaceholder = true - - if let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: self.size, imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: placeholderColor) { - self.contents = image.cgImage - } + self.updateDisplayPlaceholder(displayPlaceholder: true) } loadAnimation() } else { - let _ = renderer.loadFirstFrame(groupId: groupId, target: self, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize, completion: { _ in + let _ = renderer.loadFirstFrame(groupId: groupId, target: self, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize, completion: { [weak self] success in loadAnimation() + + if !success { + guard let strongSelf = self else { + return + } + + strongSelf.updateDisplayPlaceholder(displayPlaceholder: true) + } }) } } else if let dimensions = file.dimensions { @@ -458,7 +526,19 @@ public final class EmojiPagerContentComponent: Component { } override public init(layer: Any) { - preconditionFailure() + guard let layer = layer as? ItemLayer else { + preconditionFailure() + } + + self.item = layer.item + + self.file = layer.file + self.placeholderColor = layer.placeholderColor + self.size = layer.size + + self.onUpdateDisplayPlaceholder = { _ in } + + super.init(layer: layer) } required public init?(coder: NSCoder) { @@ -492,34 +572,65 @@ public final class EmojiPagerContentComponent: Component { } self.displayPlaceholder = displayPlaceholder - let file = self.file - let size = self.size - let placeholderColor = self.placeholderColor + self.onUpdateDisplayPlaceholder(displayPlaceholder) - Queue.concurrentDefaultQueue().async { [weak self] in - if let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: size, imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: placeholderColor) { - Queue.mainQueue().async { - guard let strongSelf = self else { - return - } - - if strongSelf.displayPlaceholder { - strongSelf.contents = image.cgImage + /*if displayPlaceholder { + if self.placeholderView == nil { + self.placeholderView = PortalView() + if let placeholderView = self.placeholderView, let shimmerView = self.shimmerView { + self.addSublayer(placeholderView.view.layer) + placeholderView.view.frame = self.bounds + shimmerView.addPortal(view: placeholderView) + } + } + if self.placeholderMaskLayer == nil { + self.placeholderMaskLayer = SimpleLayer() + self.placeholderView?.view.layer.mask = self.placeholderMaskLayer + } + let file = self.file + let size = self.size + //let placeholderColor = self.placeholderColor + + Queue.concurrentDefaultQueue().async { [weak self] in + if let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: size, imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: .black) { + Queue.mainQueue().async { + guard let strongSelf = self else { + return + } + + if strongSelf.displayPlaceholder { + strongSelf.placeholderMaskLayer?.contents = image.cgImage + } } } } - } + } else { + if let placeholderView = self.placeholderView { + self.placeholderView = nil + placeholderView.view.layer.removeFromSuperlayer() + } + if let _ = self.placeholderMaskLayer { + self.placeholderMaskLayer = nil + } + }*/ } } private final class ContentScrollView: UIScrollView, PagerExpandableScrollView { } - private let scrollView: ContentScrollView + private let shimmerHostView: PortalSourceView + private let standaloneShimmerEffect: StandaloneShimmerEffect + private let scrollView: ContentScrollView + private let boundsChangeTrackerLayer = SimpleLayer() + private var effectiveVisibleSize: CGSize = CGSize() + + private var visibleItemPlaceholderViews: [ItemLayer.Key: ItemPlaceholderView] = [:] private var visibleItemLayers: [ItemLayer.Key: ItemLayer] = [:] private var visibleGroupHeaders: [AnyHashable: ComponentView] = [:] private var ignoreScrolling: Bool = false + private var keepTopPanelVisibleUntilScrollingInput: Bool = false private var component: EmojiPagerContentComponent? private var pagerEnvironment: PagerComponentChildEnvironment? @@ -532,10 +643,24 @@ public final class EmojiPagerContentComponent: Component { private weak var peekController: PeekController? override init(frame: CGRect) { + self.shimmerHostView = PortalSourceView() + + self.standaloneShimmerEffect = StandaloneShimmerEffect() + self.scrollView = ContentScrollView() + self.scrollView.layer.anchorPoint = CGPoint() super.init(frame: frame) + self.shimmerHostView.alpha = 0.0 + self.addSubview(self.shimmerHostView) + + self.boundsChangeTrackerLayer.opacity = 0.0 + self.layer.addSublayer(self.boundsChangeTrackerLayer) + self.boundsChangeTrackerLayer.didEnterHierarchy = { [weak self] in + self?.standaloneShimmerEffect.updateLayer() + } + self.scrollView.delaysContentTouches = false if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { self.scrollView.contentInsetAdjustmentBehavior = .never @@ -546,8 +671,11 @@ public final class EmojiPagerContentComponent: Component { self.scrollView.showsVerticalScrollIndicator = true self.scrollView.showsHorizontalScrollIndicator = false self.scrollView.delegate = self + self.scrollView.clipsToBounds = false self.addSubview(self.scrollView) + //self.clipsToBounds = true + self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) /*self.useSublayerTransformForActivation = false @@ -747,7 +875,9 @@ public final class EmojiPagerContentComponent: Component { self.scrollView.setContentOffset(self.scrollView.contentOffset, animated: false) self.ignoreScrolling = wasIgnoringScrollingEvents - self.scrollView.scrollRectToVisible(CGRect(origin: group.frame.origin.offsetBy(dx: 0.0, dy: floor(-itemLayout.verticalGroupSpacing / 2.0)), size: CGSize(width: 1.0, height: self.scrollView.bounds.height)), animated: true) + self.keepTopPanelVisibleUntilScrollingInput = true + + self.scrollView.scrollRectToVisible(CGRect(origin: group.frame.origin.offsetBy(dx: 0.0, dy: floor(-itemLayout.verticalGroupSpacing / 2.0) - 41.0), size: CGSize(width: 1.0, height: self.scrollView.bounds.height)), animated: true) } } } @@ -780,6 +910,11 @@ public final class EmojiPagerContentComponent: Component { private var previousScrollingOffset: ScrollingOffsetState? public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + if self.keepTopPanelVisibleUntilScrollingInput { + self.keepTopPanelVisibleUntilScrollingInput = false + + self.updateScrollingOffset(isReset: true, transition: .immediate) + } if let presentation = scrollView.layer.presentation() { scrollView.bounds = presentation.bounds scrollView.layer.removeAllAnimations() @@ -793,7 +928,7 @@ public final class EmojiPagerContentComponent: Component { self.updateVisibleItems(attemptSynchronousLoads: false) - self.updateScrollingOffset(transition: .immediate) + self.updateScrollingOffset(isReset: false, transition: .immediate) } public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { @@ -812,21 +947,28 @@ public final class EmojiPagerContentComponent: Component { self.snapScrollingOffsetToInsets() } - private func updateScrollingOffset(transition: Transition) { + private func updateScrollingOffset(isReset: Bool, transition: Transition) { + guard let component = self.component else { + return + } + let isInteracting = scrollView.isDragging || scrollView.isDecelerating - if let previousScrollingOffsetValue = self.previousScrollingOffset { + if let previousScrollingOffsetValue = self.previousScrollingOffset, !self.keepTopPanelVisibleUntilScrollingInput { let currentBounds = scrollView.bounds let offsetToTopEdge = max(0.0, currentBounds.minY - 0.0) let offsetToBottomEdge = max(0.0, scrollView.contentSize.height - currentBounds.maxY) let relativeOffset = scrollView.contentOffset.y - previousScrollingOffsetValue.value - self.pagerEnvironment?.onChildScrollingUpdate(PagerComponentChildEnvironment.ContentScrollingUpdate( - relativeOffset: relativeOffset, - absoluteOffsetToTopEdge: offsetToTopEdge, - absoluteOffsetToBottomEdge: offsetToBottomEdge, - isInteracting: isInteracting, - transition: transition - )) + if case .detailed = component.itemLayoutType { + self.pagerEnvironment?.onChildScrollingUpdate(PagerComponentChildEnvironment.ContentScrollingUpdate( + relativeOffset: relativeOffset, + absoluteOffsetToTopEdge: offsetToTopEdge, + absoluteOffsetToBottomEdge: offsetToBottomEdge, + isReset: isReset, + isInteracting: isInteracting, + transition: transition + )) + } } self.previousScrollingOffset = ScrollingOffsetState(value: scrollView.contentOffset.y, isDraggingOrDecelerating: isInteracting) } @@ -855,7 +997,7 @@ public final class EmojiPagerContentComponent: Component { currentBounds.origin.y = self.snappedContentOffset(proposedOffset: currentBounds.minY) transition.setBounds(view: self.scrollView, bounds: currentBounds) - self.updateScrollingOffset(transition: transition) + self.updateScrollingOffset(isReset: false, transition: transition) } private func updateVisibleItems(attemptSynchronousLoads: Bool) { @@ -868,14 +1010,17 @@ public final class EmojiPagerContentComponent: Component { var validIds = Set() var validGroupHeaderIds = Set() - for groupItems in itemLayout.visibleItems(for: self.scrollView.bounds) { - if topVisibleGroupId == nil { - topVisibleGroupId = groupItems.id - } - + let effectiveVisibleBounds = CGRect(origin: self.scrollView.bounds.origin, size: self.effectiveVisibleSize) + let topVisibleDetectionBounds = effectiveVisibleBounds.offsetBy(dx: 0.0, dy: 41.0) + + for groupItems in itemLayout.visibleItems(for: effectiveVisibleBounds) { let itemGroup = component.itemGroups[groupItems.groupIndex] let itemGroupLayout = itemLayout.itemGroupLayouts[groupItems.groupIndex] + if topVisibleGroupId == nil && itemGroupLayout.frame.intersects(topVisibleDetectionBounds) { + topVisibleGroupId = groupItems.id + } + if let title = itemGroup.title { validGroupHeaderIds.insert(itemGroup.id) let groupHeaderView: ComponentView @@ -888,7 +1033,7 @@ public final class EmojiPagerContentComponent: Component { let groupHeaderSize = groupHeaderView.update( transition: .immediate, component: AnyComponent(Text( - text: title, font: Font.medium(12.0), color: theme.chat.inputMediaPanel.stickersSectionTextColor + text: title.uppercased(), font: Font.medium(12.0), color: theme.chat.inputMediaPanel.stickersSectionTextColor )), environment: {}, containerSize: CGSize(width: itemLayout.contentSize.width - itemLayout.containerInsets.left - itemLayout.containerInsets.right, height: 100.0) @@ -906,14 +1051,21 @@ public final class EmojiPagerContentComponent: Component { let itemId = ItemLayer.Key(groupId: itemGroup.id, fileId: item.file.fileId) validIds.insert(itemId) + let itemDimensions = item.file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0) + let itemNativeFitSize = itemDimensions.fitted(CGSize(width: itemLayout.nativeItemSize, height: itemLayout.nativeItemSize)) + let itemVisibleFitSize = itemDimensions.fitted(CGSize(width: itemLayout.visibleItemSize, height: itemLayout.visibleItemSize)) + + var updateItemLayerPlaceholder = false let itemLayer: ItemLayer if let current = self.visibleItemLayers[itemId] { itemLayer = current } else { + updateItemLayerPlaceholder = true + itemLayer = ItemLayer( item: item, context: component.context, - groupId: "keyboard-\(Int(itemLayout.itemSize))", + groupId: "keyboard-\(Int(itemLayout.nativeItemSize))", attemptSynchronousLoad: attemptSynchronousLoads, file: item.file, cache: component.animationCache, @@ -921,15 +1073,72 @@ public final class EmojiPagerContentComponent: Component { placeholderColor: theme.chat.inputPanel.primaryTextColor.withMultipliedAlpha(0.1), blurredBadgeColor: theme.chat.inputPanel.panelBackgroundColor.withMultipliedAlpha(0.5), displayPremiumBadgeIfAvailable: true, - pointSize: CGSize(width: itemLayout.itemSize, height: itemLayout.itemSize) + pointSize: itemNativeFitSize, + onUpdateDisplayPlaceholder: { [weak self] displayPlaceholder in + guard let strongSelf = self else { + return + } + if displayPlaceholder { + if let itemLayer = strongSelf.visibleItemLayers[itemId] { + let placeholderView: ItemPlaceholderView + if let current = strongSelf.visibleItemPlaceholderViews[itemId] { + placeholderView = current + } else { + placeholderView = ItemPlaceholderView( + context: component.context, + file: item.file, + shimmerView: strongSelf.shimmerHostView, + color: nil, + size: itemNativeFitSize + ) + strongSelf.visibleItemPlaceholderViews[itemId] = placeholderView + strongSelf.scrollView.insertSubview(placeholderView, at: 0) + } + placeholderView.frame = itemLayer.frame + placeholderView.update(size: placeholderView.bounds.size) + + strongSelf.updateShimmerIfNeeded() + } + } else { + if let placeholderView = strongSelf.visibleItemPlaceholderViews[itemId] { + strongSelf.visibleItemPlaceholderViews.removeValue(forKey: itemId) + placeholderView.removeFromSuperview() + + strongSelf.updateShimmerIfNeeded() + } + } + } ) self.scrollView.layer.addSublayer(itemLayer) self.visibleItemLayers[itemId] = itemLayer } - let itemFrame = itemLayout.frame(groupIndex: groupItems.groupIndex, itemIndex: index) - itemLayer.position = CGPoint(x: itemFrame.midX, y: itemFrame.midY) - itemLayer.bounds = CGRect(origin: CGPoint(), size: itemFrame.size) + var itemFrame = itemLayout.frame(groupIndex: groupItems.groupIndex, itemIndex: index) + + itemFrame.origin.x += floor((itemFrame.width - itemVisibleFitSize.width) / 2.0) + itemFrame.origin.y += floor((itemFrame.height - itemVisibleFitSize.height) / 2.0) + itemFrame.size = itemVisibleFitSize + + let itemPosition = CGPoint(x: itemFrame.midX, y: itemFrame.midY) + let itemBounds = CGRect(origin: CGPoint(), size: itemFrame.size) + if itemLayer.position != itemPosition { + itemLayer.position = itemPosition + } + if itemLayer.bounds != itemBounds { + itemLayer.bounds = CGRect(origin: CGPoint(), size: itemFrame.size) + } + + if let placeholderView = self.visibleItemPlaceholderViews[itemId] { + if placeholderView.layer.position != itemPosition || placeholderView.layer.bounds != itemBounds { + placeholderView.frame = itemFrame + placeholderView.update(size: itemFrame.size) + } + } else if updateItemLayerPlaceholder { + if itemLayer.displayPlaceholder { + itemLayer.onUpdateDisplayPlaceholder(true) + } + } + itemLayer.isVisibleForAnimations = true } } @@ -943,6 +1152,10 @@ public final class EmojiPagerContentComponent: Component { } for id in removedIds { self.visibleItemLayers.removeValue(forKey: id) + + if let view = self.visibleItemPlaceholderViews.removeValue(forKey: id) { + view.removeFromSuperview() + } } var removedGroupHeaderIds: [AnyHashable] = [] @@ -961,14 +1174,31 @@ public final class EmojiPagerContentComponent: Component { } } + private func updateShimmerIfNeeded() { + if self.visibleItemPlaceholderViews.isEmpty { + self.standaloneShimmerEffect.layer = nil + } else { + self.standaloneShimmerEffect.layer = self.shimmerHostView.layer + } + } + func update(component: EmojiPagerContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { self.component = component - self.theme = environment[EntityKeyboardChildEnvironment.self].value.theme - self.activeItemUpdated = environment[EntityKeyboardChildEnvironment.self].value.getContentActiveItemUpdated(component.id) + + let keyboardChildEnvironment = environment[EntityKeyboardChildEnvironment.self].value + + self.theme = keyboardChildEnvironment.theme + self.activeItemUpdated = keyboardChildEnvironment.getContentActiveItemUpdated(component.id) let pagerEnvironment = environment[PagerComponentChildEnvironment.self].value self.pagerEnvironment = pagerEnvironment + transition.setFrame(view: self.shimmerHostView, frame: CGRect(origin: CGPoint(), size: availableSize)) + + let shimmerBackgroundColor = keyboardChildEnvironment.theme.chat.inputPanel.primaryTextColor.withMultipliedAlpha(0.08) + let shimmerForegroundColor = keyboardChildEnvironment.theme.list.itemBlocksBackgroundColor.withMultipliedAlpha(0.15) + self.standaloneShimmerEffect.update(background: shimmerBackgroundColor, foreground: shimmerForegroundColor) + var itemGroups: [ItemGroupDescription] = [] for itemGroup in component.itemGroups { itemGroups.append(ItemGroupDescription( @@ -982,7 +1212,28 @@ public final class EmojiPagerContentComponent: Component { self.itemLayout = itemLayout self.ignoreScrolling = true - transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: availableSize)) + transition.setPosition(view: self.scrollView, position: CGPoint()) + let previousSize = self.scrollView.bounds.size + self.scrollView.bounds = CGRect(origin: self.scrollView.bounds.origin, size: availableSize) + + if availableSize.height > previousSize.height || transition.animation.isImmediate { + self.boundsChangeTrackerLayer.removeAllAnimations() + self.boundsChangeTrackerLayer.bounds = self.scrollView.bounds + self.effectiveVisibleSize = self.scrollView.bounds.size + } else { + self.effectiveVisibleSize = CGSize(width: availableSize.width, height: max(self.effectiveVisibleSize.height, availableSize.height)) + transition.setBounds(layer: self.boundsChangeTrackerLayer, bounds: self.scrollView.bounds, completion: { [weak self] completed in + guard let strongSelf = self else { + return + } + let effectiveVisibleSize = strongSelf.scrollView.bounds.size + if strongSelf.effectiveVisibleSize != effectiveVisibleSize { + strongSelf.effectiveVisibleSize = effectiveVisibleSize + strongSelf.updateVisibleItems(attemptSynchronousLoads: false) + } + }) + } + if self.scrollView.contentSize != itemLayout.contentSize { self.scrollView.contentSize = itemLayout.contentSize } @@ -992,7 +1243,7 @@ public final class EmojiPagerContentComponent: Component { self.previousScrollingOffset = ScrollingOffsetState(value: scrollView.contentOffset.y, isDraggingOrDecelerating: scrollView.isDragging || scrollView.isDecelerating) self.ignoreScrolling = false - self.updateVisibleItems(attemptSynchronousLoads: true) + self.updateVisibleItems(attemptSynchronousLoads: !(scrollView.isDragging || scrollView.isDecelerating)) return availableSize } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift index 0e458561ea..12cba3bc97 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift @@ -47,11 +47,14 @@ public final class EntityKeyboardComponent: Component { public let stickerContent: EmojiPagerContentComponent public let gifContent: GifPagerContentComponent public let defaultToEmojiTab: Bool - public let externalTopPanelContainer: UIView? + public let externalTopPanelContainer: PagerExternalTopPanelContainer? public let topPanelExtensionUpdated: (CGFloat, Transition) -> Void - public let hideInputUpdated: (Bool, Transition) -> Void + public let hideInputUpdated: (Bool, Bool, Transition) -> Void + public let switchToTextInput: () -> Void public let makeSearchContainerNode: (EntitySearchContentType) -> EntitySearchContainerNode public let deviceMetrics: DeviceMetrics + public let hiddenInputHeight: CGFloat + public let isExpanded: Bool public init( theme: PresentationTheme, @@ -60,11 +63,14 @@ public final class EntityKeyboardComponent: Component { stickerContent: EmojiPagerContentComponent, gifContent: GifPagerContentComponent, defaultToEmojiTab: Bool, - externalTopPanelContainer: UIView?, + externalTopPanelContainer: PagerExternalTopPanelContainer?, topPanelExtensionUpdated: @escaping (CGFloat, Transition) -> Void, - hideInputUpdated: @escaping (Bool, Transition) -> Void, + hideInputUpdated: @escaping (Bool, Bool, Transition) -> Void, + switchToTextInput: @escaping () -> Void, makeSearchContainerNode: @escaping (EntitySearchContentType) -> EntitySearchContainerNode, - deviceMetrics: DeviceMetrics + deviceMetrics: DeviceMetrics, + hiddenInputHeight: CGFloat, + isExpanded: Bool ) { self.theme = theme self.bottomInset = bottomInset @@ -75,8 +81,11 @@ public final class EntityKeyboardComponent: Component { self.externalTopPanelContainer = externalTopPanelContainer self.topPanelExtensionUpdated = topPanelExtensionUpdated self.hideInputUpdated = hideInputUpdated + self.switchToTextInput = switchToTextInput self.makeSearchContainerNode = makeSearchContainerNode self.deviceMetrics = deviceMetrics + self.hiddenInputHeight = hiddenInputHeight + self.isExpanded = isExpanded } public static func ==(lhs: EntityKeyboardComponent, rhs: EntityKeyboardComponent) -> Bool { @@ -104,6 +113,12 @@ public final class EntityKeyboardComponent: Component { if lhs.deviceMetrics != rhs.deviceMetrics { return false } + if lhs.hiddenInputHeight != rhs.hiddenInputHeight { + return false + } + if lhs.isExpanded != rhs.isExpanded { + return false + } return true } @@ -118,13 +133,14 @@ public final class EntityKeyboardComponent: Component { private var searchComponent: EntitySearchContentComponent? private var topPanelExtension: CGFloat? + private var isTopPanelExpanded: Bool = false override init(frame: CGRect) { self.pagerView = ComponentHostView() super.init(frame: frame) - self.clipsToBounds = true + //self.clipsToBounds = true self.disablesInteractiveTransitionGestureRecognizer = true self.addSubview(self.pagerView) @@ -146,7 +162,8 @@ public final class EntityKeyboardComponent: Component { let gifsContentItemIdUpdated = ActionSlot<(AnyHashable, Transition)>() contents.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(component.gifContent))) var topGifItems: [EntityKeyboardTopPanelComponent.Item] = [] - topGifItems.append(EntityKeyboardTopPanelComponent.Item( + topGifItems.removeAll() + /*topGifItems.append(EntityKeyboardTopPanelComponent.Item( id: "recent", content: AnyComponent(BundleIconComponent( name: "Chat/Input/Media/RecentTabIcon", @@ -161,7 +178,7 @@ public final class EntityKeyboardComponent: Component { tintColor: component.theme.chat.inputMediaPanel.panelIconColor, maxSize: CGSize(width: 30.0, height: 30.0)) ) - )) + ))*/ contentTopPanels.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(EntityKeyboardTopPanelComponent( theme: component.theme, items: topGifItems, @@ -199,20 +216,21 @@ public final class EntityKeyboardComponent: Component { "recent": "Chat/Input/Media/RecentTabIcon", "premium": "Chat/Input/Media/PremiumIcon" ] - if let iconName = iconMapping[id] { + let titleMapping: [String: String] = [ + "recent": "Recent", + "premium": "Premium" + ] + if let iconName = iconMapping[id], let title = titleMapping[id] { topStickerItems.append(EntityKeyboardTopPanelComponent.Item( id: itemGroup.id, - content: AnyComponent(Button( - content: AnyComponent(BundleIconComponent( - name: iconName, - tintColor: component.theme.chat.inputMediaPanel.panelIconColor, - maxSize: CGSize(width: 30.0, height: 30.0) - )), - action: { [weak self] in + content: AnyComponent(EntityKeyboardIconTopPanelComponent( + imageName: iconName, + theme: component.theme, + title: title, + pressed: { [weak self] in self?.scrollToItemGroup(contentId: "stickers", groupId: itemGroup.id) } - ).minSize(CGSize(width: 30.0, height: 30.0)) - ) + )) )) } } else { @@ -224,6 +242,8 @@ public final class EntityKeyboardComponent: Component { file: itemGroup.items[0].file, animationCache: component.stickerContent.animationCache, animationRenderer: component.stickerContent.animationRenderer, + theme: component.theme, + title: itemGroup.title ?? "", pressed: { [weak self] in self?.scrollToItemGroup(contentId: "stickers", groupId: itemGroup.id) } @@ -277,6 +297,8 @@ public final class EntityKeyboardComponent: Component { file: itemGroup.items[0].file, animationCache: component.emojiContent.animationCache, animationRenderer: component.emojiContent.animationRenderer, + theme: component.theme, + title: itemGroup.title ?? "", pressed: { [weak self] in self?.scrollToItemGroup(contentId: "emoji", groupId: itemGroup.id) } @@ -294,6 +316,20 @@ public final class EntityKeyboardComponent: Component { tintColor: component.theme.chat.inputMediaPanel.panelIconColor, maxSize: nil )))) + contentAccessoryLeftButtons.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(Button( + content: AnyComponent(BundleIconComponent( + name: "Chat/Input/Media/EntityInputGlobeIcon", + tintColor: component.theme.chat.inputMediaPanel.panelIconColor, + maxSize: nil + )), + action: { [weak self] in + guard let strongSelf = self, let component = strongSelf.component else { + return + } + component.switchToTextInput() + } + ).minSize(CGSize(width: 38.0, height: 38.0))))) + let deleteBackwards = component.emojiContent.inputInteraction.deleteBackwards contentAccessoryRightButtons.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(Button( content: AnyComponent(BundleIconComponent( name: "Chat/Input/Media/EntityInputClearIcon", @@ -301,9 +337,20 @@ public final class EntityKeyboardComponent: Component { maxSize: nil )), action: { - component.emojiContent.inputInteraction.deleteBackwards() + deleteBackwards() } - ).minSize(CGSize(width: 38.0, height: 38.0))))) + ).withHoldAction({ + deleteBackwards() + }).minSize(CGSize(width: 38.0, height: 38.0))))) + + let panelHideBehavior: PagerComponentPanelHideBehavior + if self.searchComponent != nil { + panelHideBehavior = .hide + } else if component.isExpanded { + panelHideBehavior = .show + } else { + panelHideBehavior = .hideOnScroll + } let pagerSize = self.pagerView.update( transition: transition, @@ -319,7 +366,8 @@ public final class EntityKeyboardComponent: Component { color: component.theme.chat.inputMediaPanel.stickersBackgroundColor.withMultipliedAlpha(0.75) )), topPanel: AnyComponent(EntityKeyboardTopContainerPanelComponent( - theme: component.theme + theme: component.theme, + overflowHeight: component.hiddenInputHeight )), externalTopPanelContainer: component.externalTopPanelContainer, bottomPanel: AnyComponent(EntityKeyboardBottomPanelComponent( @@ -335,7 +383,13 @@ public final class EntityKeyboardComponent: Component { } strongSelf.topPanelExtensionUpdated(height: panelState.topPanelHeight, transition: transition) }, - hidePanels: self.searchComponent != nil + isTopPanelExpandedUpdated: { [weak self] isExpanded, transition in + guard let strongSelf = self else { + return + } + strongSelf.isTopPanelExpandedUpdated(isExpanded: isExpanded, transition: transition) + }, + panelHideBehavior: panelHideBehavior )), environment: { EntityKeyboardChildEnvironment( @@ -426,6 +480,18 @@ public final class EntityKeyboardComponent: Component { } } + private func isTopPanelExpandedUpdated(isExpanded: Bool, transition: Transition) { + if self.isTopPanelExpanded != isExpanded { + self.isTopPanelExpanded = isExpanded + } + + guard let component = self.component else { + return + } + + component.hideInputUpdated(self.isTopPanelExpanded, false, transition) + } + private func openSearch() { guard let component = self.component else { return @@ -451,7 +517,7 @@ public final class EntityKeyboardComponent: Component { } ) //self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) - component.hideInputUpdated(true, Transition(animation: .curve(duration: 0.3, curve: .spring))) + component.hideInputUpdated(true, true, Transition(animation: .curve(duration: 0.3, curve: .spring))) } } @@ -464,7 +530,7 @@ public final class EntityKeyboardComponent: Component { } self.searchComponent = nil //self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) - component.hideInputUpdated(false, Transition(animation: .curve(duration: 0.4, curve: .spring))) + component.hideInputUpdated(false, false, Transition(animation: .curve(duration: 0.4, curve: .spring))) } private func scrollToItemGroup(contentId: String, groupId: AnyHashable) { diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopContainerPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopContainerPanelComponent.swift index e4a6fe549f..60d71d5d6d 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopContainerPanelComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopContainerPanelComponent.swift @@ -9,11 +9,14 @@ import Postbox final class EntityKeyboardTopContainerPanelEnvironment: Equatable { let visibilityFractionUpdated: ActionSlot<(CGFloat, Transition)> + let isExpandedUpdated: (Bool, Transition) -> Void init( - visibilityFractionUpdated: ActionSlot<(CGFloat, Transition)> + visibilityFractionUpdated: ActionSlot<(CGFloat, Transition)>, + isExpandedUpdated: @escaping (Bool, Transition) -> Void ) { self.visibilityFractionUpdated = visibilityFractionUpdated + self.isExpandedUpdated = isExpandedUpdated } static func ==(lhs: EntityKeyboardTopContainerPanelEnvironment, rhs: EntityKeyboardTopContainerPanelEnvironment) -> Bool { @@ -28,17 +31,23 @@ final class EntityKeyboardTopContainerPanelComponent: Component { typealias EnvironmentType = PagerComponentPanelEnvironment let theme: PresentationTheme + let overflowHeight: CGFloat init( - theme: PresentationTheme + theme: PresentationTheme, + overflowHeight: CGFloat ) { self.theme = theme + self.overflowHeight = overflowHeight } static func ==(lhs: EntityKeyboardTopContainerPanelComponent, rhs: EntityKeyboardTopContainerPanelComponent) -> Bool { if lhs.theme !== rhs.theme { return false } + if lhs.overflowHeight != rhs.overflowHeight { + return false + } return true } @@ -46,6 +55,7 @@ final class EntityKeyboardTopContainerPanelComponent: Component { private final class PanelView { let view = ComponentHostView() let visibilityFractionUpdated = ActionSlot<(CGFloat, Transition)>() + var isExpanded: Bool = false } final class View: UIView { @@ -57,8 +67,13 @@ final class EntityKeyboardTopContainerPanelComponent: Component { private var visibilityFraction: CGFloat = 1.0 + private var hasExpandedPanels: Bool = false + override init(frame: CGRect) { super.init(frame: frame) + + self.disablesInteractiveKeyboardGestureRecognizer = true + self.disablesInteractiveTransitionGestureRecognizer = true } required init?(coder: NSCoder) { @@ -94,7 +109,7 @@ final class EntityKeyboardTopContainerPanelComponent: Component { let panel = panelEnvironment.contentTopPanels[index] let indexOffset = index - centralIndex - let panelFrame = CGRect(origin: CGPoint(x: CGFloat(indexOffset) * availableSize.width, y: 0.0), size: CGSize(width: availableSize.width, height: intrinsicHeight)) + let panelFrame = CGRect(origin: CGPoint(x: CGFloat(indexOffset) * availableSize.width, y: -component.overflowHeight), size: CGSize(width: availableSize.width, height: intrinsicHeight + component.overflowHeight)) let isInBounds = visibleBounds.intersects(panelFrame) let isPartOfTransition: Bool @@ -118,12 +133,19 @@ final class EntityKeyboardTopContainerPanelComponent: Component { self.addSubview(panelView.view) } + let panelId = panel.id let _ = panelView.view.update( transition: panelTransition, component: panel.component, environment: { EntityKeyboardTopContainerPanelEnvironment( - visibilityFractionUpdated: panelView.visibilityFractionUpdated + visibilityFractionUpdated: panelView.visibilityFractionUpdated, + isExpandedUpdated: { [weak self] isExpanded, transition in + guard let strongSelf = self else { + return + } + strongSelf.panelIsExpandedUpdated(id: panelId, isExpanded: isExpanded, transition: transition) + } ) }, containerSize: panelFrame.size @@ -171,6 +193,45 @@ final class EntityKeyboardTopContainerPanelComponent: Component { transition.setSublayerTransform(view: panelView.view, transform: CATransform3DMakeTranslation(0.0, -panelView.view.bounds.height / 2.0 * (1.0 - value), 0.0)) } } + + private func panelIsExpandedUpdated(id: AnyHashable, isExpanded: Bool, transition: Transition) { + guard let panelView = self.panelViews[id] else { + return + } + panelView.isExpanded = isExpanded + + var hasExpanded = false + for (_, panel) in self.panelViews { + if panel.isExpanded { + hasExpanded = true + break + } + } + + if self.hasExpandedPanels != hasExpanded { + self.hasExpandedPanels = hasExpanded + + self.panelEnvironment?.isExpandedUpdated(self.hasExpandedPanels, transition) + } + } + + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.alpha.isZero { + return nil + } + for view in self.subviews.reversed() { + if let result = view.hitTest(self.convert(point, to: view), with: event), result.isUserInteractionEnabled { + return result + } + } + + let result = super.hitTest(point, with: event) + if result != self { + return result + } else { + return nil + } + } } func makeView() -> View { diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift index 1dbf32cdb8..daf3de90ef 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift @@ -1,4 +1,5 @@ import Foundation +import SwiftSignalKit import UIKit import Display import ComponentFlow @@ -9,14 +10,17 @@ import Postbox import AnimationCache import MultiAnimationRenderer import AccountContext +import MultilineTextComponent final class EntityKeyboardAnimationTopPanelComponent: Component { - typealias EnvironmentType = Empty + typealias EnvironmentType = EntityKeyboardTopPanelItemEnvironment let context: AccountContext let file: TelegramMediaFile let animationCache: AnimationCache let animationRenderer: MultiAnimationRenderer + let theme: PresentationTheme + let title: String let pressed: () -> Void init( @@ -24,12 +28,16 @@ final class EntityKeyboardAnimationTopPanelComponent: Component { file: TelegramMediaFile, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, + theme: PresentationTheme, + title: String, pressed: @escaping () -> Void ) { self.context = context self.file = file self.animationCache = animationCache self.animationRenderer = animationRenderer + self.theme = theme + self.title = title self.pressed = pressed } @@ -46,13 +54,21 @@ final class EntityKeyboardAnimationTopPanelComponent: Component { if lhs.animationRenderer !== rhs.animationRenderer { return false } + if lhs.theme !== rhs.theme { + return false + } + if lhs.title != rhs.title { + return false + } return true } final class View: UIView { var itemLayer: EmojiPagerContentComponent.View.ItemLayer? + var placeholderView: EmojiPagerContentComponent.View.ItemPlaceholderView? var component: EntityKeyboardAnimationTopPanelComponent? + var titleView: ComponentView? override init(frame: CGRect) { super.init(frame: frame) @@ -73,6 +89,8 @@ final class EntityKeyboardAnimationTopPanelComponent: Component { func update(component: EntityKeyboardAnimationTopPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { self.component = component + let itemEnvironment = environment[EntityKeyboardTopPanelItemEnvironment.self].value + if self.itemLayer == nil { let itemLayer = EmojiPagerContentComponent.View.ItemLayer( item: EmojiPagerContentComponent.Item( @@ -89,15 +107,88 @@ final class EntityKeyboardAnimationTopPanelComponent: Component { placeholderColor: .lightGray, blurredBadgeColor: .clear, displayPremiumBadgeIfAvailable: false, - pointSize: CGSize(width: 28.0, height: 28.0) + pointSize: CGSize(width: 44.0, height: 44.0), + onUpdateDisplayPlaceholder: { [weak self] displayPlaceholder in + guard let strongSelf = self else { + return + } + strongSelf.updateDisplayPlaceholder(displayPlaceholder: displayPlaceholder) + } ) self.itemLayer = itemLayer self.layer.addSublayer(itemLayer) - itemLayer.frame = CGRect(origin: CGPoint(), size: CGSize(width: 28.0, height: 28.0)) + + if itemLayer.displayPlaceholder { + self.updateDisplayPlaceholder(displayPlaceholder: true) + } + } + + let iconSize: CGSize = itemEnvironment.isExpanded ? CGSize(width: 44.0, height: 44.0) : CGSize(width: 28.0, height: 28.0) + let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) / 2.0), y: 0.0), size: iconSize) + + if let itemLayer = self.itemLayer { + transition.setPosition(layer: itemLayer, position: CGPoint(x: iconFrame.midX, y: iconFrame.midY)) + transition.setBounds(layer: itemLayer, bounds: CGRect(origin: CGPoint(), size: iconFrame.size)) itemLayer.isVisibleForAnimations = true } - return CGSize(width: 28.0, height: 28.0) + if itemEnvironment.isExpanded { + let titleView: ComponentView + if let current = self.titleView { + titleView = current + } else { + titleView = ComponentView() + self.titleView = titleView + } + let titleSize = titleView.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.title, font: Font.regular(10.0), textColor: component.theme.chat.inputPanel.primaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: 62.0, height: 100.0) + ) + if let view = titleView.view { + if view.superview == nil { + view.alpha = 0.0 + self.addSubview(view) + } + view.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) / 2.0), y: availableSize.height - titleSize.height), size: titleSize) + transition.setAlpha(view: view, alpha: 1.0) + } + } else if let titleView = self.titleView { + self.titleView = nil + if let view = titleView.view { + transition.setAlpha(view: view, alpha: 0.0, completion: { [weak view] _ in + view?.removeFromSuperview() + }) + } + } + + return availableSize + } + + private func updateDisplayPlaceholder(displayPlaceholder: Bool) { + if displayPlaceholder { + if self.placeholderView == nil, let component = self.component { + let placeholderView = EmojiPagerContentComponent.View.ItemPlaceholderView( + context: component.context, + file: component.file, + shimmerView: nil, + color: component.theme.chat.inputPanel.primaryTextColor.withMultipliedAlpha(0.08), + size: CGSize(width: 28.0, height: 28.0) + ) + self.placeholderView = placeholderView + self.insertSubview(placeholderView, at: 0) + placeholderView.frame = CGRect(origin: CGPoint(), size: CGSize(width: 28.0, height: 28.0)) + placeholderView.update(size: CGSize(width: 28.0, height: 28.0)) + } + } else { + if let placeholderView = self.placeholderView { + self.placeholderView = nil + placeholderView.removeFromSuperview() + } + } } } @@ -110,14 +201,151 @@ final class EntityKeyboardAnimationTopPanelComponent: Component { } } +final class EntityKeyboardIconTopPanelComponent: Component { + typealias EnvironmentType = EntityKeyboardTopPanelItemEnvironment + + let imageName: String + let theme: PresentationTheme + let title: String + let pressed: () -> Void + + init( + imageName: String, + theme: PresentationTheme, + title: String, + pressed: @escaping () -> Void + ) { + self.imageName = imageName + self.theme = theme + self.title = title + self.pressed = pressed + } + + static func ==(lhs: EntityKeyboardIconTopPanelComponent, rhs: EntityKeyboardIconTopPanelComponent) -> Bool { + if lhs.imageName != rhs.imageName { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.title != rhs.title { + return false + } + + return true + } + + final class View: UIView { + let iconView: UIImageView + var component: EntityKeyboardIconTopPanelComponent? + var titleView: ComponentView? + + override init(frame: CGRect) { + self.iconView = UIImageView() + + super.init(frame: frame) + + self.addSubview(self.iconView) + + self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.component?.pressed() + } + } + + func update(component: EntityKeyboardIconTopPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let itemEnvironment = environment[EntityKeyboardTopPanelItemEnvironment.self].value + + if self.component?.imageName != component.imageName { + self.iconView.image = generateTintedImage(image: UIImage(bundleImageName: component.imageName), color: component.theme.chat.inputMediaPanel.panelIconColor) + } + + self.component = component + + let nativeIconSize: CGSize = itemEnvironment.isExpanded ? CGSize(width: 44.0, height: 44.0) : CGSize(width: 28.0, height: 28.0) + let boundingIconSize: CGSize = itemEnvironment.isExpanded ? CGSize(width: 38.0, height: 38.0) : CGSize(width: 24.0, height: 24.0) + + let iconSize = (self.iconView.image?.size ?? nativeIconSize).aspectFitted(boundingIconSize) + let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) / 2.0), y: floor((nativeIconSize.height - iconSize.height) / 2.0)), size: iconSize) + + transition.setFrame(view: self.iconView, frame: iconFrame) + + if itemEnvironment.isExpanded { + let titleView: ComponentView + if let current = self.titleView { + titleView = current + } else { + titleView = ComponentView() + self.titleView = titleView + } + let titleSize = titleView.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.title, font: Font.regular(10.0), textColor: component.theme.chat.inputPanel.primaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: 62.0, height: 100.0) + ) + if let view = titleView.view { + if view.superview == nil { + view.alpha = 0.0 + self.addSubview(view) + } + view.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) / 2.0), y: availableSize.height - titleSize.height), size: titleSize) + transition.setAlpha(view: view, alpha: 1.0) + } + } else if let titleView = self.titleView { + self.titleView = nil + if let view = titleView.view { + transition.setAlpha(view: view, alpha: 0.0, completion: { [weak view] _ in + view?.removeFromSuperview() + }) + } + } + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +final class EntityKeyboardTopPanelItemEnvironment: Equatable { + let isExpanded: Bool + + init(isExpanded: Bool) { + self.isExpanded = isExpanded + } + + static func ==(lhs: EntityKeyboardTopPanelItemEnvironment, rhs: EntityKeyboardTopPanelItemEnvironment) -> Bool { + if lhs.isExpanded != rhs.isExpanded { + return false + } + return true + } +} + final class EntityKeyboardTopPanelComponent: Component { typealias EnvironmentType = EntityKeyboardTopContainerPanelEnvironment final class Item: Equatable { let id: AnyHashable - let content: AnyComponent + let content: AnyComponent - init(id: AnyHashable, content: AnyComponent) { + init(id: AnyHashable, content: AnyComponent) { self.id = id self.content = content } @@ -165,34 +393,42 @@ final class EntityKeyboardTopPanelComponent: Component { final class View: UIView, UIScrollViewDelegate { private struct ItemLayout { let sideInset: CGFloat = 7.0 - let itemSize: CGFloat = 32.0 - let innerItemSize: CGFloat = 28.0 + let itemSize: CGSize + let innerItemSize: CGSize let itemSpacing: CGFloat = 15.0 let itemCount: Int let contentSize: CGSize + let isExpanded: Bool - init(itemCount: Int) { + init(itemCount: Int, isExpanded: Bool, height: CGFloat) { + self.isExpanded = isExpanded + self.itemSize = self.isExpanded ? CGSize(width: 54.0, height: 68.0) : CGSize(width: 32.0, height: 32.0) + self.innerItemSize = self.isExpanded ? CGSize(width: 50.0, height: 62.0) : CGSize(width: 28.0, height: 28.0) self.itemCount = itemCount - self.contentSize = CGSize(width: sideInset * 2.0 + CGFloat(itemCount) * self.itemSize + CGFloat(max(0, itemCount - 1)) * itemSpacing, height: 41.0) + self.contentSize = CGSize(width: sideInset * 2.0 + CGFloat(itemCount) * self.itemSize.width + CGFloat(max(0, itemCount - 1)) * itemSpacing, height: height) } func containerFrame(at index: Int) -> CGRect { - return CGRect(origin: CGPoint(x: sideInset + CGFloat(index) * (self.itemSize + self.itemSpacing), y: floor((self.contentSize.height - self.itemSize) / 2.0)), size: CGSize(width: self.itemSize, height: self.itemSize)) + return CGRect(origin: CGPoint(x: sideInset + CGFloat(index) * (self.itemSize.width + self.itemSpacing), y: floor((self.contentSize.height - self.itemSize.height) / 2.0)), size: self.itemSize) + } + + func contentFrame(containerFrame: CGRect) -> CGRect { + var frame = containerFrame + frame.origin.x += floor((self.itemSize.width - self.innerItemSize.width)) / 2.0 + frame.origin.y += floor((self.itemSize.height - self.innerItemSize.height)) / 2.0 + frame.size = self.innerItemSize + return frame } func contentFrame(at index: Int) -> CGRect { - var frame = self.containerFrame(at: index) - frame.origin.x += floor((self.itemSize - self.innerItemSize)) / 2.0 - frame.origin.y += floor((self.itemSize - self.innerItemSize)) / 2.0 - frame.size = CGSize(width: self.innerItemSize, height: self.innerItemSize) - return frame + return self.contentFrame(containerFrame: self.containerFrame(at: index)) } func visibleItemRange(for rect: CGRect) -> (minIndex: Int, maxIndex: Int) { let offsetRect = rect.offsetBy(dx: -self.sideInset, dy: 0.0) - var minVisibleColumn = Int(floor((offsetRect.minX - self.itemSpacing) / (self.itemSize + self.itemSpacing))) + var minVisibleColumn = Int(floor((offsetRect.minX - self.itemSpacing) / (self.itemSize.width + self.itemSpacing))) minVisibleColumn = max(0, minVisibleColumn) - let maxVisibleColumn = Int(ceil((offsetRect.maxX - self.itemSpacing) / (self.itemSize + self.itemSpacing))) + let maxVisibleColumn = Int(ceil((offsetRect.maxX - self.itemSpacing) / (self.itemSize.width + self.itemSpacing))) let minVisibleIndex = minVisibleColumn let maxVisibleIndex = min(maxVisibleColumn, self.itemCount - 1) @@ -202,15 +438,23 @@ final class EntityKeyboardTopPanelComponent: Component { } private let scrollView: UIScrollView - private var itemViews: [AnyHashable: ComponentHostView] = [:] + private var itemViews: [AnyHashable: ComponentHostView] = [:] private var highlightedIconBackgroundView: UIView private var itemLayout: ItemLayout? private var ignoreScrolling: Bool = false + private var isDragging: Bool = false + private var draggingStoppedTimer: SwiftSignalKit.Timer? + + private var isExpanded: Bool = false + private var visibilityFraction: CGFloat = 1.0 + private var activeContentItemId: AnyHashable? + private var component: EntityKeyboardTopPanelComponent? + private var environment: EntityKeyboardTopContainerPanelEnvironment? override init(frame: CGRect) { self.scrollView = UIScrollView() @@ -222,7 +466,9 @@ final class EntityKeyboardTopPanelComponent: Component { super.init(frame: frame) + self.scrollView.layer.anchorPoint = CGPoint() self.scrollView.delaysContentTouches = false + self.scrollView.clipsToBounds = false if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { self.scrollView.contentInsetAdjustmentBehavior = .never } @@ -236,6 +482,8 @@ final class EntityKeyboardTopPanelComponent: Component { self.scrollView.addSubview(self.highlightedIconBackgroundView) + self.clipsToBounds = true + self.disablesInteractiveTransitionGestureRecognizerNow = { [weak self] in guard let strongSelf = self else { return false @@ -253,38 +501,98 @@ final class EntityKeyboardTopPanelComponent: Component { return } - self.updateVisibleItems(attemptSynchronousLoads: false) + self.updateVisibleItems(attemptSynchronousLoads: false, transition: .immediate) } - private func updateVisibleItems(attemptSynchronousLoads: Bool) { + public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + self.updateIsDragging(true) + } + + public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + if !decelerate { + self.updateIsDragging(false) + } + } + + public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + self.updateIsDragging(false) + } + + private func updateIsDragging(_ isDragging: Bool) { + if !isDragging { + if !self.isDragging { + return + } + + if self.draggingStoppedTimer == nil { + self.draggingStoppedTimer = SwiftSignalKit.Timer(timeout: 0.8, repeat: false, completion: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.draggingStoppedTimer = nil + strongSelf.isDragging = false + guard let environment = strongSelf.environment else { + return + } + environment.isExpandedUpdated(false, Transition(animation: .curve(duration: 0.3, curve: .spring))) + }, queue: .mainQueue()) + self.draggingStoppedTimer?.start() + } + } else { + self.draggingStoppedTimer?.invalidate() + self.draggingStoppedTimer = nil + + if !self.isDragging { + self.isDragging = true + + guard let environment = self.environment else { + return + } + environment.isExpandedUpdated(true, Transition(animation: .curve(duration: 0.3, curve: .spring))) + } + } + } + + private func updateVisibleItems(attemptSynchronousLoads: Bool, transition: Transition) { guard let component = self.component, let itemLayout = self.itemLayout else { return } + var visibleBounds = self.scrollView.bounds + visibleBounds.size.width += 200.0 + var validIds = Set() - let visibleItemRange = itemLayout.visibleItemRange(for: self.scrollView.bounds) + let visibleItemRange = itemLayout.visibleItemRange(for: visibleBounds) if !component.items.isEmpty && visibleItemRange.maxIndex >= visibleItemRange.minIndex { for index in visibleItemRange.minIndex ... visibleItemRange.maxIndex { let item = component.items[index] validIds.insert(item.id) - let itemView: ComponentHostView + var itemTransition = transition + let itemView: ComponentHostView if let current = self.itemViews[item.id] { itemView = current } else { - itemView = ComponentHostView() + itemTransition = .immediate + itemView = ComponentHostView() self.scrollView.addSubview(itemView) self.itemViews[item.id] = itemView } let itemOuterFrame = itemLayout.contentFrame(at: index) let itemSize = itemView.update( - transition: .immediate, + transition: itemTransition, component: item.content, - environment: {}, + environment: { + EntityKeyboardTopPanelItemEnvironment(isExpanded: itemLayout.isExpanded) + }, containerSize: itemOuterFrame.size ) - itemView.frame = CGRect(origin: CGPoint(x: itemOuterFrame.minX + floor((itemOuterFrame.width - itemSize.width) / 2.0), y: itemOuterFrame.minY + floor((itemOuterFrame.height - itemSize.height) / 2.0)), size: itemSize) + let itemFrame = CGRect(origin: CGPoint(x: itemOuterFrame.minX + floor((itemOuterFrame.width - itemSize.width) / 2.0), y: itemOuterFrame.minY + floor((itemOuterFrame.height - itemSize.height) / 2.0)), size: itemSize) + /*if index == visibleItemRange.minIndex, !itemTransition.animation.isImmediate { + print("\(index): \(itemView.frame) -> \(itemFrame)") + }*/ + itemTransition.setFrame(view: itemView, frame: itemFrame) } } var removedIds: [AnyHashable] = [] @@ -305,22 +613,77 @@ final class EntityKeyboardTopPanelComponent: Component { } self.component = component - let intrinsicHeight: CGFloat = 41.0 + let panelEnvironment = environment[EntityKeyboardTopContainerPanelEnvironment.self].value + self.environment = panelEnvironment + + let isExpanded = availableSize.height > 41.0 + let wasExpanded = self.isExpanded + self.isExpanded = isExpanded + + let intrinsicHeight: CGFloat = availableSize.height let height = intrinsicHeight - let itemLayout = ItemLayout(itemCount: component.items.count) + let previousItemLayout = self.itemLayout + let itemLayout = ItemLayout(itemCount: component.items.count, isExpanded: isExpanded, height: availableSize.height) self.itemLayout = itemLayout self.ignoreScrolling = true - transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: intrinsicHeight))) + + var updatedBounds: CGRect? + if wasExpanded != isExpanded, let previousItemLayout = previousItemLayout { + var visibleBounds = self.scrollView.bounds + visibleBounds.size.width += 200.0 + + let previousVisibleRange = previousItemLayout.visibleItemRange(for: visibleBounds) + if previousVisibleRange.minIndex <= previousVisibleRange.maxIndex { + let previousItemFrame = previousItemLayout.containerFrame(at: previousVisibleRange.minIndex) + let updatedItemFrame = itemLayout.containerFrame(at: previousVisibleRange.minIndex) + + let previousDistanceToItemFraction = (previousItemFrame.minX - self.scrollView.bounds.minX) / previousItemFrame.width + let newBounds = CGRect(origin: CGPoint(x: updatedItemFrame.minX - floor(previousDistanceToItemFraction * updatedItemFrame.width), y: 0.0), size: availableSize) + updatedBounds = newBounds + + var updatedVisibleBounds = newBounds + updatedVisibleBounds.size.width += 200.0 + let updatedVisibleRange = itemLayout.visibleItemRange(for: updatedVisibleBounds) + + let baseFrame = CGRect(origin: CGPoint(x: updatedItemFrame.minX, y: previousItemFrame.minY), size: previousItemFrame.size) + for index in updatedVisibleRange.minIndex ..< updatedVisibleRange.maxIndex { + let indexDifference = index - previousVisibleRange.minIndex + if let itemView = self.itemViews[component.items[index].id] { + let itemContainerOriginX = baseFrame.minX + CGFloat(indexDifference) * (previousItemLayout.itemSize.width + previousItemLayout.itemSpacing) + let itemContainerFrame = CGRect(origin: CGPoint(x: itemContainerOriginX, y: baseFrame.minY), size: baseFrame.size) + let itemOuterFrame = previousItemLayout.contentFrame(containerFrame: itemContainerFrame) + + let itemSize = itemView.bounds.size + itemView.frame = CGRect(origin: CGPoint(x: itemOuterFrame.minX + floor((itemOuterFrame.width - itemSize.width) / 2.0), y: itemOuterFrame.minY + floor((itemOuterFrame.height - itemSize.height) / 2.0)), size: itemSize) + } + } + } + } + if self.scrollView.contentSize != itemLayout.contentSize { self.scrollView.contentSize = itemLayout.contentSize } + if let updatedBounds = updatedBounds { + self.scrollView.bounds = updatedBounds + } else { + self.scrollView.bounds = CGRect(origin: self.scrollView.bounds.origin, size: availableSize) + } self.ignoreScrolling = false - self.updateVisibleItems(attemptSynchronousLoads: true) + self.updateVisibleItems(attemptSynchronousLoads: !(self.scrollView.isDragging || self.scrollView.isDecelerating), transition: transition) - environment[EntityKeyboardTopContainerPanelEnvironment.self].value.visibilityFractionUpdated.connect { [weak self] (fraction, transition) in + if let activeContentItemId = self.activeContentItemId { + if let index = component.items.firstIndex(where: { $0.id == activeContentItemId }) { + let itemFrame = itemLayout.containerFrame(at: index) + transition.setPosition(view: self.highlightedIconBackgroundView, position: CGPoint(x: itemFrame.midX, y: itemFrame.midY)) + transition.setBounds(view: self.highlightedIconBackgroundView, bounds: CGRect(origin: CGPoint(), size: itemFrame.size)) + } + } + transition.setAlpha(view: self.highlightedIconBackgroundView, alpha: isExpanded ? 0.0 : 1.0) + + panelEnvironment.visibilityFractionUpdated.connect { [weak self] (fraction, transition) in guard let strongSelf = self else { return } @@ -359,6 +722,8 @@ final class EntityKeyboardTopPanelComponent: Component { guard let component = self.component, let itemLayout = self.itemLayout else { return } + + self.activeContentItemId = itemId var found = false for i in 0 ..< component.items.count { @@ -366,7 +731,10 @@ final class EntityKeyboardTopPanelComponent: Component { found = true self.highlightedIconBackgroundView.isHidden = false let itemFrame = itemLayout.containerFrame(at: i) - transition.setFrame(view: self.highlightedIconBackgroundView, frame: itemFrame) + transition.setPosition(view: self.highlightedIconBackgroundView, position: CGPoint(x: itemFrame.midX, y: itemFrame.midY)) + transition.setBounds(view: self.highlightedIconBackgroundView, bounds: CGRect(origin: CGPoint(), size: itemFrame.size)) + + self.scrollView.scrollRectToVisible(itemFrame.insetBy(dx: -6.0, dy: 0.0), animated: true) break } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift index 630000f39b..02936915b6 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift @@ -464,6 +464,7 @@ public final class GifPagerContentComponent: Component { relativeOffset: relativeOffset, absoluteOffsetToTopEdge: offsetToTopEdge, absoluteOffsetToBottomEdge: offsetToBottomEdge, + isReset: false, isInteracting: isInteracting, transition: transition )) diff --git a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift index 34a79ff6e8..3354285be9 100644 --- a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift +++ b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift @@ -3,6 +3,7 @@ import UIKit import SwiftSignalKit import Display import AnimationCache +import Accelerate public protocol MultiAnimationRenderer: AnyObject { func add(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable) -> Disposable @@ -36,6 +37,7 @@ open class MultiAnimationRenderTarget: SimpleLayer { private final class FrameGroup { let image: UIImage + let badgeImage: UIImage? let size: CGSize let timestamp: Double @@ -50,6 +52,27 @@ private final class FrameGroup { firstFrame.data.withUnsafeBytes { bytes -> Void in memcpy(context.bytes, bytes.baseAddress!.advanced(by: firstFrame.range.lowerBound), height * bytesPerRow) + + /*var sourceBuffer = vImage_Buffer() + sourceBuffer.width = UInt(width) + sourceBuffer.height = UInt(height) + sourceBuffer.data = UnsafeMutableRawPointer(mutating: bytes.baseAddress!.advanced(by: firstFrame.range.lowerBound)) + sourceBuffer.rowBytes = bytesPerRow + + var destinationBuffer = vImage_Buffer() + destinationBuffer.width = UInt(32) + destinationBuffer.height = UInt(32) + destinationBuffer.data = context.bytes + destinationBuffer.rowBytes = bytesPerRow + + vImageBoxConvolve_ARGB8888(&sourceBuffer, + &destinationBuffer, + nil, + UInt(width - 32 - 16), UInt(height - 32 - 16), + UInt32(31), + UInt32(31), + nil, + vImage_Flags(kvImageEdgeExtend))*/ } guard let image = context.generateImage() else { @@ -59,6 +82,7 @@ private final class FrameGroup { self.image = image self.size = CGSize(width: CGFloat(width), height: CGFloat(height)) self.timestamp = timestamp + self.badgeImage = nil } } } diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputGlobeIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputGlobeIcon.imageset/Contents.json new file mode 100644 index 0000000000..28fd472ac9 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputGlobeIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "planet.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputGlobeIcon.imageset/planet.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputGlobeIcon.imageset/planet.pdf new file mode 100644 index 0000000000..c2225439d8 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputGlobeIcon.imageset/planet.pdf @@ -0,0 +1,255 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 3.500000 1.839844 cm +0.000000 0.000000 0.000000 scn +22.170000 13.160156 m +22.170000 7.267279 17.392878 2.490156 11.500000 2.490156 c +11.500000 0.830156 l +18.309671 0.830156 23.830000 6.350485 23.830000 13.160156 c +22.170000 13.160156 l +h +11.500000 2.490156 m +5.607122 2.490156 0.830000 7.267279 0.830000 13.160156 c +-0.830000 13.160156 l +-0.830000 6.350485 4.690329 0.830156 11.500000 0.830156 c +11.500000 2.490156 l +h +0.830000 13.160156 m +0.830000 19.053034 5.607122 23.830156 11.500000 23.830156 c +11.500000 25.490156 l +4.690329 25.490156 -0.830000 19.969828 -0.830000 13.160156 c +0.830000 13.160156 l +h +11.500000 23.830156 m +17.392878 23.830156 22.170000 19.053034 22.170000 13.160156 c +23.830000 13.160156 l +23.830000 19.969828 18.309671 25.490156 11.500000 25.490156 c +11.500000 23.830156 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 8.500000 1.839844 cm +0.000000 0.000000 0.000000 scn +12.170000 13.160156 m +12.170000 10.100716 11.467313 7.371819 10.373627 5.436836 c +9.265684 3.476629 7.863927 2.490156 6.500000 2.490156 c +6.500000 0.830156 l +8.725924 0.830156 10.574167 2.418047 11.818761 4.620020 c +13.077613 6.847219 13.830000 9.868322 13.830000 13.160156 c +12.170000 13.160156 l +h +6.500000 2.490156 m +5.136073 2.490156 3.734316 3.476629 2.626373 5.436836 c +1.532687 7.371819 0.830000 10.100716 0.830000 13.160156 c +-0.830000 13.160156 l +-0.830000 9.868322 -0.077613 6.847219 1.181239 4.620020 c +2.425833 2.418047 4.274076 0.830156 6.500000 0.830156 c +6.500000 2.490156 l +h +0.830000 13.160156 m +0.830000 16.219597 1.532687 18.948494 2.626373 20.883476 c +3.734316 22.843683 5.136073 23.830156 6.500000 23.830156 c +6.500000 25.490156 l +4.274076 25.490156 2.425833 23.902266 1.181239 21.700291 c +-0.077613 19.473093 -0.830000 16.451990 -0.830000 13.160156 c +0.830000 13.160156 l +h +6.500000 23.830156 m +7.863927 23.830156 9.265684 22.843683 10.373627 20.883476 c +11.467313 18.948494 12.170000 16.219597 12.170000 13.160156 c +13.830000 13.160156 l +13.830000 16.451990 13.077613 19.473093 11.818761 21.700291 c +10.574167 23.902266 8.725924 25.490156 6.500000 25.490156 c +6.500000 23.830156 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 15.000000 3.500000 cm +0.000000 0.000000 0.000000 scn +-0.830000 23.000000 m +-0.830000 0.000000 l +0.830000 0.000000 l +0.830000 23.000000 l +-0.830000 23.000000 l +h +f +n +Q +q +0.000000 1.000000 -1.000000 0.000000 5.160156 15.000000 cm +0.000000 0.000000 0.000000 scn +-0.830000 1.660156 m +-0.830000 -21.339844 l +0.830000 -21.339844 l +0.830000 1.660156 l +-0.830000 1.660156 l +h +f +n +Q +q +0.000000 1.000000 -1.000000 0.000000 9.019043 8.000000 cm +0.000000 0.000000 0.000000 scn +0.000000 3.519043 m +-0.686377 3.052367 -0.686606 3.052704 -0.686827 3.053029 c +-0.686890 3.053123 -0.687103 3.053437 -0.687230 3.053624 c +-0.687484 3.053999 -0.687707 3.054330 -0.687901 3.054617 c +-0.688287 3.055190 -0.688552 3.055586 -0.688699 3.055805 c +-0.688991 3.056243 -0.688808 3.055973 -0.688163 3.054991 c +-0.686874 3.053027 -0.683741 3.048217 -0.678884 3.040542 c +-0.669169 3.025192 -0.652556 2.998377 -0.630008 2.959931 c +-0.584915 2.883047 -0.516061 2.759615 -0.431147 2.588317 c +-0.261366 2.245815 -0.027114 1.711416 0.209833 0.974417 c +0.683340 -0.498374 1.170000 -2.787649 1.170000 -5.980957 c +2.830000 -5.980957 l +2.830000 -2.615824 2.316660 -0.155099 1.790166 1.482499 c +1.527114 2.300695 1.261366 2.911589 1.056147 3.325581 c +0.953561 3.532529 0.866165 3.690144 0.801883 3.799746 c +0.769744 3.854543 0.743388 3.897330 0.723805 3.928271 c +0.714015 3.943742 0.705917 3.956251 0.699638 3.965818 c +0.696498 3.970602 0.693813 3.974651 0.691598 3.977967 c +0.690490 3.979625 0.689500 3.981099 0.688629 3.982391 c +0.688194 3.983037 0.687788 3.983638 0.687413 3.984192 c +0.687225 3.984469 0.686966 3.984851 0.686872 3.984990 c +0.686621 3.985360 0.686377 3.985719 0.000000 3.519043 c +h +1.170000 -5.980957 m +1.170000 -9.174265 0.683340 -11.463540 0.209833 -12.936331 c +-0.027114 -13.673330 -0.261366 -14.207729 -0.431147 -14.550232 c +-0.516061 -14.721529 -0.584915 -14.844961 -0.630008 -14.921844 c +-0.652556 -14.960291 -0.669169 -14.987106 -0.678884 -15.002457 c +-0.683741 -15.010132 -0.686874 -15.014940 -0.688163 -15.016905 c +-0.688808 -15.017887 -0.688991 -15.018158 -0.688699 -15.017719 c +-0.688552 -15.017500 -0.688287 -15.017103 -0.687901 -15.016531 c +-0.687707 -15.016245 -0.687484 -15.015913 -0.687230 -15.015539 c +-0.687103 -15.015350 -0.686890 -15.015036 -0.686827 -15.014942 c +-0.686606 -15.014618 -0.686377 -15.014280 0.000000 -15.480957 c +0.686377 -15.947634 0.686621 -15.947273 0.686872 -15.946903 c +0.686966 -15.946766 0.687225 -15.946383 0.687413 -15.946106 c +0.687788 -15.945551 0.688194 -15.944952 0.688629 -15.944305 c +0.689500 -15.943014 0.690490 -15.941538 0.691598 -15.939880 c +0.693813 -15.936565 0.696498 -15.932516 0.699638 -15.927732 c +0.705917 -15.918165 0.714015 -15.905657 0.723805 -15.890184 c +0.743388 -15.859243 0.769744 -15.816458 0.801883 -15.761660 c +0.866165 -15.652058 0.953561 -15.494444 1.056147 -15.287495 c +1.261366 -14.873503 1.527114 -14.262609 1.790166 -13.444414 c +2.316660 -11.806815 2.830000 -9.346090 2.830000 -5.980957 c +1.170000 -5.980957 l +h +f +n +Q +q +0.000000 -1.000000 1.000000 0.000000 20.980957 22.000000 cm +0.000000 0.000000 0.000000 scn +0.000000 3.519043 m +-0.686377 3.052367 -0.686606 3.052704 -0.686827 3.053029 c +-0.686890 3.053123 -0.687103 3.053437 -0.687230 3.053624 c +-0.687484 3.053999 -0.687707 3.054330 -0.687901 3.054617 c +-0.688287 3.055190 -0.688552 3.055586 -0.688699 3.055805 c +-0.688991 3.056243 -0.688808 3.055973 -0.688163 3.054991 c +-0.686874 3.053027 -0.683741 3.048217 -0.678884 3.040542 c +-0.669169 3.025192 -0.652556 2.998377 -0.630008 2.959931 c +-0.584915 2.883047 -0.516061 2.759615 -0.431147 2.588317 c +-0.261366 2.245815 -0.027114 1.711416 0.209833 0.974417 c +0.683340 -0.498374 1.170000 -2.787649 1.170000 -5.980957 c +2.830000 -5.980957 l +2.830000 -2.615824 2.316660 -0.155099 1.790166 1.482499 c +1.527114 2.300695 1.261366 2.911589 1.056147 3.325581 c +0.953561 3.532529 0.866165 3.690144 0.801883 3.799746 c +0.769744 3.854543 0.743388 3.897330 0.723805 3.928271 c +0.714015 3.943742 0.705917 3.956251 0.699638 3.965818 c +0.696498 3.970602 0.693813 3.974651 0.691598 3.977967 c +0.690490 3.979625 0.689500 3.981099 0.688629 3.982391 c +0.688194 3.983037 0.687788 3.983638 0.687413 3.984192 c +0.687225 3.984469 0.686966 3.984851 0.686872 3.984990 c +0.686621 3.985360 0.686377 3.985719 0.000000 3.519043 c +h +1.170000 -5.980957 m +1.170000 -9.174265 0.683340 -11.463540 0.209833 -12.936331 c +-0.027114 -13.673330 -0.261366 -14.207729 -0.431147 -14.550232 c +-0.516061 -14.721529 -0.584915 -14.844961 -0.630008 -14.921844 c +-0.652556 -14.960291 -0.669169 -14.987106 -0.678884 -15.002457 c +-0.683741 -15.010132 -0.686874 -15.014940 -0.688163 -15.016905 c +-0.688808 -15.017887 -0.688991 -15.018158 -0.688699 -15.017719 c +-0.688552 -15.017500 -0.688287 -15.017103 -0.687901 -15.016531 c +-0.687707 -15.016245 -0.687484 -15.015913 -0.687230 -15.015539 c +-0.687103 -15.015350 -0.686890 -15.015036 -0.686827 -15.014942 c +-0.686606 -15.014618 -0.686377 -15.014280 0.000000 -15.480957 c +0.686377 -15.947634 0.686621 -15.947273 0.686872 -15.946903 c +0.686966 -15.946766 0.687225 -15.946383 0.687413 -15.946106 c +0.687788 -15.945551 0.688194 -15.944952 0.688629 -15.944305 c +0.689500 -15.943014 0.690490 -15.941538 0.691598 -15.939880 c +0.693813 -15.936565 0.696498 -15.932516 0.699638 -15.927732 c +0.705917 -15.918165 0.714015 -15.905657 0.723805 -15.890184 c +0.743388 -15.859243 0.769744 -15.816458 0.801883 -15.761660 c +0.866165 -15.652058 0.953561 -15.494444 1.056147 -15.287495 c +1.261366 -14.873503 1.527114 -14.262609 1.790166 -13.444414 c +2.316660 -11.806815 2.830000 -9.346090 2.830000 -5.980957 c +1.170000 -5.980957 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 7785 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000007875 00000 n +0000007898 00000 n +0000008071 00000 n +0000008145 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +8204 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Resources/Animations/anim_smiletosticker.json b/submodules/TelegramUI/Resources/Animations/anim_smiletosticker.json new file mode 100644 index 0000000000..26d136207a --- /dev/null +++ b/submodules/TelegramUI/Resources/Animations/anim_smiletosticker.json @@ -0,0 +1 @@ +{"v":"5.8.1","fr":60,"ip":0,"op":24,"w":30,"h":30,"nm":"smiletosticker","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Path 85","parent":5,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[12.746,-12.746,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[5,0],[0,0],[0.829,-0.406],[0,0]],"o":[[0,0],[0.406,-0.829],[0,-0.391],[0,-5],[-0.391,0],[0,0],[-0.829,0.406],[0,0]],"v":[[5.633,6.5],[5.733,6.296],[6.613,4.5],[6.613,2.5],[-2.387,-6.5],[-4.387,-6.5],[-6.183,-5.62],[-6.387,-5.52]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[{"i":[[0,0],[0,0],[0.923,0],[0,0],[0,4.418],[0,0],[0.829,-0.406],[0,0]],"o":[[0,0],[0.406,-0.829],[0,0],[-4.418,0],[0,0],[0,-0.923],[0,0],[0,0]],"v":[[5.896,6.124],[5.996,5.92],[4.876,4.124],[3.467,4.124],[-4.124,-3.467],[-4.124,-4.876],[-5.92,-5.996],[-6.124,-5.896]],"c":false}]},{"t":20,"s":[{"i":[[0,0],[0,0],[0.923,0],[0,0],[0,4.418],[0,0],[0.829,-0.406],[0,0]],"o":[[0,0],[0.406,-0.829],[0,0],[-4.418,0],[0,0],[0,-0.923],[0,0],[0,0]],"v":[[5.896,6.124],[5.996,5.92],[4.876,4.124],[3.876,4.124],[-4.124,-3.876],[-4.124,-4.876],[-5.92,-5.996],[-6.124,-5.896]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path 85","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":2,"op":24,"st":-60,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Oval 2","parent":5,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[19.043,-11.087,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"t":5,"s":[0,0,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[2.348,2.739],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Oval","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":24,"st":-60,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Oval","parent":5,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-18.997,-11.087,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"t":5,"s":[0,0,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[2.348,2.739],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Oval","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":24,"st":-60,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Ellipse 34","parent":5,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,21.212,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-2.363,0],[0,0]],"o":[[0,0],[2.363,0],[0,0]],"v":[[-3.5,-0.965],[0,0.965],[3.5,-0.965]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 34","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":5,"s":[50]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[100]},{"t":5,"s":[50]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":24,"st":-60,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Ellipse 33","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[15,15,0],"to":[-0.045,0.043,0],"ti":[0.045,-0.043,0]},{"t":10,"s":[14.728,15.259,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[16.667,16.667,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[-4.971,0],[0,4.971],[0.599,1.205],[1.758,0.874],[1.437,0],[0,-4.971]],"o":[[4.971,0],[0,-1.437],[-0.874,-1.758],[-1.205,-0.599],[-4.971,0],[0,4.971]],"v":[[0,9],[9,0],[8.064,-4],[4,-8.064],[0,-9],[-9,0]],"c":true}]},{"t":10,"s":[{"i":[[-4.971,0],[-1.46,2.97],[1.89,1.97],[0,0],[2.35,-1.126],[0,-3.49]],"o":[[3.49,0],[1.19,-2.35],[0,0],[-1.97,-1.89],[-2.97,1.524],[0,4.971]],"v":[[0.272,8.741],[8.272,3.741],[6.272,-3.259],[3.272,-6.259],[-3.728,-8.323],[-8.728,-0.259]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 33","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":24,"st":-60,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/submodules/TelegramUI/Resources/Animations/anim_stickertosmile.json b/submodules/TelegramUI/Resources/Animations/anim_stickertosmile.json new file mode 100644 index 0000000000..1f209a5db8 --- /dev/null +++ b/submodules/TelegramUI/Resources/Animations/anim_stickertosmile.json @@ -0,0 +1 @@ +{"v":"5.8.1","fr":60,"ip":60,"op":84,"w":30,"h":30,"nm":"stickertosmile","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":2,"ty":4,"nm":"Path 85","parent":6,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":67,"s":[100]},{"t":68,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[12.746,-12.746,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":60,"s":[{"i":[[0,0],[0,0],[0.923,0],[0,0],[0,4.418],[0,0],[0.829,-0.406],[0,0]],"o":[[0,0],[0.406,-0.829],[0,0],[-4.418,0],[0,0],[0,-0.923],[0,0],[0,0]],"v":[[5.896,6.124],[5.996,5.92],[4.876,4.124],[3.876,4.124],[-4.124,-3.876],[-4.124,-4.876],[-5.92,-5.996],[-6.124,-5.896]],"c":false}]},{"t":70,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[5,0],[0,0],[0.829,-0.406],[0,0]],"o":[[0,0],[0.406,-0.829],[0,-0.391],[0,-5],[-0.391,0],[0,0],[-0.829,0.406],[0,0]],"v":[[5.633,6.5],[5.733,6.296],[6.613,4.5],[6.613,2.5],[-2.387,-6.5],[-4.387,-6.5],[-6.183,-5.62],[-6.387,-5.52]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path 85","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":122,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Oval 2","parent":6,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[19.043,-11.087,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":60,"s":[0,0,100]},{"t":70,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[2.348,2.739],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Oval","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":64,"op":120,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Oval","parent":6,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-18.997,-11.087,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":60,"s":[0,0,100]},{"t":70,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[2.348,2.739],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Oval","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Ellipse 34","parent":6,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,21.212,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-2.363,0],[0,0]],"o":[[0,0],[2.363,0],[0,0]],"v":[[-3.5,-0.965],[0,0.965],[3.5,-0.965]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 34","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":60,"s":[50]},{"t":70,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":60,"s":[50]},{"t":70,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Ellipse 33","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":60,"s":[14.728,15.259,0],"to":[0.045,-0.043,0],"ti":[-0.045,0.043,0]},{"t":70,"s":[15,15,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":60,"s":[16.667,16.667,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":70,"s":[17.5,17.5,100]},{"t":80,"s":[16.667,16.667,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":60,"s":[{"i":[[-4.971,0],[-1.46,2.97],[1.89,1.97],[0,0],[2.35,-1.126],[0,-3.49]],"o":[[3.49,0],[1.19,-2.35],[0,0],[-1.97,-1.89],[-2.97,1.524],[0,4.971]],"v":[[0.272,8.741],[8.272,3.741],[6.272,-3.259],[3.272,-6.259],[-3.728,-8.323],[-8.728,-0.259]],"c":true}]},{"t":70,"s":[{"i":[[-4.971,0],[0,4.971],[0.599,1.205],[1.758,0.874],[1.437,0],[0,-4.971]],"o":[[4.971,0],[0,-1.437],[-0.874,-1.758],[-1.205,-0.599],[-4.971,0],[0,4.971]],"v":[[0,9],[9,0],[8.064,-4],[4,-8.064],[0,-9],[-9,0]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 33","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/ChatBotStartInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatBotStartInputPanelNode.swift index df7ddf406e..028769b5ec 100644 --- a/submodules/TelegramUI/Sources/ChatBotStartInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatBotStartInputPanelNode.swift @@ -89,7 +89,7 @@ final class ChatBotStartInputPanelNode: ChatInputPanelNode { self.interfaceInteraction?.sendBotStart(presentationInterfaceState.botStartPayload) } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { + 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 { if self.presentationInterfaceState != interfaceState { self.presentationInterfaceState = interfaceState } diff --git a/submodules/TelegramUI/Sources/ChatButtonKeyboardInputNode.swift b/submodules/TelegramUI/Sources/ChatButtonKeyboardInputNode.swift index 51ca4407d7..5e1898e644 100644 --- a/submodules/TelegramUI/Sources/ChatButtonKeyboardInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatButtonKeyboardInputNode.swift @@ -115,7 +115,7 @@ final class ChatButtonKeyboardInputNode: ChatInputNode { } } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, deviceMetrics: DeviceMetrics, isVisible: Bool) -> (CGFloat, CGFloat) { + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, deviceMetrics: DeviceMetrics, isVisible: Bool, isExpanded: Bool) -> (CGFloat, CGFloat) { transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: UIScreenPixel))) let updatedTheme = self.theme !== interfaceState.theme diff --git a/submodules/TelegramUI/Sources/ChatChannelSubscriberInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatChannelSubscriberInputPanelNode.swift index b79d0bab44..d75ce83bff 100644 --- a/submodules/TelegramUI/Sources/ChatChannelSubscriberInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatChannelSubscriberInputPanelNode.swift @@ -265,7 +265,7 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { } } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { + 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 { return self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: transition, interfaceState: interfaceState, metrics: metrics, force: false) } diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 86f724661d..34a5d34f2a 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -532,7 +532,11 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { return } - strongSelf.requestLayout(transition) + if transition.isAnimated { + strongSelf.scheduleLayoutTransitionRequest(transition) + } else { + strongSelf.requestLayout(transition) + } } self.addSubnode(self.inputPanelContainerNode) @@ -592,6 +596,12 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.textInputPanelNode?.updateActivity = { [weak self] in self?.updateTypingActivity(true) } + self.textInputPanelNode?.toggleExpandMediaInput = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.inputPanelContainerNode.toggleIfEnabled() + } self.inputMediaNodeDataDisposable = (self.inputMediaNodeDataPromise.get() |> deliverOnMainQueue).start(next: { [weak self] value in @@ -1056,7 +1066,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { if inputTextPanelNode.isFocused { self.context.sharedContext.mainWindow?.simulateKeyboardDismiss(transition: .animated(duration: 0.5, curve: .spring)) } - let _ = inputTextPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: layout.intrinsicInsets.bottom, additionalSideInsets: layout.additionalInsets, maxHeight: layout.size.height - insets.top - inputPanelBottomInset, isSecondary: false, transition: transition, interfaceState: self.chatPresentationInterfaceState, metrics: layout.metrics) + let _ = inputTextPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: layout.intrinsicInsets.bottom, additionalSideInsets: layout.additionalInsets, maxHeight: layout.size.height - insets.top - inputPanelBottomInset, isSecondary: false, transition: transition, interfaceState: self.chatPresentationInterfaceState, metrics: layout.metrics, isMediaInputExpanded: self.inputPanelContainerNode.expansionFraction == 1.0) } if let prevInputPanelNode = self.inputPanelNode, inputPanelNode.canHandleTransition(from: prevInputPanelNode) { inputPanelNodeHandlesTransition = true @@ -1066,7 +1076,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } else { dismissedInputPanelNode = self.inputPanelNode } - let inputPanelHeight = inputPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: layout.intrinsicInsets.bottom, additionalSideInsets: layout.additionalInsets, maxHeight: layout.size.height - insets.top - inputPanelBottomInset, isSecondary: false, transition: inputPanelNode.supernode !== self ? .immediate : transition, interfaceState: self.chatPresentationInterfaceState, metrics: layout.metrics) + let inputPanelHeight = inputPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: layout.intrinsicInsets.bottom, additionalSideInsets: layout.additionalInsets, maxHeight: layout.size.height - insets.top - inputPanelBottomInset, isSecondary: false, transition: inputPanelNode.supernode !== self ? .immediate : transition, interfaceState: self.chatPresentationInterfaceState, metrics: layout.metrics, isMediaInputExpanded: self.inputPanelContainerNode.expansionFraction == 1.0) inputPanelSize = CGSize(width: layout.size.width, height: inputPanelHeight) self.inputPanelNode = inputPanelNode if inputPanelNode.supernode !== self { @@ -1074,7 +1084,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.inputPanelClippingNode.insertSubnode(inputPanelNode, aboveSubnode: self.inputPanelBackgroundNode) } } else { - let inputPanelHeight = inputPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: layout.intrinsicInsets.bottom, additionalSideInsets: layout.additionalInsets, maxHeight: layout.size.height - insets.top - inputPanelBottomInset - 120.0, isSecondary: false, transition: transition, interfaceState: self.chatPresentationInterfaceState, metrics: layout.metrics) + let inputPanelHeight = inputPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: layout.intrinsicInsets.bottom, additionalSideInsets: layout.additionalInsets, maxHeight: layout.size.height - insets.top - inputPanelBottomInset - 120.0, isSecondary: false, transition: transition, interfaceState: self.chatPresentationInterfaceState, metrics: layout.metrics, isMediaInputExpanded: self.inputPanelContainerNode.expansionFraction == 1.0) inputPanelSize = CGSize(width: layout.size.width, height: inputPanelHeight) } } else { @@ -1085,7 +1095,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { if let secondaryInputPanelNode = inputPanelNodes.secondary, !previewing { if secondaryInputPanelNode !== self.secondaryInputPanelNode { dismissedSecondaryInputPanelNode = self.secondaryInputPanelNode - let inputPanelHeight = secondaryInputPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: layout.intrinsicInsets.bottom, additionalSideInsets: layout.additionalInsets, maxHeight: layout.size.height - insets.top - inputPanelBottomInset, isSecondary: true, transition: .immediate, interfaceState: self.chatPresentationInterfaceState, metrics: layout.metrics) + let inputPanelHeight = secondaryInputPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: layout.intrinsicInsets.bottom, additionalSideInsets: layout.additionalInsets, maxHeight: layout.size.height - insets.top - inputPanelBottomInset, isSecondary: true, transition: .immediate, interfaceState: self.chatPresentationInterfaceState, metrics: layout.metrics, isMediaInputExpanded: self.inputPanelContainerNode.expansionFraction == 1.0) secondaryInputPanelSize = CGSize(width: layout.size.width, height: inputPanelHeight) self.secondaryInputPanelNode = secondaryInputPanelNode if secondaryInputPanelNode.supernode == nil { @@ -1093,7 +1103,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.inputPanelClippingNode.insertSubnode(secondaryInputPanelNode, aboveSubnode: self.inputPanelBackgroundNode) } } else { - let inputPanelHeight = secondaryInputPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: layout.intrinsicInsets.bottom, additionalSideInsets: layout.additionalInsets, maxHeight: layout.size.height - insets.top - inputPanelBottomInset, isSecondary: true, transition: transition, interfaceState: self.chatPresentationInterfaceState, metrics: layout.metrics) + let inputPanelHeight = secondaryInputPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: layout.intrinsicInsets.bottom, additionalSideInsets: layout.additionalInsets, maxHeight: layout.size.height - insets.top - inputPanelBottomInset, isSecondary: true, transition: transition, interfaceState: self.chatPresentationInterfaceState, metrics: layout.metrics, isMediaInputExpanded: self.inputPanelContainerNode.expansionFraction == 1.0) secondaryInputPanelSize = CGSize(width: layout.size.width, height: inputPanelHeight) } } else { @@ -1142,7 +1152,11 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { var maximumInputNodeHeight = layout.size.height - max(layout.statusBarHeight ?? 0.0, layout.safeInsets.top) - 10.0 if let inputPanelSize = inputPanelSize { - maximumInputNodeHeight -= inputPanelSize.height + if let inputNode = self.inputNode, inputNode.hideInput, !inputNode.adjustLayoutForHiddenInput { + maximumInputNodeHeight -= inputPanelNodeBaseHeight + } else { + maximumInputNodeHeight -= inputPanelSize.height + } } if let secondaryInputPanelSize = secondaryInputPanelSize { maximumInputNodeHeight -= secondaryInputPanelSize.height @@ -1210,13 +1224,13 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } } - if inputNode.hideInput, let inputPanelSize = inputPanelSize { + if inputNode.hideInput, inputNode.adjustLayoutForHiddenInput, let inputPanelSize = inputPanelSize { maximumInputNodeHeight += inputPanelSize.height } let inputHeight = layout.standardInputHeight + self.inputPanelContainerNode.expansionFraction * (maximumInputNodeHeight - layout.standardInputHeight) - let heightAndOverflow = inputNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: cleanInsets.bottom, standardInputHeight: inputHeight, inputHeight: layout.inputHeight ?? 0.0, maximumHeight: maximumInputNodeHeight, inputPanelHeight: inputPanelNodeBaseHeight, transition: immediatelyLayoutInputNodeAndAnimateAppearance ? .immediate : transition, interfaceState: self.chatPresentationInterfaceState, deviceMetrics: layout.deviceMetrics, isVisible: self.isInFocus) + let heightAndOverflow = inputNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: cleanInsets.bottom, standardInputHeight: inputHeight, inputHeight: layout.inputHeight ?? 0.0, maximumHeight: maximumInputNodeHeight, inputPanelHeight: inputPanelNodeBaseHeight, transition: immediatelyLayoutInputNodeAndAnimateAppearance ? .immediate : transition, interfaceState: self.chatPresentationInterfaceState, deviceMetrics: layout.deviceMetrics, isVisible: self.isInFocus, isExpanded: self.inputPanelContainerNode.stableIsExpanded) let boundedHeight = min(heightAndOverflow.0, layout.standardInputHeight) @@ -1267,7 +1281,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.historyNode.isSelectionGestureEnabled = isSelectionEnabled if let inputMediaNode = self.inputMediaNode, inputMediaNode != self.inputNode { - let _ = inputMediaNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: cleanInsets.bottom, standardInputHeight: layout.standardInputHeight, inputHeight: layout.inputHeight ?? 0.0, maximumHeight: maximumInputNodeHeight, inputPanelHeight: inputPanelSize?.height ?? 0.0, transition: .immediate, interfaceState: self.chatPresentationInterfaceState, deviceMetrics: layout.deviceMetrics, isVisible: false) + let _ = inputMediaNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: cleanInsets.bottom, standardInputHeight: layout.standardInputHeight, inputHeight: layout.inputHeight ?? 0.0, maximumHeight: maximumInputNodeHeight, inputPanelHeight: inputPanelSize?.height ?? 0.0, transition: .immediate, interfaceState: self.chatPresentationInterfaceState, deviceMetrics: layout.deviceMetrics, isVisible: false, isExpanded: self.inputPanelContainerNode.stableIsExpanded) } transition.updateFrame(node: self.titleAccessoryPanelContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: 66.0))) @@ -1358,7 +1372,11 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { if self.dismissedAsOverlay { inputPanelFrame!.origin.y = layout.size.height } - inputPanelsHeight += inputPanelSize!.height + if let inputNode = self.inputNode, inputNode.hideInput, !inputNode.adjustLayoutForHiddenInput { + inputPanelsHeight += inputPanelNodeBaseHeight + } else { + inputPanelsHeight += inputPanelSize!.height + } } if self.secondaryInputPanelNode != nil { @@ -1384,7 +1402,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } if let inputNode = self.inputNode { - if inputNode.hideInput { + if inputNode.hideInput && inputNode.adjustLayoutForHiddenInput { inputPanelsHeight = 0.0 } } @@ -1963,7 +1981,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { private func updateInputPanelBackgroundExpansion(transition: ContainedViewLayoutTransition) { if let inputNode = self.inputNode { - if inputNode.hideInput { + if inputNode.hideInput && inputNode.adjustLayoutForHiddenInput { self.storedHideInputExpanded = self.inputPanelContainerNode.expansionFraction == 1.0 self.inputPanelContainerNode.expand() } else { @@ -2270,6 +2288,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.inputPanelContainerNode.collapse() if let inputNode = self.inputNode { inputNode.hideInput = false + inputNode.adjustLayoutForHiddenInput = false if let inputNode = inputNode as? ChatEntityKeyboardInputNode { inputNode.markInputCollapsed() } @@ -2344,7 +2363,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } self.inputMediaNode = inputNode if let (validLayout, _) = self.validLayout { - let _ = inputNode.updateLayout(width: validLayout.size.width, leftInset: validLayout.safeInsets.left, rightInset: validLayout.safeInsets.right, bottomInset: validLayout.intrinsicInsets.bottom, standardInputHeight: validLayout.standardInputHeight, inputHeight: validLayout.inputHeight ?? 0.0, maximumHeight: validLayout.standardInputHeight, inputPanelHeight: 44.0, transition: .immediate, interfaceState: self.chatPresentationInterfaceState, deviceMetrics: validLayout.deviceMetrics, isVisible: false) + let _ = inputNode.updateLayout(width: validLayout.size.width, leftInset: validLayout.safeInsets.left, rightInset: validLayout.safeInsets.right, bottomInset: validLayout.intrinsicInsets.bottom, standardInputHeight: validLayout.standardInputHeight, inputHeight: validLayout.inputHeight ?? 0.0, maximumHeight: validLayout.standardInputHeight, inputPanelHeight: 44.0, transition: .immediate, interfaceState: self.chatPresentationInterfaceState, deviceMetrics: validLayout.deviceMetrics, isVisible: false, isExpanded: self.inputPanelContainerNode.stableIsExpanded) } } diff --git a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift index ec3ec5f6db..f5a5170437 100644 --- a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift @@ -14,6 +14,7 @@ import TelegramCore import ComponentDisplayAdapters import SettingsUI import TextFormat +import PagerComponent final class ChatEntityKeyboardInputNode: ChatInputNode { struct InputData: Equatable { @@ -215,11 +216,11 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { var title: String? if group.id == AnyHashable("recent") { //TODO:localize - title = "Recently Used".uppercased() + title = "Recently Used" } else { for (id, info, _) in view.collectionInfos { if AnyHashable(id) == group.id, let info = info as? StickerPackCollectionInfo { - title = info.title.uppercased() + title = info.title break } } @@ -396,17 +397,17 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { var title: String? if group.id == AnyHashable("saved") { //TODO:localize - title = "Saved".uppercased() + title = "Saved" } else if group.id == AnyHashable("recent") { //TODO:localize - title = "Recently Used".uppercased() + title = "Recently Used" } else if group.id == AnyHashable("premium") { //TODO:localize - title = "Premium".uppercased() + title = "Premium" } else { for (id, info, _) in view.collectionInfos { if AnyHashable(id) == group.id, let info = info as? StickerPackCollectionInfo { - title = info.title.uppercased() + title = info.title break } } @@ -462,7 +463,12 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { private var isMarkInputCollapsed: Bool = false - private var currentState: (width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, interfaceState: ChatPresentationInterfaceState, deviceMetrics: DeviceMetrics, isVisible: Bool)? + var externalTopPanelContainerImpl: PagerExternalTopPanelContainer? + override var externalTopPanelContainer: UIView? { + return self.externalTopPanelContainerImpl + } + + private var currentState: (width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, interfaceState: ChatPresentationInterfaceState, deviceMetrics: DeviceMetrics, isVisible: Bool, isExpanded: Bool)? init(context: AccountContext, currentInputData: InputData, updatedInputData: Signal, defaultToEmojiTab: Bool, controllerInteraction: ChatControllerInteraction) { self.context = context @@ -477,7 +483,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { self.view.addSubview(self.entityKeyboardView) - self.externalTopPanelContainer = SparseContainerView() + self.externalTopPanelContainerImpl = PagerExternalTopPanelContainer() self.inputDataDisposable = (updatedInputData |> deliverOnMainQueue).start(next: { [weak self] inputData in @@ -530,20 +536,25 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { } private func performLayout() { - guard let (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, deviceMetrics, isVisible) = self.currentState else { + guard let (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, deviceMetrics, isVisible, isExpanded) = self.currentState else { return } - let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, inputHeight: inputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: .immediate, interfaceState: interfaceState, deviceMetrics: deviceMetrics, isVisible: isVisible) + let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, inputHeight: inputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: .immediate, interfaceState: interfaceState, deviceMetrics: deviceMetrics, isVisible: isVisible, isExpanded: isExpanded) } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, deviceMetrics: DeviceMetrics, isVisible: Bool) -> (CGFloat, CGFloat) { - self.currentState = (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, deviceMetrics, isVisible) + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, deviceMetrics: DeviceMetrics, isVisible: Bool, isExpanded: Bool) -> (CGFloat, CGFloat) { + self.currentState = (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, deviceMetrics, isVisible, isExpanded) let wasMarkedInputCollapsed = self.isMarkInputCollapsed self.isMarkInputCollapsed = false let expandedHeight = standardInputHeight + self.expansionFraction * (maximumHeight - standardInputHeight) + var hiddenInputHeight: CGFloat = 0.0 + if self.hideInput && !self.adjustLayoutForHiddenInput { + hiddenInputHeight = inputPanelHeight + } + let context = self.context let controllerInteraction = self.controllerInteraction let inputNodeInteraction = self.inputNodeInteraction! @@ -564,7 +575,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { stickerContent: self.currentInputData.stickers, gifContent: self.currentInputData.gifs, defaultToEmojiTab: self.defaultToEmojiTab, - externalTopPanelContainer: self.externalTopPanelContainer, + externalTopPanelContainer: self.externalTopPanelContainerImpl, topPanelExtensionUpdated: { [weak self] topPanelExtension, transition in guard let strongSelf = self else { return @@ -574,15 +585,24 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { strongSelf.topBackgroundExtensionUpdated?(transition.containedViewLayoutTransition) } }, - hideInputUpdated: { [weak self] hideInput, transition in + hideInputUpdated: { [weak self] hideInput, adjustLayout, transition in guard let strongSelf = self else { return } - if strongSelf.hideInput != hideInput { + if strongSelf.hideInput != hideInput || strongSelf.adjustLayoutForHiddenInput != adjustLayout { strongSelf.hideInput = hideInput + strongSelf.adjustLayoutForHiddenInput = adjustLayout strongSelf.hideInputUpdated?(transition.containedViewLayoutTransition) } }, + switchToTextInput: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.controllerInteraction.updateInputMode { _ in + return .text + } + }, makeSearchContainerNode: { content in let mappedMode: ChatMediaInputSearchMode switch content { @@ -605,7 +625,9 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { } ) }, - deviceMetrics: deviceMetrics + deviceMetrics: deviceMetrics, + hiddenInputHeight: hiddenInputHeight, + isExpanded: isExpanded )), environment: {}, containerSize: CGSize(width: width, height: expandedHeight) diff --git a/submodules/TelegramUI/Sources/ChatFeedNavigationInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatFeedNavigationInputPanelNode.swift index a81a9e812e..f815821911 100644 --- a/submodules/TelegramUI/Sources/ChatFeedNavigationInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatFeedNavigationInputPanelNode.swift @@ -54,7 +54,7 @@ final class ChatFeedNavigationInputPanelNode: ChatInputPanelNode { self.interfaceInteraction?.navigateFeed() } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { + 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 { if self.presentationInterfaceState != interfaceState { self.presentationInterfaceState = interfaceState } diff --git a/submodules/TelegramUI/Sources/ChatInputNode.swift b/submodules/TelegramUI/Sources/ChatInputNode.swift index cf978d6891..311f6a0a8d 100644 --- a/submodules/TelegramUI/Sources/ChatInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatInputNode.swift @@ -11,18 +11,21 @@ class ChatInputNode: ASDisplayNode { return .single(Void()) } - var externalTopPanelContainer: UIView? + var externalTopPanelContainer: UIView? { + return nil + } var topBackgroundExtension: CGFloat = 41.0 var topBackgroundExtensionUpdated: ((ContainedViewLayoutTransition) -> Void)? var hideInput: Bool = false + var adjustLayoutForHiddenInput: Bool = false var hideInputUpdated: ((ContainedViewLayoutTransition) -> Void)? var expansionFraction: CGFloat = 0.0 var expansionFractionUpdated: ((ContainedViewLayoutTransition) -> Void)? - func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, deviceMetrics: DeviceMetrics, isVisible: Bool) -> (CGFloat, CGFloat) { + func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, deviceMetrics: DeviceMetrics, isVisible: Bool, isExpanded: Bool) -> (CGFloat, CGFloat) { return (0.0, 0.0) } } diff --git a/submodules/TelegramUI/Sources/ChatInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatInputPanelNode.swift index 52001cf08b..659fa7acb2 100644 --- a/submodules/TelegramUI/Sources/ChatInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatInputPanelNode.swift @@ -15,7 +15,7 @@ class ChatInputPanelNode: ASDisplayNode { func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition) { } - func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { + 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 { return 0.0 } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift b/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift index 68d107097c..a275b87060 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift @@ -302,6 +302,9 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte } var stickersEnabled = true + + let stickersAreEmoji = !isTextEmpty + if let peer = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel { if isTextEmpty, case .broadcast = peer.info, canSendMessagesToPeer(peer) { accessoryItems.append(.silentPost(chatPresentationInterfaceState.interfaceState.silentPosting)) @@ -318,7 +321,7 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte accessoryItems.append(.commands) } - accessoryItems.append(.stickers(stickersEnabled)) + accessoryItems.append(.stickers(isEnabled: stickersEnabled, isEmoji: stickersAreEmoji)) if isTextEmpty, let message = chatPresentationInterfaceState.keyboardButtonsMessage, let _ = message.visibleButtonKeyboardMarkup, chatPresentationInterfaceState.interfaceState.messageActionsState.dismissedButtonKeyboardMessageId != message.id { accessoryItems.append(.inputButtons) diff --git a/submodules/TelegramUI/Sources/ChatMediaInputNode.swift b/submodules/TelegramUI/Sources/ChatMediaInputNode.swift index 4870710465..fbcf89e302 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputNode.swift @@ -577,7 +577,7 @@ final class ChatMediaInputNode: ChatInputNode { var requestDisableStickerAnimations: ((Bool) -> Void)? - private var validLayout: (CGFloat, CGFloat, CGFloat, CGFloat, CGFloat, CGFloat, CGFloat, CGFloat, ChatPresentationInterfaceState, DeviceMetrics, Bool)? + private var validLayout: (CGFloat, CGFloat, CGFloat, CGFloat, CGFloat, CGFloat, CGFloat, CGFloat, ChatPresentationInterfaceState, DeviceMetrics, Bool, Bool)? private var paneArrangement: ChatMediaInputPaneArrangement private var initializedArrangement = false @@ -1473,7 +1473,7 @@ final class ChatMediaInputNode: ChatInputNode { } }))) - if let (_, _, _, _, _, _, _, _, interfaceState, _, _) = strongSelf.validLayout { + if let (_, _, _, _, _, _, _, _, interfaceState, _, _, _) = strongSelf.validLayout { var isScheduledMessages = false if case .scheduledMessages = interfaceState.subject { isScheduledMessages = true @@ -1595,7 +1595,7 @@ final class ChatMediaInputNode: ChatInputNode { |> map { isStarred -> (UIView, CGRect, PeekControllerContent)? in if let strongSelf = self { var menuItems: [ContextMenuItem] = [] - if let (_, _, _, _, _, _, _, _, interfaceState, _, _) = strongSelf.validLayout { + if let (_, _, _, _, _, _, _, _, interfaceState, _, _, _) = strongSelf.validLayout { var isScheduledMessages = false if case .scheduledMessages = interfaceState.subject { isScheduledMessages = true @@ -1747,7 +1747,7 @@ final class ChatMediaInputNode: ChatInputNode { |> map { isStarred, hasPremium -> (UIView, CGRect, PeekControllerContent)? in if let strongSelf = self { var menuItems: [ContextMenuItem] = [] - if let (_, _, _, _, _, _, _, _, interfaceState, _, _) = strongSelf.validLayout { + if let (_, _, _, _, _, _, _, _, interfaceState, _, _, _) = strongSelf.validLayout { var isScheduledMessages = false if case .scheduledMessages = interfaceState.subject { isScheduledMessages = true @@ -1922,8 +1922,8 @@ final class ChatMediaInputNode: ChatInputNode { self.paneArrangement = self.paneArrangement.withIndexTransition(0.0).withCurrentIndex(index) let updatedGifPanelWasActive = self.paneArrangement.panes[self.paneArrangement.currentIndex] == .gifs - if let (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, deviceMetrics, isVisible) = self.validLayout { - let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, inputHeight: inputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: transition, interfaceState: interfaceState, deviceMetrics: deviceMetrics, isVisible: isVisible) + if let (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, deviceMetrics, isVisible, isExpanded) = self.validLayout { + let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, inputHeight: inputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: transition, interfaceState: interfaceState, deviceMetrics: deviceMetrics, isVisible: isVisible, isExpanded: isExpanded) self.updateAppearanceTransition(transition: transition) } if updatedGifPanelWasActive != previousGifPanelWasActive { @@ -1940,8 +1940,8 @@ final class ChatMediaInputNode: ChatInputNode { } } } else { - if let (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, deviceMetrics, isVisible) = self.validLayout { - let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, inputHeight: inputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: .animated(duration: 0.25, curve: .spring), interfaceState: interfaceState, deviceMetrics: deviceMetrics, isVisible: isVisible) + if let (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, deviceMetrics, isVisible, isExpanded) = self.validLayout { + let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, inputHeight: inputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: .animated(duration: 0.25, curve: .spring), interfaceState: interfaceState, deviceMetrics: deviceMetrics, isVisible: isVisible, isExpanded: isExpanded) } } } @@ -2179,20 +2179,20 @@ final class ChatMediaInputNode: ChatInputNode { } func simulateUpdateLayout(isVisible: Bool) { - if let (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, deviceMetrics, _) = self.validLayout { - let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, inputHeight: inputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: .immediate, interfaceState: interfaceState, deviceMetrics: deviceMetrics, isVisible: isVisible) + if let (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, deviceMetrics, _, isExpanded) = self.validLayout { + let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, inputHeight: inputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: .immediate, interfaceState: interfaceState, deviceMetrics: deviceMetrics, isVisible: isVisible, isExpanded: isExpanded) } } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, deviceMetrics: DeviceMetrics, isVisible: Bool) -> (CGFloat, CGFloat) { + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, deviceMetrics: DeviceMetrics, isVisible: Bool, isExpanded: Bool) -> (CGFloat, CGFloat) { var searchMode: ChatMediaInputSearchMode? - if let (_, _, _, _, _, _, _, _, interfaceState, _, _) = self.validLayout, case let .media(_, maybeExpanded, _) = interfaceState.inputMode, let expanded = maybeExpanded, case let .search(mode) = expanded { + if let (_, _, _, _, _, _, _, _, interfaceState, _, _, _) = self.validLayout, case let .media(_, maybeExpanded, _) = interfaceState.inputMode, let expanded = maybeExpanded, case let .search(mode) = expanded { searchMode = mode } let wasVisible = self.validLayout?.10 ?? false - self.validLayout = (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, deviceMetrics, isVisible) + self.validLayout = (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, deviceMetrics, isVisible, isExpanded) if self.theme !== interfaceState.theme || self.strings !== interfaceState.strings { self.updateThemeAndStrings(chatWallpaper: interfaceState.chatWallpaper, theme: interfaceState.theme, strings: interfaceState.strings) @@ -2545,7 +2545,7 @@ final class ChatMediaInputNode: ChatInputNode { self.stickerPane.removeFromSupernode() } case .changed: - if let (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, deviceMetrics, isVisible) = self.validLayout { + if let (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, deviceMetrics, isVisible, isExpanded) = self.validLayout { let translationX = -recognizer.translation(in: self.view).x var indexTransition = translationX / width if self.paneArrangement.currentIndex == 0 { @@ -2554,10 +2554,10 @@ final class ChatMediaInputNode: ChatInputNode { indexTransition = min(0.0, indexTransition) } self.paneArrangement = self.paneArrangement.withIndexTransition(indexTransition) - let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, inputHeight: inputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: .immediate, interfaceState: interfaceState, deviceMetrics: deviceMetrics, isVisible: isVisible) + let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, inputHeight: inputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: .immediate, interfaceState: interfaceState, deviceMetrics: deviceMetrics, isVisible: isVisible, isExpanded: isExpanded) } case .ended: - if let (width, _, _, _, _, _, _, _, _, _, _) = self.validLayout { + if let (width, _, _, _, _, _, _, _, _, _, _, _) = self.validLayout { var updatedIndex = self.paneArrangement.currentIndex if abs(self.paneArrangement.indexTransition * width) > 30.0 { if self.paneArrangement.indexTransition < 0.0 { @@ -2570,9 +2570,9 @@ final class ChatMediaInputNode: ChatInputNode { self.setCurrentPane(self.paneArrangement.panes[updatedIndex], transition: .animated(duration: 0.25, curve: .spring)) } case .cancelled: - if let (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, deviceMetrics, isVisible) = self.validLayout { + if let (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, deviceMetrics, isVisible, isExpanded) = self.validLayout { self.paneArrangement = self.paneArrangement.withIndexTransition(0.0) - let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, inputHeight: inputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: .animated(duration: 0.25, curve: .spring), interfaceState: interfaceState, deviceMetrics: deviceMetrics, isVisible: isVisible) + let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, inputHeight: inputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: .animated(duration: 0.25, curve: .spring), interfaceState: interfaceState, deviceMetrics: deviceMetrics, isVisible: isVisible, isExpanded: isExpanded) } default: break diff --git a/submodules/TelegramUI/Sources/ChatMessageReportInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatMessageReportInputPanelNode.swift index 39f695d631..507373f8a2 100644 --- a/submodules/TelegramUI/Sources/ChatMessageReportInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageReportInputPanelNode.swift @@ -71,7 +71,7 @@ final class ChatMessageReportInputPanelNode: ChatInputPanelNode { } } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { + 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 { if self.presentationInterfaceState != interfaceState { self.presentationInterfaceState = interfaceState diff --git a/submodules/TelegramUI/Sources/ChatMessageSelectionInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatMessageSelectionInputPanelNode.swift index 754c1371d9..e8c465c9e4 100644 --- a/submodules/TelegramUI/Sources/ChatMessageSelectionInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageSelectionInputPanelNode.swift @@ -17,7 +17,7 @@ final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { private let shareButton: HighlightableButtonNode private let separatorNode: ASDisplayNode - private var validLayout: (width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, metrics: LayoutMetrics, isSecondary: Bool)? + private var validLayout: (width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, metrics: LayoutMetrics, isSecondary: Bool, isMediaInputExpanded: Bool)? private var presentationInterfaceState: ChatPresentationInterfaceState? private var actions: ChatAvailableMessageActions? @@ -33,8 +33,8 @@ final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { if self.selectedMessages.isEmpty { self.actions = nil - if let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, metrics, isSecondary) = self.validLayout, let interfaceState = self.presentationInterfaceState { - let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: .immediate, interfaceState: interfaceState, metrics: metrics) + if let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, metrics, isSecondary, isMediaInputExpanded) = self.validLayout, let interfaceState = self.presentationInterfaceState { + let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: .immediate, interfaceState: interfaceState, metrics: metrics, isMediaInputExpanded: isMediaInputExpanded) } self.canDeleteMessagesDisposable.set(nil) } else if let context = self.context { @@ -42,8 +42,8 @@ final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { |> deliverOnMainQueue).start(next: { [weak self] actions in if let strongSelf = self { strongSelf.actions = actions - if let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, metrics, isSecondary) = strongSelf.validLayout, let interfaceState = strongSelf.presentationInterfaceState { - let _ = strongSelf.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: .immediate, interfaceState: interfaceState, metrics: metrics) + if let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, metrics, isSecondary, isMediaInputExpanded) = strongSelf.validLayout, let interfaceState = strongSelf.presentationInterfaceState { + let _ = strongSelf.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: .immediate, interfaceState: interfaceState, metrics: metrics, isMediaInputExpanded: isMediaInputExpanded) } } })) @@ -154,8 +154,8 @@ final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { } } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { - self.validLayout = (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, metrics, isSecondary) + 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 { + self.validLayout = (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, metrics, isSecondary, isMediaInputExpanded) let panelHeight = defaultHeight(metrics: metrics) diff --git a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift index b66b0bb07a..b5c56c774e 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift @@ -355,8 +355,9 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { var updatedAttributes: [NSAttributedString.Key: Any] = currentDict updatedAttributes[NSAttributedString.Key.foregroundColor] = UIColor.clear.cgColor updatedAttributes[NSAttributedString.Key("Attribute__EmbeddedItem")] = InlineStickerItem(emoji: ChatTextInputTextCustomEmojiAttribute(stickerPack: stickerPack, fileId: fileId)) + updatedAttributes[ChatTextInputAttributes.customEmoji] = ChatTextInputTextCustomEmojiAttribute(stickerPack: stickerPack, fileId: fileId) - let insertString = NSAttributedString(string: "[\u{00a0}\u{00a0}\u{00a0}]", attributes: updatedAttributes) + let insertString = NSAttributedString(string: updatedString.attributedSubstring(from: range).string, attributes: updatedAttributes) updatedString.replaceCharacters(in: range, with: insertString) } attributedText = updatedString diff --git a/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift index ef4e9a8f36..d5a11df5da 100644 --- a/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift @@ -135,7 +135,7 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { } } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { + 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 { if self.presentationInterfaceState != interfaceState { var updateWaveform = false if self.presentationInterfaceState?.recordedMediaPreview != interfaceState.recordedMediaPreview { diff --git a/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift index fe74f4e2d8..3f97a2f20e 100644 --- a/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift @@ -23,7 +23,7 @@ final class ChatRestrictedInputPanelNode: ChatInputPanelNode { self.addSubnode(self.textNode) } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { + 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 { if self.presentationInterfaceState != interfaceState { self.presentationInterfaceState = interfaceState } diff --git a/submodules/TelegramUI/Sources/ChatSearchInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatSearchInputPanelNode.swift index 2b8150c7ea..b6988a7d8c 100644 --- a/submodules/TelegramUI/Sources/ChatSearchInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatSearchInputPanelNode.swift @@ -28,7 +28,7 @@ final class ChatSearchInputPanelNode: ChatInputPanelNode { private var needsSearchResultsTooltip = true - private var validLayout: (width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, metrics: LayoutMetrics, isSecondary: Bool)? + private var validLayout: (width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, metrics: LayoutMetrics, isSecondary: Bool, isMediaInputExpanded: Bool)? override var interfaceInteraction: ChatPanelInterfaceInteraction? { didSet { @@ -38,8 +38,8 @@ final class ChatSearchInputPanelNode: ChatInputPanelNode { if let strongSelf = self, strongSelf.displayActivity != value { strongSelf.displayActivity = value strongSelf.activityIndicator.isHidden = !value - if let interfaceState = strongSelf.presentationInterfaceState, let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, metrics, isSecondary) = strongSelf.validLayout { - let _ = strongSelf.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: .immediate, interfaceState: interfaceState, metrics: metrics) + if let interfaceState = strongSelf.presentationInterfaceState, let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, metrics, isSecondary, isMediaInputExpanded) = strongSelf.validLayout { + let _ = strongSelf.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: .immediate, interfaceState: interfaceState, metrics: metrics, isMediaInputExpanded: isMediaInputExpanded) } } })) @@ -127,8 +127,8 @@ final class ChatSearchInputPanelNode: ChatInputPanelNode { } } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { - self.validLayout = (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, metrics, isSecondary) + 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 { + self.validLayout = (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, metrics, isSecondary, isMediaInputExpanded) if self.presentationInterfaceState != interfaceState { let themeUpdated = self.presentationInterfaceState?.theme !== interfaceState.theme diff --git a/submodules/TelegramUI/Sources/ChatTextInputActionButtonsNode.swift b/submodules/TelegramUI/Sources/ChatTextInputActionButtonsNode.swift index b805b4dc1c..dd633334c6 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputActionButtonsNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputActionButtonsNode.swift @@ -132,7 +132,7 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode { self.backdropNode.update(rect: rect, within: containerSize, transition: transition) } - func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) { + func updateLayout(size: CGSize, isMediaInputExpanded: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) { self.validLayout = size transition.updateFrame(layer: self.micButton.layer, frame: CGRect(origin: CGPoint(), size: size)) self.micButton.layoutItems() @@ -183,10 +183,7 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode { } transition.updateFrame(node: self.expandMediaInputButton, frame: CGRect(origin: CGPoint(), size: size)) - var expanded = false - if case let .media(_, maybeExpanded, _) = interfaceState.inputMode, maybeExpanded != nil { - expanded = true - } + let expanded = isMediaInputExpanded transition.updateSublayerTransformScale(node: self.expandMediaInputButton, scale: CGPoint(x: 1.0, y: expanded ? 1.0 : -1.0)) } diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index dbccddb807..76487a0605 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -25,17 +25,25 @@ import ManagedAnimationNode import AttachmentUI import EditableChatTextNode import EmojiTextAttachmentView +import LottieAnimationComponent +import ComponentFlow private let accessoryButtonFont = Font.medium(14.0) private let counterFont = Font.with(size: 14.0, design: .regular, traits: [.monospacedNumbers]) private final class AccessoryItemIconButtonNode: HighlightTrackingButtonNode { - private let item: ChatTextInputAccessoryItem + private var item: ChatTextInputAccessoryItem + private var theme: PresentationTheme private var width: CGFloat + private let iconImageNode: ASImageNode + private var animationView: ComponentView? private var imageEdgeInsets = UIEdgeInsets() init(item: ChatTextInputAccessoryItem, theme: PresentationTheme, strings: PresentationStrings) { self.item = item + self.theme = theme + + self.iconImageNode = ASImageNode() let (image, text, accessibilityLabel, alpha, insets) = AccessoryItemIconButtonNode.imageAndInsets(item: item, theme: theme, strings: strings) @@ -46,7 +54,13 @@ private final class AccessoryItemIconButtonNode: HighlightTrackingButtonNode { self.isAccessibilityElement = true self.accessibilityTraits = [.button] - self.addSubnode(self.imageNode) + self.iconImageNode.isUserInteractionEnabled = false + self.addSubnode(self.iconImageNode) + + if case .stickers = item { + self.iconImageNode.isHidden = true + self.animationView = ComponentView() + } if let text = text { self.setAttributedTitle(NSAttributedString(string: text, font: accessoryButtonFont, textColor: theme.chat.inputPanel.inputControlColor), for: .normal) @@ -54,8 +68,8 @@ private final class AccessoryItemIconButtonNode: HighlightTrackingButtonNode { self.setAttributedTitle(NSAttributedString(), for: .normal) } - self.imageNode.image = image - self.imageNode.alpha = alpha + self.iconImageNode.image = image + self.iconImageNode.alpha = alpha self.imageEdgeInsets = insets self.accessibilityLabel = accessibilityLabel @@ -74,6 +88,8 @@ private final class AccessoryItemIconButtonNode: HighlightTrackingButtonNode { } func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { + self.theme = theme + let (image, text, accessibilityLabel, alpha, insets) = AccessoryItemIconButtonNode.imageAndInsets(item: item, theme: theme, strings: strings) self.width = AccessoryItemIconButtonNode.calculateWidth(item: item, image: image, text: text, strings: strings) @@ -84,9 +100,9 @@ private final class AccessoryItemIconButtonNode: HighlightTrackingButtonNode { self.setAttributedTitle(NSAttributedString(), for: .normal) } - self.imageNode.image = image + self.iconImageNode.image = image self.imageEdgeInsets = insets - self.imageNode.alpha = alpha + self.iconImageNode.alpha = alpha self.accessibilityLabel = accessibilityLabel } @@ -99,7 +115,7 @@ private final class AccessoryItemIconButtonNode: HighlightTrackingButtonNode { switch item { case .keyboard: return (PresentationResourcesChat.chatInputTextFieldKeyboardImage(theme), nil, strings.VoiceOver_Keyboard, 1.0, UIEdgeInsets()) - case let .stickers(enabled): + case let .stickers(enabled, _): return (PresentationResourcesChat.chatInputTextFieldStickersImage(theme), nil, strings.VoiceOver_Stickers, enabled ? 1.0 : 0.4, UIEdgeInsets()) case .inputButtons: return (PresentationResourcesChat.chatInputTextFieldInputButtonsImage(theme), nil, strings.VoiceOver_BotKeyboard, 1.0, UIEdgeInsets()) @@ -136,11 +152,48 @@ private final class AccessoryItemIconButtonNode: HighlightTrackingButtonNode { } } - func updateLayout(size: CGSize) { - if let image = self.imageNode.image { + func updateLayout(item: ChatTextInputAccessoryItem, size: CGSize) { + self.item = item + + if let image = self.iconImageNode.image { 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.imageNode.frame = imageFrame + self.iconImageNode.frame = imageFrame + + if case let .stickers(_, isEmoji) = item, let animationView = self.animationView { + let animationFrame = imageFrame.insetBy(dx: -4.0, dy: -4.0) + + var colors: [String: UIColor] = [:] + let colorKeys: [String] = [ + "Ellipse 33.Ellipse 33.Stroke 1", + "Ellipse 34.Ellipse 34.Stroke 1", + "Oval.Oval.Fill 1", + "Oval 2.Oval.Fill 1", + "Path 85.Path 85.Stroke 1" + ] + for colorKey in colorKeys { + colors[colorKey] = self.theme.chat.inputPanel.inputControlColor + } + let _ = animationView.update( + transition: .immediate, + component: AnyComponent(LottieAnimationComponent( + animation: LottieAnimationComponent.AnimationItem( + name: !isEmoji ? "anim_stickertosmile" : "anim_smiletosticker", + colors: colors, + mode: .animateTransitionFromPrevious + ), + size: animationFrame.size + )), + environment: {}, + containerSize: animationFrame.size + ) + if let view = animationView.view { + if view.superview == nil { + self.view.addSubview(view) + } + view.frame = animationFrame + } + } } } @@ -196,7 +249,7 @@ private func calculateTextFieldRealInsets(presentationInterfaceState: ChatPresen } var right: CGFloat = 0.0 - right += accessoryButtonsWidth + right += max(0.0, accessoryButtonsWidth - 14.0) return UIEdgeInsets(top: 4.5 + top, left: 0.0, bottom: 5.5 + bottom, right: right) } @@ -347,13 +400,14 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { private var accessoryItemButtons: [(ChatTextInputAccessoryItem, AccessoryItemIconButtonNode)] = [] - private var validLayout: (CGFloat, CGFloat, CGFloat, CGFloat, UIEdgeInsets, CGFloat, LayoutMetrics, Bool)? + private var validLayout: (CGFloat, CGFloat, CGFloat, CGFloat, UIEdgeInsets, CGFloat, LayoutMetrics, Bool, Bool)? private var leftMenuInset: CGFloat = 0.0 var displayAttachmentMenu: () -> Void = { } var sendMessage: () -> Void = { } var paste: (ChatTextInputPanelPasteData) -> Void = { _ in } var updateHeight: (Bool) -> Void = { _ in } + var toggleExpandMediaInput: (() -> Void)? var updateActivity: () -> Void = { } @@ -450,8 +504,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { for item in accessoryItems { var itemAndButton: (ChatTextInputAccessoryItem, AccessoryItemIconButtonNode)? for i in 0 ..< self.accessoryItemButtons.count { - if self.accessoryItemButtons[i].0 == item { + if self.accessoryItemButtons[i].0.key == item.key { itemAndButton = self.accessoryItemButtons[i] + itemAndButton?.0 = item self.accessoryItemButtons.remove(at: i) break } @@ -661,15 +716,15 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } self.actionButtons.micButton.offsetRecordingControls = { [weak self] in if let strongSelf = self, let presentationInterfaceState = strongSelf.presentationInterfaceState { - if let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, metrics, isSecondary) = strongSelf.validLayout { - let _ = strongSelf.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: .immediate, interfaceState: presentationInterfaceState, metrics: metrics) + if let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, metrics, isSecondary, isMediaInputExpanded) = strongSelf.validLayout { + let _ = strongSelf.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: .immediate, interfaceState: presentationInterfaceState, metrics: metrics, isMediaInputExpanded: isMediaInputExpanded) } } } self.actionButtons.micButton.updateCancelTranslation = { [weak self] in if let strongSelf = self, let presentationInterfaceState = strongSelf.presentationInterfaceState { - if let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, metrics, isSecondary) = strongSelf.validLayout { - let _ = strongSelf.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: .immediate, interfaceState: presentationInterfaceState, metrics: metrics) + if let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, metrics, isSecondary, isMediaInputExpanded) = strongSelf.validLayout { + let _ = strongSelf.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: .immediate, interfaceState: presentationInterfaceState, metrics: metrics, isMediaInputExpanded: isMediaInputExpanded) } } } @@ -911,10 +966,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.actionButtons.updateAbsoluteRect(CGRect(origin: rect.origin.offsetBy(dx: self.actionButtons.frame.minX, dy: self.actionButtons.frame.minY), size: self.actionButtons.frame.size), within: containerSize, transition: transition) } } - - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { + 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 { let previousAdditionalSideInsets = self.validLayout?.4 - self.validLayout = (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, metrics, isSecondary) + self.validLayout = (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, metrics, isSecondary, isMediaInputExpanded) let textFieldWaitsForTouchUp: Bool if case .regular = metrics.widthClass, bottomInset.isZero { @@ -1228,8 +1282,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { for item in interfaceState.inputTextPanelState.accessoryItems { var itemAndButton: (ChatTextInputAccessoryItem, AccessoryItemIconButtonNode)? for i in 0 ..< self.accessoryItemButtons.count { - if self.accessoryItemButtons[i].0 == item { + if self.accessoryItemButtons[i].0.key == item.key { itemAndButton = self.accessoryItemButtons[i] + itemAndButton?.0 = item self.accessoryItemButtons.remove(at: i) break } @@ -1661,7 +1716,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } if let presentationInterfaceState = self.presentationInterfaceState { - self.actionButtons.updateLayout(size: CGSize(width: 44.0, height: minimalHeight), transition: transition, interfaceState: presentationInterfaceState) + self.actionButtons.updateLayout(size: CGSize(width: 44.0, height: minimalHeight), isMediaInputExpanded: isMediaInputExpanded, transition: transition, interfaceState: presentationInterfaceState) } if let _ = interfaceState.inputTextPanelState.mediaRecordingState { @@ -1784,9 +1839,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } var nextButtonTopRight = CGPoint(x: width - rightInset - textFieldInsets.right - accessoryButtonInset, y: panelHeight - textFieldInsets.bottom - minimalInputHeight) - for (_, button) in self.accessoryItemButtons.reversed() { + for (item, button) in self.accessoryItemButtons.reversed() { let buttonSize = CGSize(width: button.buttonWidth, height: minimalInputHeight) - button.updateLayout(size: buttonSize) + button.updateLayout(item: item, size: buttonSize) let buttonFrame = CGRect(origin: CGPoint(x: nextButtonTopRight.x - buttonSize.width, y: nextButtonTopRight.y + floor((minimalInputHeight - buttonSize.height) / 2.0)), size: buttonSize) if button.supernode == nil { self.clippingNode.addSubnode(button) @@ -2159,7 +2214,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.counterTextNode.attributedText = NSAttributedString(string: "", font: counterFont, textColor: .black) } - if let (width, leftInset, rightInset, _, _, maxHeight, metrics, _) = self.validLayout { + if let (width, leftInset, rightInset, _, _, maxHeight, metrics, _, _) = self.validLayout { var composeButtonsOffset: CGFloat = 0.0 if self.extendedSearchLayout { composeButtonsOffset = 44.0 @@ -2299,8 +2354,10 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } + let hideExpandMediaInput = hideMicButton + if mediaInputIsActive { - //hideMicButton = true + hideMicButton = true } if hideMicButton { @@ -2325,7 +2382,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } - if mediaInputIsActive && !"".isEmpty { + if mediaInputIsActive && !hideExpandMediaInput { if self.actionButtons.expandMediaInputButton.alpha.isZero { self.actionButtons.expandMediaInputButton.alpha = 1.0 if animated { @@ -2350,7 +2407,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } private func updateTextHeight(animated: Bool) { - if let (width, leftInset, rightInset, _, additionalSideInsets, maxHeight, metrics, _) = self.validLayout { + if let (width, leftInset, rightInset, _, additionalSideInsets, maxHeight, metrics, _, _) = self.validLayout { let (_, textFieldHeight) = self.calculateTextFieldMetrics(width: width - leftInset - rightInset - additionalSideInsets.right - self.leftMenuInset, maxHeight: maxHeight, metrics: metrics) let panelHeight = self.panelHeight(textFieldHeight: textFieldHeight, metrics: metrics) if !self.bounds.size.height.isEqual(to: panelHeight) { @@ -2836,7 +2893,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } @objc func expandButtonPressed() { - self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in + self.toggleExpandMediaInput?() + /*self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in if case let .media(mode, expanded, focused) = state.inputMode { if let _ = expanded { return (.media(mode: mode, expanded: nil, focused: focused), state.interfaceState.messageActionsState.closedButtonKeyboardMessageId) @@ -2846,37 +2904,37 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } else { return (state.inputMode, state.interfaceState.messageActionsState.closedButtonKeyboardMessageId) } - }) + })*/ } @objc func accessoryItemButtonPressed(_ button: UIView) { for (item, currentButton) in self.accessoryItemButtons { if currentButton === button { switch item { - case let .stickers(enabled): - if enabled { - self.interfaceInteraction?.openStickers() - } else { - self.interfaceInteraction?.displayRestrictedInfo(.stickers, .tooltip) - } - case .keyboard: - self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in - return (.text, state.keyboardButtonsMessage?.id) - }) - case .inputButtons: - self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in - return (.inputButtons, nil) - }) - case .commands: - self.interfaceInteraction?.updateTextInputStateAndMode { _, inputMode in - return (ChatTextInputState(inputText: NSAttributedString(string: "/")), .text) - } - case .silentPost: - self.interfaceInteraction?.toggleSilentPost() - case .messageAutoremoveTimeout: - self.interfaceInteraction?.setupMessageAutoremoveTimeout() - case .scheduledMessages: - self.interfaceInteraction?.openScheduledMessages() + case let .stickers(enabled, _): + if enabled { + self.interfaceInteraction?.openStickers() + } else { + self.interfaceInteraction?.displayRestrictedInfo(.stickers, .tooltip) + } + case .keyboard: + self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in + return (.text, state.keyboardButtonsMessage?.id) + }) + case .inputButtons: + self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in + return (.inputButtons, nil) + }) + case .commands: + self.interfaceInteraction?.updateTextInputStateAndMode { _, inputMode in + return (ChatTextInputState(inputText: NSAttributedString(string: "/")), .text) + } + case .silentPost: + self.interfaceInteraction?.toggleSilentPost() + case .messageAutoremoveTimeout: + self.interfaceInteraction?.setupMessageAutoremoveTimeout() + case .scheduledMessages: + self.interfaceInteraction?.openScheduledMessages() } break } diff --git a/submodules/TelegramUI/Sources/ChatUnblockInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatUnblockInputPanelNode.swift index 3ff6e5ddfe..c8eb3bbf8f 100644 --- a/submodules/TelegramUI/Sources/ChatUnblockInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatUnblockInputPanelNode.swift @@ -82,7 +82,7 @@ final class ChatUnblockInputPanelNode: ChatInputPanelNode { self.interfaceInteraction?.unblockPeer() } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { + 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 { if self.presentationInterfaceState != interfaceState { self.presentationInterfaceState = interfaceState diff --git a/submodules/TelegramUI/Sources/DeleteChatInputPanelNode.swift b/submodules/TelegramUI/Sources/DeleteChatInputPanelNode.swift index ad0310c571..f5fbf51104 100644 --- a/submodules/TelegramUI/Sources/DeleteChatInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/DeleteChatInputPanelNode.swift @@ -35,7 +35,7 @@ final class DeleteChatInputPanelNode: ChatInputPanelNode { self.interfaceInteraction?.deleteChat() } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { + 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 { if self.presentationInterfaceState != interfaceState { self.presentationInterfaceState = interfaceState diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index e29d553d77..d1064dcaa8 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -379,7 +379,7 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode { self.separatorNode.backgroundColor = presentationData.theme.rootController.navigationBar.separatorColor let interfaceState = ChatPresentationInterfaceState(chatWallpaper: .color(0), theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, limitsConfiguration: .defaultValue, fontSize: .regular, bubbleCorners: PresentationChatBubbleCorners(mainRadius: 16.0, auxiliaryRadius: 8.0, mergeBubbleCorners: true), accountPeerId: self.context.account.peerId, mode: .standard(previewing: false), chatLocation: .peer(id: self.peerId), subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil) - let panelHeight = self.selectionPanel.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: layout.intrinsicInsets.bottom, additionalSideInsets: UIEdgeInsets(), maxHeight: 0.0, isSecondary: false, transition: transition, interfaceState: interfaceState, metrics: layout.metrics) + let panelHeight = self.selectionPanel.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: layout.intrinsicInsets.bottom, additionalSideInsets: UIEdgeInsets(), maxHeight: 0.0, isSecondary: false, transition: transition, interfaceState: interfaceState, metrics: layout.metrics, isMediaInputExpanded: false) transition.updateFrame(node: self.selectionPanel, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: panelHeight))) diff --git a/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift index 6d71ab7ba3..a99acd9b86 100644 --- a/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift @@ -491,7 +491,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { if textInputPanelNode.frame.width.isZero { panelTransition = .immediate } - var panelHeight = textInputPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: layout.intrinsicInsets.bottom, additionalSideInsets: UIEdgeInsets(), maxHeight: layout.size.height / 2.0, isSecondary: false, transition: panelTransition, interfaceState: self.presentationInterfaceState, metrics: layout.metrics) + var panelHeight = textInputPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: layout.intrinsicInsets.bottom, additionalSideInsets: UIEdgeInsets(), maxHeight: layout.size.height / 2.0, isSecondary: false, transition: panelTransition, interfaceState: self.presentationInterfaceState, metrics: layout.metrics, isMediaInputExpanded: false) if self.searchDisplayController == nil { panelHeight += insets.bottom } else { diff --git a/submodules/TelegramUI/Sources/SecretChatHandshakeStatusInputPanelNode.swift b/submodules/TelegramUI/Sources/SecretChatHandshakeStatusInputPanelNode.swift index 67053612a4..0dcdecc376 100644 --- a/submodules/TelegramUI/Sources/SecretChatHandshakeStatusInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/SecretChatHandshakeStatusInputPanelNode.swift @@ -44,7 +44,7 @@ final class SecretChatHandshakeStatusInputPanelNode: ChatInputPanelNode { self.interfaceInteraction?.unblockPeer() } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { + 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 { if self.presentationInterfaceState != interfaceState { self.presentationInterfaceState = interfaceState diff --git a/submodules/TextFormat/Sources/StringWithAppliedEntities.swift b/submodules/TextFormat/Sources/StringWithAppliedEntities.swift index 2d6ebff158..ad3588a8b4 100644 --- a/submodules/TextFormat/Sources/StringWithAppliedEntities.swift +++ b/submodules/TextFormat/Sources/StringWithAppliedEntities.swift @@ -249,6 +249,8 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.Timecode), value: TelegramTimecode(time: time, text: text), range: range) } } + case let .CustomEmoji(stickerPack, fileId): + string.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(stickerPack: stickerPack, fileId: fileId), range: range) default: break }