diff --git a/submodules/ComponentFlow/Source/Base/Transition.swift b/submodules/ComponentFlow/Source/Base/Transition.swift index a57d781e9d..ecfc9ec761 100644 --- a/submodules/ComponentFlow/Source/Base/Transition.swift +++ b/submodules/ComponentFlow/Source/Base/Transition.swift @@ -192,14 +192,17 @@ public struct Transition { } switch self.animation { case .none: - view.frame = frame + view.bounds = CGRect(origin: view.bounds.origin, size: frame.size) + view.layer.position = CGPoint(x: frame.midX, y: frame.midY) view.layer.removeAnimation(forKey: "position") view.layer.removeAnimation(forKey: "bounds") completion?(true) case .curve: let previousPosition = view.layer.presentation()?.position ?? view.center let previousBounds = view.layer.presentation()?.bounds ?? view.bounds - view.frame = frame + + view.bounds = CGRect(origin: previousBounds.origin, size: frame.size) + view.center = CGPoint(x: frame.midX, y: frame.midY) self.animatePosition(view: view, from: previousPosition, to: view.center, completion: completion) self.animateBounds(view: view, from: previousBounds, to: view.bounds) @@ -293,12 +296,17 @@ public struct Transition { view.layer.sublayerTransform = transform completion?(true) case let .curve(duration, curve): - let previousValue = view.layer.sublayerTransform + let previousValue: CATransform3D + if let presentation = view.layer.presentation() { + previousValue = presentation.sublayerTransform + } else { + previousValue = view.layer.sublayerTransform + } view.layer.sublayerTransform = transform view.layer.animate( from: NSValue(caTransform3D: previousValue), to: NSValue(caTransform3D: transform), - keyPath: "transform", + keyPath: "sublayerTransform", duration: duration, delay: 0.0, curve: curve, diff --git a/submodules/ComponentFlow/Source/Host/ComponentHostView.swift b/submodules/ComponentFlow/Source/Host/ComponentHostView.swift index 3c4698a38e..bd29143893 100644 --- a/submodules/ComponentFlow/Source/Host/ComponentHostView.swift +++ b/submodules/ComponentFlow/Source/Host/ComponentHostView.swift @@ -118,3 +118,93 @@ public final class ComponentHostView: UIView { return findTaggedViewImpl(view: componentView, tag: tag) } } + +public final class ComponentView { + private var currentComponent: AnyComponent? + private var currentContainerSize: CGSize? + private var currentSize: CGSize? + public private(set) var view: UIView? + private(set) var isUpdating: Bool = false + + public init() { + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func update(transition: Transition, component: AnyComponent, @EnvironmentBuilder environment: () -> Environment, forceUpdate: Bool = false, containerSize: CGSize) -> CGSize { + let size = self._update(transition: transition, component: component, maybeEnvironment: environment, updateEnvironment: true, forceUpdate: forceUpdate, containerSize: containerSize) + self.currentSize = size + return size + } + + private func _update(transition: Transition, component: AnyComponent, maybeEnvironment: () -> Environment, updateEnvironment: Bool, forceUpdate: Bool, containerSize: CGSize) -> CGSize { + precondition(!self.isUpdating) + self.isUpdating = true + + precondition(containerSize.width.isFinite) + precondition(containerSize.height.isFinite) + + let componentView: UIView + if let current = self.view { + componentView = current + } else { + componentView = component._makeView() + self.view = componentView + } + + let context = componentView.context(component: component) + + let componentState: ComponentState = context.erasedState + + if updateEnvironment { + EnvironmentBuilder._environment = context.erasedEnvironment + let environmentResult = maybeEnvironment() + EnvironmentBuilder._environment = nil + context.erasedEnvironment = environmentResult + } + + let isEnvironmentUpdated = context.erasedEnvironment.calculateIsUpdated() + + + if !forceUpdate, !isEnvironmentUpdated, let currentComponent = self.currentComponent, let currentContainerSize = self.currentContainerSize, let currentSize = self.currentSize { + if currentContainerSize == containerSize && currentComponent == component { + self.isUpdating = false + return currentSize + } + } + self.currentComponent = component + self.currentContainerSize = containerSize + + componentState._updated = { [weak self] transition in + guard let strongSelf = self else { + return + } + let _ = strongSelf._update(transition: transition, component: component, maybeEnvironment: { + preconditionFailure() + } as () -> Environment, updateEnvironment: false, forceUpdate: true, containerSize: containerSize) + } + + let updatedSize = component._update(view: componentView, availableSize: containerSize, environment: context.erasedEnvironment, transition: transition) + if transition.userData(ComponentHostViewSkipSettingFrame.self) == nil { + transition.setFrame(view: componentView, frame: CGRect(origin: CGPoint(), size: updatedSize)) + } + + if isEnvironmentUpdated { + context.erasedEnvironment._isUpdated = false + } + + self.isUpdating = false + + return updatedSize + } + + public func findTaggedView(tag: Any) -> UIView? { + guard let view = self.view else { + return nil + } + return findTaggedViewImpl(view: view, tag: tag) + } +} + diff --git a/submodules/ComponentFlow/Source/Utils/ActionSlot.swift b/submodules/ComponentFlow/Source/Utils/ActionSlot.swift index c6dd4aac40..ed351e1dff 100644 --- a/submodules/ComponentFlow/Source/Utils/ActionSlot.swift +++ b/submodules/ComponentFlow/Source/Utils/ActionSlot.swift @@ -15,7 +15,7 @@ public final class Action { public final class ActionSlot: Equatable { private var target: ((Arguments) -> Void)? - init() { + public init() { } public static func ==(lhs: ActionSlot, rhs: ActionSlot) -> Bool { diff --git a/submodules/Components/PagerComponent/Sources/PagerComponent.swift b/submodules/Components/PagerComponent/Sources/PagerComponent.swift index c24f6a728b..040f784ca4 100644 --- a/submodules/Components/PagerComponent/Sources/PagerComponent.swift +++ b/submodules/Components/PagerComponent/Sources/PagerComponent.swift @@ -3,19 +3,31 @@ import UIKit import Display import ComponentFlow +public protocol PagerExpandableScrollView: UIScrollView { +} + +public protocol PagerPanGestureRecognizer: UIGestureRecognizer { +} + public final class PagerComponentChildEnvironment: Equatable { public struct ContentScrollingUpdate { public var relativeOffset: CGFloat - public var absoluteOffsetToClosestEdge: CGFloat? + public var absoluteOffsetToTopEdge: CGFloat? + public var absoluteOffsetToBottomEdge: CGFloat? + public var isInteracting: Bool public var transition: Transition public init( relativeOffset: CGFloat, - absoluteOffsetToClosestEdge: CGFloat?, + absoluteOffsetToTopEdge: CGFloat?, + absoluteOffsetToBottomEdge: CGFloat?, + isInteracting: Bool, transition: Transition ) { self.relativeOffset = relativeOffset - self.absoluteOffsetToClosestEdge = absoluteOffsetToClosestEdge + self.absoluteOffsetToTopEdge = absoluteOffsetToTopEdge + self.absoluteOffsetToBottomEdge = absoluteOffsetToBottomEdge + self.isInteracting = isInteracting self.transition = transition } } @@ -40,28 +52,34 @@ public final class PagerComponentChildEnvironment: Equatable { } } -public final class PagerComponentPanelEnvironment: Equatable { +public final class PagerComponentPanelEnvironment: Equatable { public let contentOffset: CGFloat - public let contentTopPanels: [AnyComponentWithIdentity] + public let contentTopPanels: [AnyComponentWithIdentity] public let contentIcons: [AnyComponentWithIdentity] + public let contentAccessoryLeftButtons: [AnyComponentWithIdentity] public let contentAccessoryRightButtons: [AnyComponentWithIdentity] public let activeContentId: AnyHashable? public let navigateToContentId: (AnyHashable) -> Void + public let visibilityFractionUpdated: ActionSlot<(CGFloat, Transition)> init( contentOffset: CGFloat, - contentTopPanels: [AnyComponentWithIdentity], + contentTopPanels: [AnyComponentWithIdentity], contentIcons: [AnyComponentWithIdentity], + contentAccessoryLeftButtons: [AnyComponentWithIdentity], contentAccessoryRightButtons: [AnyComponentWithIdentity], activeContentId: AnyHashable?, - navigateToContentId: @escaping (AnyHashable) -> Void + navigateToContentId: @escaping (AnyHashable) -> Void, + visibilityFractionUpdated: ActionSlot<(CGFloat, Transition)> ) { self.contentOffset = contentOffset self.contentTopPanels = contentTopPanels self.contentIcons = contentIcons + self.contentAccessoryLeftButtons = contentAccessoryLeftButtons self.contentAccessoryRightButtons = contentAccessoryRightButtons self.activeContentId = activeContentId self.navigateToContentId = navigateToContentId + self.visibilityFractionUpdated = visibilityFractionUpdated } public static func ==(lhs: PagerComponentPanelEnvironment, rhs: PagerComponentPanelEnvironment) -> Bool { @@ -74,12 +92,18 @@ public final class PagerComponentPanelEnvironment: Equatable { if lhs.contentIcons != rhs.contentIcons { return false } + if lhs.contentAccessoryLeftButtons != rhs.contentAccessoryLeftButtons { + return false + } if lhs.contentAccessoryRightButtons != rhs.contentAccessoryRightButtons { return false } if lhs.activeContentId != rhs.activeContentId { return false } + if lhs.visibilityFractionUpdated !== rhs.visibilityFractionUpdated { + return false + } return true } @@ -93,38 +117,48 @@ public struct PagerComponentPanelState { } } -public final class PagerComponent: Component { +public final class PagerComponentViewTag { + public init() { + } +} + +public final class PagerComponent: Component { public typealias EnvironmentType = ChildEnvironmentType public let contentInsets: UIEdgeInsets public let contents: [AnyComponentWithIdentity<(ChildEnvironmentType, PagerComponentChildEnvironment)>] - public let contentTopPanels: [AnyComponentWithIdentity] + public let contentTopPanels: [AnyComponentWithIdentity] public let contentIcons: [AnyComponentWithIdentity] + public let contentAccessoryLeftButtons:[AnyComponentWithIdentity] public let contentAccessoryRightButtons:[AnyComponentWithIdentity] public let defaultId: AnyHashable? public let contentBackground: AnyComponent? - public let topPanel: AnyComponent? + public let topPanel: AnyComponent>? public let externalTopPanelContainer: UIView? - public let bottomPanel: AnyComponent? + public let bottomPanel: AnyComponent>? public let panelStateUpdated: ((PagerComponentPanelState, Transition) -> Void)? + public let hidePanels: Bool public init( contentInsets: UIEdgeInsets, contents: [AnyComponentWithIdentity<(ChildEnvironmentType, PagerComponentChildEnvironment)>], - contentTopPanels: [AnyComponentWithIdentity], + contentTopPanels: [AnyComponentWithIdentity], contentIcons: [AnyComponentWithIdentity], - contentAccessoryRightButtons:[AnyComponentWithIdentity], + contentAccessoryLeftButtons: [AnyComponentWithIdentity], + contentAccessoryRightButtons: [AnyComponentWithIdentity], defaultId: AnyHashable?, contentBackground: AnyComponent?, - topPanel: AnyComponent?, + topPanel: AnyComponent>?, externalTopPanelContainer: UIView?, - bottomPanel: AnyComponent?, - panelStateUpdated: ((PagerComponentPanelState, Transition) -> Void)? + bottomPanel: AnyComponent>?, + panelStateUpdated: ((PagerComponentPanelState, Transition) -> Void)?, + hidePanels: Bool ) { self.contentInsets = contentInsets self.contents = contents self.contentTopPanels = contentTopPanels self.contentIcons = contentIcons + self.contentAccessoryLeftButtons = contentAccessoryLeftButtons self.contentAccessoryRightButtons = contentAccessoryRightButtons self.defaultId = defaultId self.contentBackground = contentBackground @@ -132,6 +166,7 @@ public final class PagerComponent: Component { self.externalTopPanelContainer = externalTopPanelContainer self.bottomPanel = bottomPanel self.panelStateUpdated = panelStateUpdated + self.hidePanels = hidePanels } public static func ==(lhs: PagerComponent, rhs: PagerComponent) -> Bool { @@ -162,43 +197,56 @@ public final class PagerComponent: Component { if lhs.bottomPanel != rhs.bottomPanel { return false } + if lhs.hidePanels != rhs.hidePanels { + return false + } return true } - public final class View: UIView { + public final class View: UIView, ComponentTaggedView { private final class ContentView { let view: ComponentHostView<(ChildEnvironmentType, PagerComponentChildEnvironment)> - var scrollingPanelOffsetToClosestEdge: CGFloat = 0.0 + var scrollingPanelOffsetToTopEdge: CGFloat = 0.0 + var scrollingPanelOffsetToBottomEdge: CGFloat = .greatestFiniteMagnitude + var scrollingPanelOffsetFraction: CGFloat = 0.0 init(view: ComponentHostView<(ChildEnvironmentType, PagerComponentChildEnvironment)>) { self.view = view } } + private final class PagerPanGestureRecognizerImpl: UIPanGestureRecognizer, PagerPanGestureRecognizer { + } + private struct PaneTransitionGestureState { var fraction: CGFloat = 0.0 } private var contentViews: [AnyHashable: ContentView] = [:] private var contentBackgroundView: ComponentHostView? - private var topPanelView: ComponentHostView? - private var bottomPanelView: ComponentHostView? + private let topPanelVisibilityFractionUpdated = ActionSlot<(CGFloat, Transition)>() + private var topPanelView: ComponentHostView>? + private let bottomPanelVisibilityFractionUpdated = ActionSlot<(CGFloat, Transition)>() + private var bottomPanelView: ComponentHostView>? - private var centralId: AnyHashable? + private var topPanelHeight: CGFloat? + private var bottomPanelHeight: CGFloat? + + public private(set) var centralId: AnyHashable? private var paneTransitionGestureState: PaneTransitionGestureState? - private var component: PagerComponent? + private var component: PagerComponent? private weak var state: EmptyComponentState? - private var panRecognizer: UIPanGestureRecognizer? + private var panRecognizer: PagerPanGestureRecognizerImpl? override init(frame: CGRect) { super.init(frame: frame) self.disablesInteractiveTransitionGestureRecognizer = true - let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) + let panRecognizer = PagerPanGestureRecognizerImpl(target: self, action: #selector(self.panGesture(_:))) self.panRecognizer = panRecognizer self.addGestureRecognizer(panRecognizer) } @@ -207,6 +255,14 @@ public final class PagerComponent: Component { fatalError("init(coder:) has not been implemented") } + public func matches(tag: Any) -> Bool { + if tag is PagerComponentViewTag { + return true + } + + return false + } + @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) { switch recognizer.state { case .began: @@ -252,7 +308,7 @@ public final class PagerComponent: Component { } } - func update(component: PagerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: PagerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { self.component = component self.state = state @@ -288,22 +344,22 @@ public final class PagerComponent: Component { var contentInsets = component.contentInsets - let scrollingPanelOffsetToClosestEdge: CGFloat + var scrollingPanelOffsetFraction: CGFloat if let centralId = centralId, let centralContentView = self.contentViews[centralId] { - scrollingPanelOffsetToClosestEdge = centralContentView.scrollingPanelOffsetToClosestEdge + scrollingPanelOffsetFraction = centralContentView.scrollingPanelOffsetFraction } else { - scrollingPanelOffsetToClosestEdge = 0.0 + scrollingPanelOffsetFraction = 0.0 } var topPanelHeight: CGFloat = 0.0 if let topPanel = component.topPanel { - let topPanelView: ComponentHostView + let topPanelView: ComponentHostView> var topPanelTransition = transition if let current = self.topPanelView { topPanelView = current } else { topPanelTransition = .immediate - topPanelView = ComponentHostView() + topPanelView = ComponentHostView>() topPanelView.clipsToBounds = true self.topPanelView = topPanelView } @@ -321,20 +377,38 @@ public final class PagerComponent: Component { contentOffset: 0.0, contentTopPanels: component.contentTopPanels, contentIcons: [], + contentAccessoryLeftButtons: [], contentAccessoryRightButtons: [], activeContentId: centralId, - navigateToContentId: navigateToContentId + navigateToContentId: navigateToContentId, + visibilityFractionUpdated: self.topPanelVisibilityFractionUpdated ) }, containerSize: availableSize ) - let topPanelOffset = max(0.0, min(topPanelSize.height, scrollingPanelOffsetToClosestEdge)) + self.topPanelHeight = topPanelSize.height + + var topPanelOffset = topPanelSize.height * scrollingPanelOffsetFraction + + var topPanelVisibilityFraction: CGFloat = 1.0 - scrollingPanelOffsetFraction + if component.hidePanels { + topPanelVisibilityFraction = 0.0 + } + + self.topPanelVisibilityFractionUpdated.invoke((topPanelVisibilityFraction, topPanelTransition)) topPanelHeight = max(0.0, topPanelSize.height - topPanelOffset) + if component.hidePanels { + topPanelOffset = topPanelSize.height + } + if component.externalTopPanelContainer != nil { - let visibleTopPanelHeight = max(0.0, topPanelSize.height - topPanelOffset) + var visibleTopPanelHeight = max(0.0, topPanelSize.height - topPanelOffset) + if component.hidePanels { + visibleTopPanelHeight = 0.0 + } transition.setFrame(view: topPanelView, frame: CGRect(origin: CGPoint(), size: CGSize(width: topPanelSize.width, height: visibleTopPanelHeight))) } else { transition.setFrame(view: topPanelView, frame: CGRect(origin: CGPoint(x: 0.0, y: -topPanelOffset), size: topPanelSize)) @@ -342,22 +416,24 @@ public final class PagerComponent: Component { contentInsets.top += topPanelSize.height } else { - if let bottomPanelView = self.bottomPanelView { - self.bottomPanelView = nil + if let topPanelView = self.topPanelView { + self.topPanelView = nil - bottomPanelView.removeFromSuperview() + topPanelView.removeFromSuperview() } + + self.topPanelHeight = 0.0 } var bottomPanelOffset: CGFloat = 0.0 if let bottomPanel = component.bottomPanel { - let bottomPanelView: ComponentHostView + let bottomPanelView: ComponentHostView> var bottomPanelTransition = transition if let current = self.bottomPanelView { bottomPanelView = current } else { bottomPanelTransition = .immediate - bottomPanelView = ComponentHostView() + bottomPanelView = ComponentHostView>() self.bottomPanelView = bottomPanelView self.addSubview(bottomPanelView) } @@ -365,19 +441,26 @@ public final class PagerComponent: Component { transition: bottomPanelTransition, component: bottomPanel, environment: { - PagerComponentPanelEnvironment( + PagerComponentPanelEnvironment( contentOffset: 0.0, contentTopPanels: [], contentIcons: component.contentIcons, + contentAccessoryLeftButtons: component.contentAccessoryLeftButtons, contentAccessoryRightButtons: component.contentAccessoryRightButtons, activeContentId: centralId, - navigateToContentId: navigateToContentId + navigateToContentId: navigateToContentId, + visibilityFractionUpdated: self.bottomPanelVisibilityFractionUpdated ) }, containerSize: availableSize ) - bottomPanelOffset = max(0.0, min(bottomPanelSize.height, scrollingPanelOffsetToClosestEdge)) + self.bottomPanelHeight = bottomPanelSize.height + + bottomPanelOffset = bottomPanelSize.height * scrollingPanelOffsetFraction + if component.hidePanels { + bottomPanelOffset = bottomPanelSize.height + } transition.setFrame(view: bottomPanelView, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelSize.height + bottomPanelOffset), size: bottomPanelSize)) @@ -388,8 +471,12 @@ public final class PagerComponent: Component { bottomPanelView.removeFromSuperview() } + + self.bottomPanelHeight = 0.0 } + let effectiveTopPanelHeight: CGFloat = component.hidePanels ? 0.0 : topPanelHeight + if let contentBackground = component.contentBackground { let contentBackgroundView: ComponentHostView var contentBackgroundTransition = transition @@ -405,9 +492,9 @@ public final class PagerComponent: Component { transition: contentBackgroundTransition, component: contentBackground, environment: {}, - containerSize: CGSize(width: availableSize.width, height: availableSize.height - topPanelHeight - contentInsets.bottom + bottomPanelOffset) + containerSize: CGSize(width: availableSize.width, height: availableSize.height - effectiveTopPanelHeight - contentInsets.bottom + bottomPanelOffset) ) - contentBackgroundTransition.setFrame(view: contentBackgroundView, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight), size: contentBackgroundSize)) + contentBackgroundTransition.setFrame(view: contentBackgroundView, frame: CGRect(origin: CGPoint(x: 0.0, y: effectiveTopPanelHeight), size: contentBackgroundSize)) } else { if let contentBackgroundView = self.contentBackgroundView { self.contentBackgroundView = nil @@ -558,13 +645,44 @@ public final class PagerComponent: Component { return } - if let absoluteOffsetToClosestEdge = update.absoluteOffsetToClosestEdge { - contentView.scrollingPanelOffsetToClosestEdge = absoluteOffsetToClosestEdge - } else { - contentView.scrollingPanelOffsetToClosestEdge = 1000.0 + var offsetDelta: CGFloat? + offsetDelta = (update.absoluteOffsetToTopEdge ?? 0.0) - contentView.scrollingPanelOffsetToTopEdge + + contentView.scrollingPanelOffsetToTopEdge = update.absoluteOffsetToTopEdge ?? 0.0 + contentView.scrollingPanelOffsetToBottomEdge = update.absoluteOffsetToBottomEdge ?? .greatestFiniteMagnitude + + if let topPanelHeight = self.topPanelHeight, let bottomPanelHeight = self.bottomPanelHeight { + var scrollingPanelOffsetFraction = contentView.scrollingPanelOffsetFraction + + if topPanelHeight > 0.0, let offsetDelta = offsetDelta { + let fractionDelta = -offsetDelta / topPanelHeight + scrollingPanelOffsetFraction = max(0.0, min(1.0, contentView.scrollingPanelOffsetFraction - fractionDelta)) + } + + if bottomPanelHeight > 0.0 && contentView.scrollingPanelOffsetToBottomEdge < bottomPanelHeight { + scrollingPanelOffsetFraction = min(scrollingPanelOffsetFraction, contentView.scrollingPanelOffsetToBottomEdge / bottomPanelHeight) + } else if topPanelHeight > 0.0 && contentView.scrollingPanelOffsetToTopEdge < topPanelHeight { + scrollingPanelOffsetFraction = min(scrollingPanelOffsetFraction, contentView.scrollingPanelOffsetToTopEdge / topPanelHeight) + } + + var transition = update.transition + if !update.isInteracting { + if scrollingPanelOffsetFraction < 0.5 { + scrollingPanelOffsetFraction = 0.0 + } else { + scrollingPanelOffsetFraction = 1.0 + } + if case .none = transition.animation { + } else { + transition = transition.withAnimation(.curve(duration: 0.25, curve: .easeInOut)) + } + } + + if scrollingPanelOffsetFraction != contentView.scrollingPanelOffsetFraction { + contentView.scrollingPanelOffsetFraction = scrollingPanelOffsetFraction + self.state?.updated(transition: transition) + } } - - state?.updated(transition: update.transition) } } diff --git a/submodules/Display/Source/ContainedViewLayoutTransition.swift b/submodules/Display/Source/ContainedViewLayoutTransition.swift index 6ec8dccddc..15fea6c105 100644 --- a/submodules/Display/Source/ContainedViewLayoutTransition.swift +++ b/submodules/Display/Source/ContainedViewLayoutTransition.swift @@ -146,6 +146,8 @@ public extension ContainedViewLayoutTransition { } else { switch self { case .immediate: + node.layer.removeAnimation(forKey: "position") + node.layer.removeAnimation(forKey: "bounds") node.frame = frame if let completion = completion { completion(true) @@ -173,6 +175,8 @@ public extension ContainedViewLayoutTransition { } else { switch self { case .immediate: + node.layer.removeAnimation(forKey: "position") + node.layer.removeAnimation(forKey: "bounds") node.position = frame.center node.bounds = CGRect(origin: CGPoint(), size: frame.size) if let completion = completion { @@ -206,6 +210,8 @@ public extension ContainedViewLayoutTransition { } else { switch self { case .immediate: + layer.removeAnimation(forKey: "position") + layer.removeAnimation(forKey: "bounds") layer.position = frame.center layer.bounds = CGRect(origin: CGPoint(), size: frame.size) if let completion = completion { @@ -277,6 +283,7 @@ public extension ContainedViewLayoutTransition { } else { switch self { case .immediate: + node.layer.removeAnimation(forKey: "bounds") node.bounds = bounds if let completion = completion { completion(true) @@ -304,6 +311,7 @@ public extension ContainedViewLayoutTransition { } else { switch self { case .immediate: + layer.removeAnimation(forKey: "bounds") layer.bounds = bounds if let completion = completion { completion(true) @@ -326,6 +334,7 @@ public extension ContainedViewLayoutTransition { } else { switch self { case .immediate: + node.layer.removeAnimation(forKey: "position") node.position = position if let completion = completion { completion(true) @@ -353,6 +362,7 @@ public extension ContainedViewLayoutTransition { } else { switch self { case .immediate: + layer.removeAnimation(forKey: "position") layer.position = position if let completion = completion { completion(true) @@ -615,6 +625,8 @@ public extension ContainedViewLayoutTransition { } else { switch self { case .immediate: + view.layer.removeAnimation(forKey: "position") + view.layer.removeAnimation(forKey: "bounds") view.frame = frame if let completion = completion { completion(true) @@ -642,6 +654,8 @@ public extension ContainedViewLayoutTransition { } else { switch self { case .immediate: + layer.removeAnimation(forKey: "position") + layer.removeAnimation(forKey: "bounds") layer.frame = frame if let completion = completion { completion(true) @@ -790,6 +804,7 @@ public extension ContainedViewLayoutTransition { switch self { case .immediate: + node.layer.removeAnimation(forKey: "cornerRadius") node.cornerRadius = cornerRadius if let completion = completion { completion(true) @@ -815,6 +830,7 @@ public extension ContainedViewLayoutTransition { switch self { case .immediate: + layer.removeAnimation(forKey: "cornerRadius") layer.cornerRadius = cornerRadius if let completion = completion { completion(true) @@ -1084,6 +1100,7 @@ public extension ContainedViewLayoutTransition { switch self { case .immediate: + node.layer.removeAnimation(forKey: "sublayerTransform") node.layer.sublayerTransform = CATransform3DMakeScale(scale, scale, 1.0) if let completion = completion { completion(true) @@ -1116,6 +1133,7 @@ public extension ContainedViewLayoutTransition { switch self { case .immediate: + node.layer.removeAnimation(forKey: "sublayerTransform") node.layer.sublayerTransform = CATransform3DMakeScale(scale, scale, 1.0) if let completion = completion { completion(true) @@ -1153,6 +1171,7 @@ public extension ContainedViewLayoutTransition { switch self { case .immediate: + node.layer.removeAnimation(forKey: "sublayerTransform") node.layer.sublayerTransform = transform if let completion = completion { completion(true) @@ -1200,6 +1219,7 @@ public extension ContainedViewLayoutTransition { switch self { case .immediate: + layer.removeAnimation(forKey: "sublayerTransform") layer.sublayerTransform = CATransform3DMakeScale(scale.x, scale.y, 1.0) if let completion = completion { completion(true) @@ -1248,6 +1268,7 @@ public extension ContainedViewLayoutTransition { switch self { case .immediate: + layer.removeAnimation(forKey: "transform") layer.transform = CATransform3DMakeScale(scale.x, scale.y, 1.0) if let completion = completion { completion(true) @@ -1275,6 +1296,7 @@ public extension ContainedViewLayoutTransition { switch self { case .immediate: + layer.removeAnimation(forKey: "sublayerTransform") layer.sublayerTransform = CATransform3DMakeTranslation(offset.x, offset.y, 0.0) if let completion = completion { completion(true) diff --git a/submodules/Display/Source/GridNodeScroller.swift b/submodules/Display/Source/GridNodeScroller.swift index 22a778f6e8..dc884cf79c 100644 --- a/submodules/Display/Source/GridNodeScroller.swift +++ b/submodules/Display/Source/GridNodeScroller.swift @@ -6,12 +6,12 @@ private class GridNodeScrollerLayer: CALayer { } } -private class GridNodeScrollerView: UIScrollView { - override class var layerClass: AnyClass { +public class GridNodeScrollerView: UIScrollView { + override public class var layerClass: AnyClass { return GridNodeScrollerLayer.self } - override init(frame: CGRect) { + override public init(frame: CGRect) { super.init(frame: frame) if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { @@ -19,15 +19,15 @@ private class GridNodeScrollerView: UIScrollView { } } - required init?(coder aDecoder: NSCoder) { + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - override func touchesShouldCancel(in view: UIView) -> Bool { + override public func touchesShouldCancel(in view: UIView) -> Bool { return true } - @objc func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + @objc private func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return false } } diff --git a/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift b/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift index 4b03fb865c..f92e1ef1f4 100644 --- a/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift +++ b/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift @@ -605,7 +605,7 @@ private func loadItem(path: String) -> AnimationCacheItem? { } let decompressedSize = readUInt32(data: compressedData, offset: 0) - if decompressedSize <= 0 || decompressedSize > 20 * 1024 * 1024 { + if decompressedSize <= 0 || decompressedSize > 40 * 1024 * 1024 { return nil } guard let data = decompressData(data: compressedData, range: 4 ..< compressedData.count, decompressedSize: Int(decompressedSize)) else { diff --git a/submodules/TelegramUI/Components/ChatInputPanelContainer/BUILD b/submodules/TelegramUI/Components/ChatInputPanelContainer/BUILD index 7813794065..3ae830cb24 100644 --- a/submodules/TelegramUI/Components/ChatInputPanelContainer/BUILD +++ b/submodules/TelegramUI/Components/ChatInputPanelContainer/BUILD @@ -13,6 +13,7 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", + "//submodules/Components/PagerComponent:PagerComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/ChatInputPanelContainer/Sources/ChatInputPanelContainer.swift b/submodules/TelegramUI/Components/ChatInputPanelContainer/Sources/ChatInputPanelContainer.swift index 6b2a4df214..3ecfc8b2e8 100644 --- a/submodules/TelegramUI/Components/ChatInputPanelContainer/Sources/ChatInputPanelContainer.swift +++ b/submodules/TelegramUI/Components/ChatInputPanelContainer/Sources/ChatInputPanelContainer.swift @@ -2,37 +2,172 @@ import Foundation import UIKit import Display import AsyncDisplayKit +import PagerComponent -private final class ExpansionPanRecognizer: UIPanGestureRecognizer, UIGestureRecognizerDelegate { - private var targetScrollView: UIScrollView? +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) { + 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{ + 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) +} + +private final class ExpansionPanRecognizer: UIGestureRecognizer, UIGestureRecognizerDelegate { + enum LockDirection { + case up + case down + } - override init(target: Any?, action: Selector?) { + var requiredLockDirection: LockDirection = .up + + private var beginPosition = CGPoint() + private var currentTranslation = CGPoint() + + override public init(target: Any?, action: Selector?) { super.init(target: target, action: action) self.delegate = self } - override func reset() { - self.targetScrollView = nil + override public func reset() { + super.reset() + + self.state = .possible + self.currentTranslation = CGPoint() } - func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { - /*if let scrollView = otherGestureRecognizer.view as? UIScrollView { - if scrollView.bounds.height > 200.0 { - self.targetScrollView = scrollView - scrollView.contentOffset = CGPoint() - } - }*/ + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if let _ = otherGestureRecognizer.view as? PagerExpandableScrollView { + return true + } + if let _ = gestureRecognizer as? PagerPanGestureRecognizer { + return true + } + + return true + } + + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if let _ = otherGestureRecognizer.view as? PagerExpandableScrollView { + return true + } + if otherGestureRecognizer is UIPanGestureRecognizer { + return true + } return false } - override func touchesMoved(_ touches: Set, with event: UIEvent) { + override public func touchesBegan(_ touches: Set, with event: UIEvent) { + super.touchesBegan(touches, with: event) + + guard let touch = touches.first, let view = self.view else { + self.state = .failed + return + } + + 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) { + found = true + } + } + if found { + self.beginPosition = point + } else { + self.state = .failed + } + } + + override public func touchesMoved(_ touches: Set, with event: UIEvent) { super.touchesMoved(touches, with: event) - if let targetScrollView = self.targetScrollView { - targetScrollView.contentOffset = CGPoint() + guard let touch = touches.first, let view = self.view else { + self.state = .failed + return } + + let point = touch.location(in: self.view) + + let translation = CGPoint(x: point.x - self.beginPosition.x, y: point.y - self.beginPosition.y) + self.currentTranslation = translation + + if self.state == .possible { + if abs(translation.x) > 8.0 { + self.state = .failed + return + } + var lockDirection: LockDirection? + let point = touch.location(in: self.view) + 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 let lockDirection = lockDirection { + if abs(translation.y) > 2.0 { + switch lockDirection { + case .up: + if translation.y < 0.0 { + self.state = .began + } else { + self.state = .failed + } + case .down: + if translation.y > 0.0 { + self.state = .began + } else { + self.state = .failed + } + } + } + } else { + self.state = .failed + } + } else { + self.state = .changed + } + } + + override public func touchesEnded(_ touches: Set, with event: UIEvent) { + super.touchesEnded(touches, with: event) + + self.state = .ended + } + + override public func touchesCancelled(_ touches: Set, with event: UIEvent) { + super.touchesCancelled(touches, with: event) + + self.state = .cancelled + } + + func translation() -> CGPoint { + return self.currentTranslation + } + + func velocity() -> CGPoint { + return CGPoint() } } @@ -66,7 +201,7 @@ public final class ChatInputPanelContainer: SparseNode, UIScrollViewDelegate { return } - let delta = -recognizer.translation(in: self.view).y / scrollableDistance + let delta = -recognizer.translation().y / scrollableDistance self.expansionFraction = max(0.0, min(1.0, self.initialExpansionFraction + delta)) self.expansionUpdated?(.immediate) @@ -75,7 +210,7 @@ public final class ChatInputPanelContainer: SparseNode, UIScrollViewDelegate { return } - let velocity = recognizer.velocity(in: self.view) + let velocity = recognizer.velocity() if abs(self.initialExpansionFraction - self.expansionFraction) > 0.25 { if self.initialExpansionFraction < 0.5 { self.expansionFraction = 1.0 @@ -95,6 +230,11 @@ public final class ChatInputPanelContainer: SparseNode, UIScrollViewDelegate { self.expansionFraction = 1.0 } } + + if let expansionRecognizer = self.expansionRecognizer { + expansionRecognizer.requiredLockDirection = self.expansionFraction == 0.0 ? .up : .down + } + self.expansionUpdated?(.animated(duration: 0.4, curve: .spring)) default: break @@ -107,7 +247,13 @@ public final class ChatInputPanelContainer: SparseNode, UIScrollViewDelegate { self.scrollableDistance = scrollableDistance } + public func expand() { + self.expansionFraction = 1.0 + self.expansionRecognizer?.requiredLockDirection = self.expansionFraction == 0.0 ? .up : .down + } + public func collapse() { self.expansionFraction = 0.0 + self.expansionRecognizer?.requiredLockDirection = self.expansionFraction == 0.0 ? .up : .down } } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index 132efd729c..12a0905961 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -25,14 +25,14 @@ import UndoUI private let premiumBadgeIcon: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat List/PeerPremiumIcon"), color: .white) -private final class PremiumBadgeView: BlurredBackgroundView { +private final class PremiumBadgeView: UIView { private let iconLayer: SimpleLayer init() { self.iconLayer = SimpleLayer() self.iconLayer.contents = premiumBadgeIcon?.cgImage - super.init(color: .clear, enableBlur: true) + super.init(frame: CGRect()) self.layer.addSublayer(self.iconLayer) } @@ -42,11 +42,13 @@ private final class PremiumBadgeView: BlurredBackgroundView { } func update(backgroundColor: UIColor, size: CGSize) { - self.updateColor(color: backgroundColor, transition: .immediate) + //self.updateColor(color: backgroundColor, transition: .immediate) + self.backgroundColor = backgroundColor + self.layer.cornerRadius = size.width / 2.0 self.iconLayer.frame = CGRect(origin: CGPoint(), size: size).insetBy(dx: 2.0, dy: 2.0) - super.update(size: size, cornerRadius: min(size.width / 2.0, size.height / 2.0), transition: .immediate) + //super.update(size: size, cornerRadius: min(size.width / 2.0, size.height / 2.0), transition: .immediate) } } @@ -150,6 +152,7 @@ public final class EmojiPagerContentComponent: Component { case detailed } + public let id: AnyHashable public let context: AccountContext public let animationCache: AnimationCache public let animationRenderer: MultiAnimationRenderer @@ -158,6 +161,7 @@ public final class EmojiPagerContentComponent: Component { public let itemLayoutType: ItemLayoutType public init( + id: AnyHashable, context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, @@ -165,6 +169,7 @@ public final class EmojiPagerContentComponent: Component { itemGroups: [ItemGroup], itemLayoutType: ItemLayoutType ) { + self.id = id self.context = context self.animationCache = animationCache self.animationRenderer = animationRenderer @@ -174,6 +179,9 @@ public final class EmojiPagerContentComponent: Component { } public static func ==(lhs: EmojiPagerContentComponent, rhs: EmojiPagerContentComponent) -> Bool { + if lhs.id != rhs.id { + return false + } if lhs.context !== rhs.context { return false } @@ -196,14 +204,24 @@ public final class EmojiPagerContentComponent: Component { return true } - public final class View: UIView, UIScrollViewDelegate { + public final class Tag { + public let id: AnyHashable + + public init(id: AnyHashable) { + self.id = id + } + } + + public final class View: UIView, UIScrollViewDelegate, ComponentTaggedView { private struct ItemGroupDescription: Equatable { + let id: AnyHashable let hasTitle: Bool let itemCount: Int } private struct ItemGroupLayout: Equatable { let frame: CGRect + let id: AnyHashable let itemTopOffset: CGFloat let itemCount: Int } @@ -230,9 +248,9 @@ public final class EmojiPagerContentComponent: Component { self.verticalSpacing = 9.0 minSpacing = 9.0 case .detailed: - self.itemSize = 60.0 - self.verticalSpacing = 9.0 - minSpacing = 9.0 + self.itemSize = 76.0 + self.verticalSpacing = 2.0 + minSpacing = 2.0 } self.verticalGroupSpacing = 18.0 @@ -254,6 +272,7 @@ public final class EmojiPagerContentComponent: Component { let groupContentSize = CGSize(width: width, height: itemTopOffset + CGFloat(numRowsInGroup) * self.itemSize + 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, itemTopOffset: itemTopOffset, itemCount: itemGroup.itemCount )) @@ -281,8 +300,8 @@ public final class EmojiPagerContentComponent: Component { ) } - func visibleItems(for rect: CGRect) -> [(groupIndex: Int, groupItems: Range)] { - var result: [(groupIndex: Int, groupItems: Range)] = [] + func visibleItems(for rect: CGRect) -> [(id: AnyHashable, groupIndex: Int, groupItems: Range)] { + var result: [(id: AnyHashable, groupIndex: Int, groupItems: Range)] = [] for groupIndex in 0 ..< self.itemGroupLayouts.count { let group = self.itemGroupLayouts[groupIndex] @@ -300,6 +319,7 @@ public final class EmojiPagerContentComponent: Component { if maxVisibleIndex >= minVisibleIndex { result.append(( + id: group.id, groupIndex: groupIndex, groupItems: minVisibleIndex ..< (maxVisibleIndex + 1) )) @@ -492,15 +512,19 @@ public final class EmojiPagerContentComponent: Component { } } - private let scrollView: UIScrollView + private final class ContentScrollView: UIScrollView, PagerExpandableScrollView { + } + + private let scrollView: ContentScrollView private var visibleItemLayers: [ItemLayer.Key: ItemLayer] = [:] - private var visibleGroupHeaders: [AnyHashable: ComponentHostView] = [:] + private var visibleGroupHeaders: [AnyHashable: ComponentView] = [:] private var ignoreScrolling: Bool = false private var component: EmojiPagerContentComponent? private var pagerEnvironment: PagerComponentChildEnvironment? private var theme: PresentationTheme? + private var activeItemUpdated: ActionSlot<(AnyHashable, Transition)>? private var itemLayout: ItemLayout? private var currentContextGestureItemKey: ItemLayer.Key? @@ -508,7 +532,7 @@ public final class EmojiPagerContentComponent: Component { private weak var peekController: PeekController? override init(frame: CGRect) { - self.scrollView = UIScrollView() + self.scrollView = ContentScrollView() super.init(frame: frame) @@ -703,6 +727,31 @@ public final class EmojiPagerContentComponent: Component { fatalError("init(coder:) has not been implemented") } + public func matches(tag: Any) -> Bool { + if let tag = tag as? Tag { + if tag.id == self.component?.id { + return true + } + } + return false + } + + public func scrollToItemGroup(groupId: AnyHashable) { + guard let itemLayout = self.itemLayout else { + return + } + for group in itemLayout.itemGroupLayouts { + if group.id == groupId { + let wasIgnoringScrollingEvents = self.ignoreScrolling + self.ignoreScrolling = true + 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) + } + } + } + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { if let component = self.component, let (item, itemKey) = self.item(atPoint: recognizer.location(in: self)), let itemLayer = self.visibleItemLayers[itemKey] { @@ -723,7 +772,12 @@ public final class EmojiPagerContentComponent: Component { return nil } - private var previousScrollingOffset: CGFloat? + private struct ScrollingOffsetState: Equatable { + var value: CGFloat + var isDraggingOrDecelerating: Bool + } + + private var previousScrollingOffset: ScrollingOffsetState? public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { if let presentation = scrollView.layer.presentation() { @@ -759,21 +813,22 @@ public final class EmojiPagerContentComponent: Component { } private func updateScrollingOffset(transition: Transition) { + let isInteracting = scrollView.isDragging || scrollView.isDecelerating if let previousScrollingOffsetValue = self.previousScrollingOffset { let currentBounds = scrollView.bounds let offsetToTopEdge = max(0.0, currentBounds.minY - 0.0) let offsetToBottomEdge = max(0.0, scrollView.contentSize.height - currentBounds.maxY) - let offsetToClosestEdge = min(offsetToTopEdge, offsetToBottomEdge) - let relativeOffset = scrollView.contentOffset.y - previousScrollingOffsetValue + let relativeOffset = scrollView.contentOffset.y - previousScrollingOffsetValue.value self.pagerEnvironment?.onChildScrollingUpdate(PagerComponentChildEnvironment.ContentScrollingUpdate( relativeOffset: relativeOffset, - absoluteOffsetToClosestEdge: offsetToClosestEdge, + absoluteOffsetToTopEdge: offsetToTopEdge, + absoluteOffsetToBottomEdge: offsetToBottomEdge, + isInteracting: isInteracting, transition: transition )) - self.previousScrollingOffset = scrollView.contentOffset.y } - self.previousScrollingOffset = scrollView.contentOffset.y + self.previousScrollingOffset = ScrollingOffsetState(value: scrollView.contentOffset.y, isDraggingOrDecelerating: isInteracting) } private func snappedContentOffset(proposedOffset: CGFloat) -> CGFloat { @@ -808,22 +863,27 @@ public final class EmojiPagerContentComponent: Component { return } + var topVisibleGroupId: AnyHashable? + var validIds = Set() var validGroupHeaderIds = Set() for groupItems in itemLayout.visibleItems(for: self.scrollView.bounds) { + if topVisibleGroupId == nil { + topVisibleGroupId = groupItems.id + } + let itemGroup = component.itemGroups[groupItems.groupIndex] let itemGroupLayout = itemLayout.itemGroupLayouts[groupItems.groupIndex] if let title = itemGroup.title { validGroupHeaderIds.insert(itemGroup.id) - let groupHeaderView: ComponentHostView + let groupHeaderView: ComponentView if let current = self.visibleGroupHeaders[itemGroup.id] { groupHeaderView = current } else { - groupHeaderView = ComponentHostView() + groupHeaderView = ComponentView() self.visibleGroupHeaders[itemGroup.id] = groupHeaderView - self.scrollView.addSubview(groupHeaderView) } let groupHeaderSize = groupHeaderView.update( transition: .immediate, @@ -833,7 +893,12 @@ public final class EmojiPagerContentComponent: Component { environment: {}, containerSize: CGSize(width: itemLayout.contentSize.width - itemLayout.containerInsets.left - itemLayout.containerInsets.right, height: 100.0) ) - groupHeaderView.frame = CGRect(origin: CGPoint(x: itemLayout.containerInsets.left, y: itemGroupLayout.frame.minY + 1.0), size: groupHeaderSize) + if let view = groupHeaderView.view { + if view.superview == nil { + self.scrollView.addSubview(view) + } + view.frame = CGRect(origin: CGPoint(x: itemLayout.containerInsets.left, y: itemGroupLayout.frame.minY + 1.0), size: groupHeaderSize) + } } for index in groupItems.groupItems.lowerBound ..< groupItems.groupItems.upperBound { @@ -848,7 +913,7 @@ public final class EmojiPagerContentComponent: Component { itemLayer = ItemLayer( item: item, context: component.context, - groupId: "keyboard", + groupId: "keyboard-\(Int(itemLayout.itemSize))", attemptSynchronousLoad: attemptSynchronousLoads, file: item.file, cache: component.animationCache, @@ -884,17 +949,22 @@ public final class EmojiPagerContentComponent: Component { for (id, groupHeaderView) in self.visibleGroupHeaders { if !validGroupHeaderIds.contains(id) { removedGroupHeaderIds.append(id) - groupHeaderView.removeFromSuperview() + groupHeaderView.view?.removeFromSuperview() } } for id in removedGroupHeaderIds { self.visibleGroupHeaders.removeValue(forKey: id) } + + if let topVisibleGroupId = topVisibleGroupId { + self.activeItemUpdated?.invoke((topVisibleGroupId, .immediate)) + } } 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 pagerEnvironment = environment[PagerComponentChildEnvironment.self].value self.pagerEnvironment = pagerEnvironment @@ -902,6 +972,7 @@ public final class EmojiPagerContentComponent: Component { var itemGroups: [ItemGroupDescription] = [] for itemGroup in component.itemGroups { itemGroups.append(ItemGroupDescription( + id: itemGroup.id, hasTitle: itemGroup.title != nil, itemCount: itemGroup.items.count )) @@ -918,7 +989,7 @@ public final class EmojiPagerContentComponent: Component { if self.scrollView.scrollIndicatorInsets != pagerEnvironment.containerInsets { self.scrollView.scrollIndicatorInsets = pagerEnvironment.containerInsets } - self.previousScrollingOffset = self.scrollView.contentOffset.y + self.previousScrollingOffset = ScrollingOffsetState(value: scrollView.contentOffset.y, isDraggingOrDecelerating: scrollView.isDragging || scrollView.isDecelerating) self.ignoreScrolling = false self.updateVisibleItems(attemptSynchronousLoads: true) diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift index 5fbe61559c..0e458561ea 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift @@ -11,9 +11,14 @@ import BundleIconComponent public final class EntityKeyboardChildEnvironment: Equatable { public let theme: PresentationTheme + public let getContentActiveItemUpdated: (AnyHashable) -> ActionSlot<(AnyHashable, Transition)>? - public init(theme: PresentationTheme) { + public init( + theme: PresentationTheme, + getContentActiveItemUpdated: @escaping (AnyHashable) -> ActionSlot<(AnyHashable, Transition)>? + ) { self.theme = theme + self.getContentActiveItemUpdated = getContentActiveItemUpdated } public static func ==(lhs: EntityKeyboardChildEnvironment, rhs: EntityKeyboardChildEnvironment) -> Bool { @@ -25,7 +30,17 @@ public final class EntityKeyboardChildEnvironment: Equatable { } } +public enum EntitySearchContentType { + case stickers + case gifs +} + public final class EntityKeyboardComponent: Component { + public final class MarkInputCollapsed { + public init() { + } + } + public let theme: PresentationTheme public let bottomInset: CGFloat public let emojiContent: EmojiPagerContentComponent @@ -34,6 +49,9 @@ public final class EntityKeyboardComponent: Component { public let defaultToEmojiTab: Bool public let externalTopPanelContainer: UIView? public let topPanelExtensionUpdated: (CGFloat, Transition) -> Void + public let hideInputUpdated: (Bool, Transition) -> Void + public let makeSearchContainerNode: (EntitySearchContentType) -> EntitySearchContainerNode + public let deviceMetrics: DeviceMetrics public init( theme: PresentationTheme, @@ -43,7 +61,10 @@ public final class EntityKeyboardComponent: Component { gifContent: GifPagerContentComponent, defaultToEmojiTab: Bool, externalTopPanelContainer: UIView?, - topPanelExtensionUpdated: @escaping (CGFloat, Transition) -> Void + topPanelExtensionUpdated: @escaping (CGFloat, Transition) -> Void, + hideInputUpdated: @escaping (Bool, Transition) -> Void, + makeSearchContainerNode: @escaping (EntitySearchContentType) -> EntitySearchContainerNode, + deviceMetrics: DeviceMetrics ) { self.theme = theme self.bottomInset = bottomInset @@ -53,6 +74,9 @@ public final class EntityKeyboardComponent: Component { self.defaultToEmojiTab = defaultToEmojiTab self.externalTopPanelContainer = externalTopPanelContainer self.topPanelExtensionUpdated = topPanelExtensionUpdated + self.hideInputUpdated = hideInputUpdated + self.makeSearchContainerNode = makeSearchContainerNode + self.deviceMetrics = deviceMetrics } public static func ==(lhs: EntityKeyboardComponent, rhs: EntityKeyboardComponent) -> Bool { @@ -77,6 +101,9 @@ public final class EntityKeyboardComponent: Component { if lhs.externalTopPanelContainer != rhs.externalTopPanelContainer { return false } + if lhs.deviceMetrics != rhs.deviceMetrics { + return false + } return true } @@ -85,6 +112,12 @@ public final class EntityKeyboardComponent: Component { private let pagerView: ComponentHostView private var component: EntityKeyboardComponent? + private weak var state: EmptyComponentState? + + private var searchView: ComponentHostView? + private var searchComponent: EntitySearchContentComponent? + + private var topPanelExtension: CGFloat? override init(frame: CGRect) { self.pagerView = ComponentHostView() @@ -92,6 +125,7 @@ public final class EntityKeyboardComponent: Component { super.init(frame: frame) self.clipsToBounds = true + self.disablesInteractiveTransitionGestureRecognizer = true self.addSubview(self.pagerView) } @@ -101,11 +135,15 @@ public final class EntityKeyboardComponent: Component { } func update(component: EntityKeyboardComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.state = state + var contents: [AnyComponentWithIdentity<(EntityKeyboardChildEnvironment, PagerComponentChildEnvironment)>] = [] - var contentTopPanels: [AnyComponentWithIdentity] = [] + var contentTopPanels: [AnyComponentWithIdentity] = [] var contentIcons: [AnyComponentWithIdentity] = [] + var contentAccessoryLeftButtons: [AnyComponentWithIdentity] = [] var contentAccessoryRightButtons: [AnyComponentWithIdentity] = [] + let gifsContentItemIdUpdated = ActionSlot<(AnyHashable, Transition)>() contents.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(component.gifContent))) var topGifItems: [EntityKeyboardTopPanelComponent.Item] = [] topGifItems.append(EntityKeyboardTopPanelComponent.Item( @@ -126,13 +164,24 @@ public final class EntityKeyboardComponent: Component { )) contentTopPanels.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(EntityKeyboardTopPanelComponent( theme: component.theme, - items: topGifItems + items: topGifItems, + activeContentItemIdUpdated: gifsContentItemIdUpdated )))) contentIcons.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(BundleIconComponent( name: "Chat/Input/Media/EntityInputGifsIcon", tintColor: component.theme.chat.inputMediaPanel.panelIconColor, maxSize: nil )))) + contentAccessoryLeftButtons.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(Button( + content: AnyComponent(BundleIconComponent( + name: "Chat/Input/Media/EntityInputSearchIcon", + tintColor: component.theme.chat.inputMediaPanel.panelIconColor, + maxSize: nil + )), + action: { [weak self] in + self?.openSearch() + } + ).minSize(CGSize(width: 38.0, height: 38.0))))) /*contentAccessoryRightButtons.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(Button( content: AnyComponent(BundleIconComponent( name: "Chat/Input/Media/EntityInputSettingsIcon", @@ -152,38 +201,59 @@ public final class EntityKeyboardComponent: Component { ] if let iconName = iconMapping[id] { topStickerItems.append(EntityKeyboardTopPanelComponent.Item( - id: id, - content: AnyComponent(BundleIconComponent( - name: iconName, - tintColor: component.theme.chat.inputMediaPanel.panelIconColor, - maxSize: CGSize(width: 30.0, height: 30.0)) + 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 + self?.scrollToItemGroup(contentId: "stickers", groupId: itemGroup.id) + } + ).minSize(CGSize(width: 30.0, height: 30.0)) ) )) } } else { if !itemGroup.items.isEmpty { topStickerItems.append(EntityKeyboardTopPanelComponent.Item( - id: AnyHashable(itemGroup.items[0].file.fileId), + id: itemGroup.id, content: AnyComponent(EntityKeyboardAnimationTopPanelComponent( context: component.stickerContent.context, file: itemGroup.items[0].file, animationCache: component.stickerContent.animationCache, - animationRenderer: component.stickerContent.animationRenderer + animationRenderer: component.stickerContent.animationRenderer, + pressed: { [weak self] in + self?.scrollToItemGroup(contentId: "stickers", groupId: itemGroup.id) + } )) )) } } } + let stickersContentItemIdUpdated = ActionSlot<(AnyHashable, Transition)>() contents.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(component.stickerContent))) contentTopPanels.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(EntityKeyboardTopPanelComponent( theme: component.theme, - items: topStickerItems + items: topStickerItems, + activeContentItemIdUpdated: stickersContentItemIdUpdated )))) contentIcons.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(BundleIconComponent( name: "Chat/Input/Media/EntityInputStickersIcon", tintColor: component.theme.chat.inputMediaPanel.panelIconColor, maxSize: nil )))) + contentAccessoryLeftButtons.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(Button( + content: AnyComponent(BundleIconComponent( + name: "Chat/Input/Media/EntityInputSearchIcon", + tintColor: component.theme.chat.inputMediaPanel.panelIconColor, + maxSize: nil + )), + action: { [weak self] in + self?.openSearch() + } + ).minSize(CGSize(width: 38.0, height: 38.0))))) contentAccessoryRightButtons.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(Button( content: AnyComponent(BundleIconComponent( name: "Chat/Input/Media/EntityInputSettingsIcon", @@ -195,24 +265,29 @@ public final class EntityKeyboardComponent: Component { } ).minSize(CGSize(width: 38.0, height: 38.0))))) + let emojiContentItemIdUpdated = ActionSlot<(AnyHashable, Transition)>() contents.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(component.emojiContent))) var topEmojiItems: [EntityKeyboardTopPanelComponent.Item] = [] for itemGroup in component.emojiContent.itemGroups { if !itemGroup.items.isEmpty { topEmojiItems.append(EntityKeyboardTopPanelComponent.Item( - id: AnyHashable(itemGroup.items[0].file.fileId), + id: itemGroup.id, content: AnyComponent(EntityKeyboardAnimationTopPanelComponent( context: component.emojiContent.context, file: itemGroup.items[0].file, animationCache: component.emojiContent.animationCache, - animationRenderer: component.emojiContent.animationRenderer + animationRenderer: component.emojiContent.animationRenderer, + pressed: { [weak self] in + self?.scrollToItemGroup(contentId: "emoji", groupId: itemGroup.id) + } )) )) } } contentTopPanels.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(EntityKeyboardTopPanelComponent( theme: component.theme, - items: topEmojiItems + items: topEmojiItems, + activeContentItemIdUpdated: emojiContentItemIdUpdated )))) contentIcons.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(BundleIconComponent( name: "Chat/Input/Media/EntityInputEmojiIcon", @@ -237,6 +312,7 @@ public final class EntityKeyboardComponent: Component { contents: contents, contentTopPanels: contentTopPanels, contentIcons: contentIcons, + contentAccessoryLeftButtons: contentAccessoryLeftButtons, contentAccessoryRightButtons: contentAccessoryRightButtons, defaultId: component.defaultToEmojiTab ? "emoji" : "stickers", contentBackground: AnyComponent(BlurredBackgroundComponent( @@ -253,21 +329,149 @@ public final class EntityKeyboardComponent: Component { self?.component?.emojiContent.inputInteraction.deleteBackwards() } )), - panelStateUpdated: { panelState, transition in - component.topPanelExtensionUpdated(panelState.topPanelHeight, transition) - } + panelStateUpdated: { [weak self] panelState, transition in + guard let strongSelf = self else { + return + } + strongSelf.topPanelExtensionUpdated(height: panelState.topPanelHeight, transition: transition) + }, + hidePanels: self.searchComponent != nil )), environment: { - EntityKeyboardChildEnvironment(theme: component.theme) + EntityKeyboardChildEnvironment( + theme: component.theme, + getContentActiveItemUpdated: { id in + if id == AnyHashable("gifs") { + return gifsContentItemIdUpdated + } else if id == AnyHashable("stickers") { + return stickersContentItemIdUpdated + } else if id == AnyHashable("emoji") { + return emojiContentItemIdUpdated + } + return nil + } + ) }, containerSize: availableSize ) transition.setFrame(view: self.pagerView, frame: CGRect(origin: CGPoint(), size: pagerSize)) + if transition.userData(MarkInputCollapsed.self) != nil { + self.searchComponent = nil + } + + if let searchComponent = self.searchComponent { + var animateIn = false + let searchView: ComponentHostView + var searchViewTransition = transition + if let current = self.searchView { + searchView = current + } else { + searchViewTransition = .immediate + searchView = ComponentHostView() + self.searchView = searchView + self.addSubview(searchView) + + animateIn = true + component.topPanelExtensionUpdated(0.0, transition) + } + + let _ = searchView.update( + transition: searchViewTransition, + component: AnyComponent(searchComponent), + environment: { + EntitySearchContentEnvironment( + context: component.stickerContent.context, + theme: component.theme, + deviceMetrics: component.deviceMetrics + ) + }, + containerSize: availableSize + ) + searchViewTransition.setFrame(view: searchView, frame: CGRect(origin: CGPoint(), size: availableSize)) + + if animateIn { + transition.animateAlpha(view: searchView, from: 0.0, to: 1.0) + transition.animatePosition(view: searchView, from: CGPoint(x: 0.0, y: self.topPanelExtension ?? 0.0), to: CGPoint(), additive: true, completion: nil) + } + } else { + if let searchView = self.searchView { + self.searchView = nil + + transition.setFrame(view: searchView, frame: CGRect(origin: CGPoint(x: 0.0, y: self.topPanelExtension ?? 0.0), size: availableSize)) + transition.setAlpha(view: searchView, alpha: 0.0, completion: { [weak searchView] _ in + searchView?.removeFromSuperview() + }) + + if let topPanelExtension = self.topPanelExtension { + component.topPanelExtensionUpdated(topPanelExtension, transition) + } + } + } + self.component = component return availableSize } + + private func topPanelExtensionUpdated(height: CGFloat, transition: Transition) { + guard let component = self.component else { + return + } + if self.topPanelExtension != height { + self.topPanelExtension = height + } + if self.searchComponent == nil { + component.topPanelExtensionUpdated(height, transition) + } + } + + private func openSearch() { + guard let component = self.component else { + return + } + if self.searchComponent != nil { + return + } + + if let pagerView = self.pagerView.findTaggedView(tag: PagerComponentViewTag()) as? PagerComponent.View, let centralId = pagerView.centralId { + let contentType: EntitySearchContentType + if centralId == AnyHashable("gifs") { + contentType = .gifs + } else { + contentType = .stickers + } + + self.searchComponent = EntitySearchContentComponent( + makeContainerNode: { + return component.makeSearchContainerNode(contentType) + }, + dismissSearch: { [weak self] in + self?.closeSearch() + } + ) + //self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) + component.hideInputUpdated(true, Transition(animation: .curve(duration: 0.3, curve: .spring))) + } + } + + private func closeSearch() { + guard let component = self.component else { + return + } + if self.searchComponent == nil { + return + } + 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))) + } + + private func scrollToItemGroup(contentId: String, groupId: AnyHashable) { + if let pagerView = self.pagerView.findTaggedView(tag: EmojiPagerContentComponent.Tag(id: contentId)) as? EmojiPagerContentComponent.View { + pagerView.scrollToItemGroup(groupId: groupId) + } + } } public func makeView() -> View { diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelComponent.swift index 16c2a40147..ff57b95ec9 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelComponent.swift @@ -81,7 +81,7 @@ private final class BottomPanelIconComponent: Component { } final class EntityKeyboardBottomPanelComponent: Component { - typealias EnvironmentType = PagerComponentPanelEnvironment + typealias EnvironmentType = PagerComponentPanelEnvironment let theme: PresentationTheme let bottomInset: CGFloat @@ -111,16 +111,19 @@ final class EntityKeyboardBottomPanelComponent: Component { final class View: UIView { private final class AccessoryButtonView { let id: AnyHashable + var component: AnyComponent let view: ComponentHostView - init(id: AnyHashable, view: ComponentHostView) { + init(id: AnyHashable, component: AnyComponent, view: ComponentHostView) { self.id = id + self.component = component self.view = view } } private let backgroundView: BlurredBackgroundView private let separatorView: UIView + private var leftAccessoryButton: AccessoryButtonView? private var rightAccessoryButton: AccessoryButtonView? private var iconViews: [AnyHashable: ComponentHostView] = [:] @@ -160,9 +163,61 @@ final class EntityKeyboardBottomPanelComponent: Component { let intrinsicHeight: CGFloat = 38.0 let height = intrinsicHeight + component.bottomInset - let panelEnvironment = environment[PagerComponentPanelEnvironment.self].value + let panelEnvironment = environment[PagerComponentPanelEnvironment.self].value let activeContentId = panelEnvironment.activeContentId + var leftAccessoryButtonComponent: AnyComponentWithIdentity? + for contentAccessoryLeftButton in panelEnvironment.contentAccessoryLeftButtons { + if contentAccessoryLeftButton.id == activeContentId { + leftAccessoryButtonComponent = contentAccessoryLeftButton + break + } + } + let previousLeftAccessoryButton = self.leftAccessoryButton + + if let leftAccessoryButtonComponent = leftAccessoryButtonComponent { + var leftAccessoryButtonTransition = transition + let leftAccessoryButton: AccessoryButtonView + if let current = self.leftAccessoryButton, (current.id == leftAccessoryButtonComponent.id || current.component == leftAccessoryButtonComponent.component) { + leftAccessoryButton = current + leftAccessoryButton.component = leftAccessoryButtonComponent.component + } else { + leftAccessoryButtonTransition = .immediate + leftAccessoryButton = AccessoryButtonView(id: leftAccessoryButtonComponent.id, component: leftAccessoryButtonComponent.component, view: ComponentHostView()) + self.leftAccessoryButton = leftAccessoryButton + self.addSubview(leftAccessoryButton.view) + } + + let leftAccessoryButtonSize = leftAccessoryButton.view.update( + transition: leftAccessoryButtonTransition, + component: leftAccessoryButtonComponent.component, + environment: {}, + containerSize: CGSize(width: .greatestFiniteMagnitude, height: intrinsicHeight) + ) + leftAccessoryButtonTransition.setFrame(view: leftAccessoryButton.view, frame: CGRect(origin: CGPoint(x: 2.0, y: 2.0), size: leftAccessoryButtonSize)) + } else { + self.leftAccessoryButton = nil + } + + if previousLeftAccessoryButton?.view !== self.leftAccessoryButton?.view { + if case .none = transition.animation { + previousLeftAccessoryButton?.view.removeFromSuperview() + } else { + if let previousLeftAccessoryButton = previousLeftAccessoryButton { + let previousLeftAccessoryButtonView = previousLeftAccessoryButton.view + previousLeftAccessoryButtonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + previousLeftAccessoryButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousLeftAccessoryButtonView] _ in + previousLeftAccessoryButtonView?.removeFromSuperview() + }) + } + + if let leftAccessoryButtonView = self.leftAccessoryButton?.view { + leftAccessoryButtonView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2) + leftAccessoryButtonView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + } + var rightAccessoryButtonComponent: AnyComponentWithIdentity? for contentAccessoryRightButton in panelEnvironment.contentAccessoryRightButtons { if contentAccessoryRightButton.id == activeContentId { @@ -175,11 +230,12 @@ final class EntityKeyboardBottomPanelComponent: Component { if let rightAccessoryButtonComponent = rightAccessoryButtonComponent { var rightAccessoryButtonTransition = transition let rightAccessoryButton: AccessoryButtonView - if let current = self.rightAccessoryButton, current.id == rightAccessoryButtonComponent.id { + if let current = self.rightAccessoryButton, (current.id == rightAccessoryButtonComponent.id || current.component == rightAccessoryButtonComponent.component) { rightAccessoryButton = current + current.component = rightAccessoryButtonComponent.component } else { rightAccessoryButtonTransition = .immediate - rightAccessoryButton = AccessoryButtonView(id: rightAccessoryButtonComponent.id, view: ComponentHostView()) + rightAccessoryButton = AccessoryButtonView(id: rightAccessoryButtonComponent.id, component: rightAccessoryButtonComponent.component, view: ComponentHostView()) self.rightAccessoryButton = rightAccessoryButton self.addSubview(rightAccessoryButton.view) } @@ -195,7 +251,7 @@ final class EntityKeyboardBottomPanelComponent: Component { self.rightAccessoryButton = nil } - if previousRightAccessoryButton !== self.rightAccessoryButton?.view { + if previousRightAccessoryButton?.view !== self.rightAccessoryButton?.view { if case .none = transition.animation { previousRightAccessoryButton?.view.removeFromSuperview() } else { diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopContainerPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopContainerPanelComponent.swift index 875bdd0156..e4a6fe549f 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopContainerPanelComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopContainerPanelComponent.swift @@ -7,8 +7,25 @@ import TelegramPresentationData import TelegramCore import Postbox +final class EntityKeyboardTopContainerPanelEnvironment: Equatable { + let visibilityFractionUpdated: ActionSlot<(CGFloat, Transition)> + + init( + visibilityFractionUpdated: ActionSlot<(CGFloat, Transition)> + ) { + self.visibilityFractionUpdated = visibilityFractionUpdated + } + + static func ==(lhs: EntityKeyboardTopContainerPanelEnvironment, rhs: EntityKeyboardTopContainerPanelEnvironment) -> Bool { + if lhs.visibilityFractionUpdated !== rhs.visibilityFractionUpdated { + return false + } + return true + } +} + final class EntityKeyboardTopContainerPanelComponent: Component { - typealias EnvironmentType = PagerComponentPanelEnvironment + typealias EnvironmentType = PagerComponentPanelEnvironment let theme: PresentationTheme @@ -26,13 +43,20 @@ final class EntityKeyboardTopContainerPanelComponent: Component { return true } + private final class PanelView { + let view = ComponentHostView() + let visibilityFractionUpdated = ActionSlot<(CGFloat, Transition)>() + } + final class View: UIView { - private var panelViews: [AnyHashable: ComponentHostView] = [:] + private var panelViews: [AnyHashable: PanelView] = [:] private var component: EntityKeyboardTopContainerPanelComponent? - private var panelEnvironment: PagerComponentPanelEnvironment? + private var panelEnvironment: PagerComponentPanelEnvironment? private weak var state: EmptyComponentState? + private var visibilityFraction: CGFloat = 1.0 + override init(frame: CGRect) { super.init(frame: frame) } @@ -84,26 +108,30 @@ final class EntityKeyboardTopContainerPanelComponent: Component { validPanelIds.insert(panel.id) var panelTransition = transition - let panelView: ComponentHostView + let panelView: PanelView if let current = self.panelViews[panel.id] { panelView = current } else { panelTransition = .immediate - panelView = ComponentHostView() + panelView = PanelView() self.panelViews[panel.id] = panelView - self.addSubview(panelView) + self.addSubview(panelView.view) } - let _ = panelView.update( + let _ = panelView.view.update( transition: panelTransition, component: panel.component, - environment: {}, + environment: { + EntityKeyboardTopContainerPanelEnvironment( + visibilityFractionUpdated: panelView.visibilityFractionUpdated + ) + }, containerSize: panelFrame.size ) if isInBounds { - transition.animatePosition(view: panelView, from: CGPoint(x: transitionOffsetFraction * availableSize.width, y: 0.0), to: CGPoint(), additive: true, completion: nil) + transition.animatePosition(view: panelView.view, from: CGPoint(x: transitionOffsetFraction * availableSize.width, y: 0.0), to: CGPoint(), additive: true, completion: nil) } - panelTransition.setFrame(view: panelView, frame: panelFrame, completion: { [weak self] completed in + panelTransition.setFrame(view: panelView.view, frame: panelFrame, completion: { [weak self] completed in if isPartOfTransition && completed { self?.state?.updated(transition: .immediate) } @@ -115,15 +143,34 @@ final class EntityKeyboardTopContainerPanelComponent: Component { for (id, panelView) in self.panelViews { if !validPanelIds.contains(id) { removedPanelIds.append(id) - panelView.removeFromSuperview() + panelView.view.removeFromSuperview() } } for id in removedPanelIds { self.panelViews.removeValue(forKey: id) } + environment[PagerComponentPanelEnvironment.self].value.visibilityFractionUpdated.connect { [weak self] (fraction, transition) in + guard let strongSelf = self else { + return + } + strongSelf.updateVisibilityFraction(value: fraction, transition: transition) + } + return CGSize(width: availableSize.width, height: height) } + + private func updateVisibilityFraction(value: CGFloat, transition: Transition) { + if self.visibilityFraction == value { + return + } + + self.visibilityFraction = value + for (_, panelView) in self.panelViews { + panelView.visibilityFractionUpdated.invoke((value, transition)) + transition.setSublayerTransform(view: panelView.view, transform: CATransform3DMakeTranslation(0.0, -panelView.view.bounds.height / 2.0 * (1.0 - value), 0.0)) + } + } } func makeView() -> View { diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift index 69ee408867..1dbf32cdb8 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift @@ -17,17 +17,20 @@ final class EntityKeyboardAnimationTopPanelComponent: Component { let file: TelegramMediaFile let animationCache: AnimationCache let animationRenderer: MultiAnimationRenderer + let pressed: () -> Void init( context: AccountContext, file: TelegramMediaFile, animationCache: AnimationCache, - animationRenderer: MultiAnimationRenderer + animationRenderer: MultiAnimationRenderer, + pressed: @escaping () -> Void ) { self.context = context self.file = file self.animationCache = animationCache self.animationRenderer = animationRenderer + self.pressed = pressed } static func ==(lhs: EntityKeyboardAnimationTopPanelComponent, rhs: EntityKeyboardAnimationTopPanelComponent) -> Bool { @@ -49,16 +52,27 @@ final class EntityKeyboardAnimationTopPanelComponent: Component { final class View: UIView { var itemLayer: EmojiPagerContentComponent.View.ItemLayer? + var component: EntityKeyboardAnimationTopPanelComponent? override init(frame: CGRect) { super.init(frame: frame) + + 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: EntityKeyboardAnimationTopPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + if self.itemLayer == nil { let itemLayer = EmojiPagerContentComponent.View.ItemLayer( item: EmojiPagerContentComponent.Item( @@ -97,7 +111,7 @@ final class EntityKeyboardAnimationTopPanelComponent: Component { } final class EntityKeyboardTopPanelComponent: Component { - typealias EnvironmentType = Empty + typealias EnvironmentType = EntityKeyboardTopContainerPanelEnvironment final class Item: Equatable { let id: AnyHashable @@ -122,13 +136,16 @@ final class EntityKeyboardTopPanelComponent: Component { let theme: PresentationTheme let items: [Item] + let activeContentItemIdUpdated: ActionSlot<(AnyHashable, Transition)> init( theme: PresentationTheme, - items: [Item] + items: [Item], + activeContentItemIdUpdated: ActionSlot<(AnyHashable, Transition)> ) { self.theme = theme self.items = items + self.activeContentItemIdUpdated = activeContentItemIdUpdated } static func ==(lhs: EntityKeyboardTopPanelComponent, rhs: EntityKeyboardTopPanelComponent) -> Bool { @@ -138,6 +155,9 @@ final class EntityKeyboardTopPanelComponent: Component { if lhs.items != rhs.items { return false } + if lhs.activeContentItemIdUpdated !== rhs.activeContentItemIdUpdated { + return false + } return true } @@ -188,6 +208,8 @@ final class EntityKeyboardTopPanelComponent: Component { private var itemLayout: ItemLayout? private var ignoreScrolling: Bool = false + private var visibilityFraction: CGFloat = 1.0 + private var component: EntityKeyboardTopPanelComponent? override init(frame: CGRect) { @@ -296,16 +318,63 @@ final class EntityKeyboardTopPanelComponent: Component { } self.ignoreScrolling = false - if let _ = component.items.first { - self.highlightedIconBackgroundView.isHidden = false - let itemFrame = itemLayout.containerFrame(at: 0) - transition.setFrame(view: self.highlightedIconBackgroundView, frame: itemFrame) - } - self.updateVisibleItems(attemptSynchronousLoads: true) + environment[EntityKeyboardTopContainerPanelEnvironment.self].value.visibilityFractionUpdated.connect { [weak self] (fraction, transition) in + guard let strongSelf = self else { + return + } + strongSelf.visibilityFractionUpdated(value: fraction, transition: transition) + } + + component.activeContentItemIdUpdated.connect { [weak self] (itemId, transition) in + guard let strongSelf = self else { + return + } + strongSelf.activeContentItemIdUpdated(itemId: itemId, transition: transition) + } + return CGSize(width: availableSize.width, height: height) } + + private func visibilityFractionUpdated(value: CGFloat, transition: Transition) { + if self.visibilityFraction == value { + return + } + + self.visibilityFraction = value + + let scale = max(0.01, self.visibilityFraction) + + transition.setScale(view: self.highlightedIconBackgroundView, scale: scale) + transition.setAlpha(view: self.highlightedIconBackgroundView, alpha: self.visibilityFraction) + + for (_, itemView) in self.itemViews { + transition.setSublayerTransform(view: itemView, transform: CATransform3DMakeScale(scale, scale, 1.0)) + transition.setAlpha(view: itemView, alpha: self.visibilityFraction) + } + } + + private func activeContentItemIdUpdated(itemId: AnyHashable, transition: Transition) { + guard let component = self.component, let itemLayout = self.itemLayout else { + return + } + + var found = false + for i in 0 ..< component.items.count { + if component.items[i].id == itemId { + found = true + self.highlightedIconBackgroundView.isHidden = false + let itemFrame = itemLayout.containerFrame(at: i) + transition.setFrame(view: self.highlightedIconBackgroundView, frame: itemFrame) + + break + } + } + if !found { + self.highlightedIconBackgroundView.isHidden = true + } + } } func makeView() -> View { diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntitySearchContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntitySearchContentComponent.swift new file mode 100644 index 0000000000..a6febf912f --- /dev/null +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntitySearchContentComponent.swift @@ -0,0 +1,118 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import PagerComponent +import TelegramPresentationData +import TelegramCore +import Postbox +import AnimationCache +import MultiAnimationRenderer +import AccountContext +import AsyncDisplayKit +import ComponentDisplayAdapters + +public protocol EntitySearchContainerNode: ASDisplayNode { + var onCancel: (() -> Void)? { get set } + + func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, inputHeight: CGFloat, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition) +} + +final class EntitySearchContentEnvironment: Equatable { + let context: AccountContext + let theme: PresentationTheme + let deviceMetrics: DeviceMetrics + + init( + context: AccountContext, + theme: PresentationTheme, + deviceMetrics: DeviceMetrics + ) { + self.context = context + self.theme = theme + self.deviceMetrics = deviceMetrics + } + + static func ==(lhs: EntitySearchContentEnvironment, rhs: EntitySearchContentEnvironment) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.deviceMetrics != rhs.deviceMetrics { + return false + } + + return true + } +} + +final class EntitySearchContentComponent: Component { + typealias EnvironmentType = EntitySearchContentEnvironment + + let makeContainerNode: () -> EntitySearchContainerNode + let dismissSearch: () -> Void + + init( + makeContainerNode: @escaping () -> EntitySearchContainerNode, + dismissSearch: @escaping () -> Void + ) { + self.makeContainerNode = makeContainerNode + self.dismissSearch = dismissSearch + } + + static func ==(lhs: EntitySearchContentComponent, rhs: EntitySearchContentComponent) -> Bool { + return true + } + + final class View: UIView { + private var containerNode: EntitySearchContainerNode? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: EntitySearchContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let containerNode: EntitySearchContainerNode + if let current = self.containerNode { + containerNode = current + } else { + containerNode = component.makeContainerNode() + self.containerNode = containerNode + self.addSubnode(containerNode) + } + + let environmentValue = environment[EntitySearchContentEnvironment.self].value + + transition.setFrame(view: containerNode.view, frame: CGRect(origin: CGPoint(), size: availableSize)) + containerNode.updateLayout( + size: availableSize, + leftInset: 0.0, + rightInset: 0.0, + bottomInset: 0.0, + inputHeight: 0.0, + deviceMetrics: environmentValue.deviceMetrics, + transition: transition.containedViewLayoutTransition + ) + + containerNode.onCancel = { + component.dismissSearch() + } + + 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) + } +} diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift index 3b8a5c5c17..630000f39b 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift @@ -453,16 +453,18 @@ public final class GifPagerContentComponent: Component { } private func updateScrollingOffset(transition: Transition) { + let isInteracting = scrollView.isDragging || scrollView.isTracking || scrollView.isDecelerating if let previousScrollingOffsetValue = self.previousScrollingOffset { let currentBounds = scrollView.bounds let offsetToTopEdge = max(0.0, currentBounds.minY - 0.0) let offsetToBottomEdge = max(0.0, scrollView.contentSize.height - currentBounds.maxY) - let offsetToClosestEdge = min(offsetToTopEdge, offsetToBottomEdge) let relativeOffset = scrollView.contentOffset.y - previousScrollingOffsetValue self.pagerEnvironment?.onChildScrollingUpdate(PagerComponentChildEnvironment.ContentScrollingUpdate( relativeOffset: relativeOffset, - absoluteOffsetToClosestEdge: offsetToClosestEdge, + absoluteOffsetToTopEdge: offsetToTopEdge, + absoluteOffsetToBottomEdge: offsetToBottomEdge, + isInteracting: isInteracting, transition: transition )) self.previousScrollingOffset = scrollView.contentOffset.y diff --git a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift index c952603feb..34a79ff6e8 100644 --- a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift +++ b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift @@ -34,18 +34,6 @@ open class MultiAnimationRenderTarget: SimpleLayer { } } -private func convertFrameToImage(frame: AnimationCacheItemFrame) -> UIImage? { - switch frame.format { - case let .rgba(width, height, bytesPerRow): - let context = DrawingContext(size: CGSize(width: CGFloat(width), height: CGFloat(height)), scale: 1.0, opaque: false, bytesPerRow: bytesPerRow) - let range = frame.range - frame.data.withUnsafeBytes { bytes -> Void in - memcpy(context.bytes, bytes.baseAddress!.advanced(by: range.lowerBound), min(context.length, range.upperBound - range.lowerBound)) - } - return context.generateImage() - } -} - private final class FrameGroup { let image: UIImage let size: CGSize diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index f498039eb7..81e19b79b4 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -1523,7 +1523,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { - strongSelf.chatDisplayNode.inputPanelContainerNode.collapse() + strongSelf.chatDisplayNode.collapseInput() + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { current in var current = current current = current.updatedInterfaceState { interfaceState in @@ -1614,6 +1615,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { + strongSelf.chatDisplayNode.collapseInput() + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) }.updatedInputMode { current in if case let .media(mode, maybeExpanded, focused) = current, maybeExpanded != nil { @@ -9931,7 +9934,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case .media: break default: - self.chatDisplayNode.inputPanelContainerNode.collapse() + self.chatDisplayNode.collapseInput() } if self.isNodeLoaded { diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 3f67c5b134..86f724661d 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -108,6 +108,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { private let inputPanelBackgroundNode: NavigationBackgroundNode private var intrinsicInputPanelBackgroundNodeSize: CGSize? private let inputPanelBackgroundSeparatorNode: ASDisplayNode + private var inputPanelBottomBackgroundSeparatorBaseOffset: CGFloat = 0.0 private let inputPanelBottomBackgroundSeparatorNode: ASDisplayNode private var plainInputSeparatorAlpha: CGFloat? private var usePlainInputSeparator: Bool @@ -539,7 +540,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.inputPanelContainerNode.addSubnode(self.inputPanelClippingNode) self.inputPanelClippingNode.addSubnode(self.inputPanelBackgroundNode) self.inputPanelClippingNode.addSubnode(self.inputPanelBackgroundSeparatorNode) - self.inputPanelClippingNode.addSubnode(self.inputPanelBottomBackgroundSeparatorNode) + self.inputPanelBackgroundNode.addSubnode(self.inputPanelBottomBackgroundSeparatorNode) self.contentContainerNode.addSubnode(self.inputContextPanelContainer) @@ -773,8 +774,10 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.insertSubnode(navigationModalFrame, aboveSubnode: self.contentContainerNode) } if transition.isAnimated, let animateFromFraction = animateFromFraction, animateFromFraction != 1.0 - self.inputPanelContainerNode.expansionFraction { - navigationModalFrame.updateDismissal(transition: transition, progress: animateFromFraction, additionalProgress: 0.0, completion: {}) + navigationModalFrame.update(layout: layout, transition: .immediate) + navigationModalFrame.updateDismissal(transition: .immediate, progress: animateFromFraction, additionalProgress: 0.0, completion: {}) } + navigationModalFrame.update(layout: layout, transition: transition) navigationModalFrame.updateDismissal(transition: transition, progress: 1.0 - self.inputPanelContainerNode.expansionFraction, additionalProgress: 0.0, completion: {}) self.inputPanelClippingNode.clipsToBounds = true @@ -786,11 +789,14 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { navigationModalFrame?.removeFromSupernode() }) } + self.inputPanelClippingNode.clipsToBounds = true transition.updateCornerRadius(node: self.inputPanelClippingNode, cornerRadius: 0.0, completion: { [weak self] completed in guard let strongSelf = self, completed else { return } - strongSelf.inputPanelClippingNode.clipsToBounds = false + //strongSelf.inputPanelClippingNode.clipsToBounds = false + let _ = strongSelf + let _ = completed }) } @@ -1014,9 +1020,10 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { }) var insets: UIEdgeInsets + var inputPanelBottomInsetTerm: CGFloat = 0.0 if inputNodeForState != nil { insets = layout.insets(options: []) - insets.bottom = max(insets.bottom, layout.standardInputHeight) + inputPanelBottomInsetTerm = max(insets.bottom, layout.standardInputHeight) } else { insets = layout.insets(options: [.input]) } @@ -1041,13 +1048,15 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { let inputPanelNodes = inputPanelForChatPresentationIntefaceState(self.chatPresentationInterfaceState, context: self.context, currentPanel: self.inputPanelNode, currentSecondaryPanel: self.secondaryInputPanelNode, textInputPanelNode: self.textInputPanelNode, interfaceInteraction: self.interfaceInteraction) + let inputPanelBottomInset = max(insets.bottom, inputPanelBottomInsetTerm) + if let inputPanelNode = inputPanelNodes.primary, !previewing { if inputPanelNode !== self.inputPanelNode { if let inputTextPanelNode = self.inputPanelNode as? ChatTextInputPanelNode { 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 - insets.bottom, 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) } if let prevInputPanelNode = self.inputPanelNode, inputPanelNode.canHandleTransition(from: prevInputPanelNode) { inputPanelNodeHandlesTransition = true @@ -1057,7 +1066,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 - insets.bottom, 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) inputPanelSize = CGSize(width: layout.size.width, height: inputPanelHeight) self.inputPanelNode = inputPanelNode if inputPanelNode.supernode !== self { @@ -1065,7 +1074,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 - insets.bottom - 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) inputPanelSize = CGSize(width: layout.size.width, height: inputPanelHeight) } } else { @@ -1076,7 +1085,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 - insets.bottom, 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) secondaryInputPanelSize = CGSize(width: layout.size.width, height: inputPanelHeight) self.secondaryInputPanelNode = secondaryInputPanelNode if secondaryInputPanelNode.supernode == nil { @@ -1084,7 +1093,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 - insets.bottom, 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) secondaryInputPanelSize = CGSize(width: layout.size.width, height: inputPanelHeight) } } else { @@ -1165,6 +1174,9 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { inputNode.topBackgroundExtensionUpdated = { [weak self] transition in self?.updateInputPanelBackgroundExtension(transition: transition) } + inputNode.hideInputUpdated = { [weak self] transition in + self?.updateInputPanelBackgroundExpansion(transition: transition) + } inputNode.expansionFractionUpdated = { [weak self] transition in self?.updateInputPanelBackgroundExpansion(transition: transition) } @@ -1198,6 +1210,10 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } } + if inputNode.hideInput, 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) @@ -1334,6 +1350,11 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { if self.inputPanelNode != nil { inputPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - bottomOverflowOffset - inputPanelsHeight - inputPanelSize!.height), size: CGSize(width: layout.size.width, height: inputPanelSize!.height)) + if let inputNode = self.inputNode { + if inputNode.hideInput { + inputPanelFrame = inputPanelFrame!.offsetBy(dx: 0.0, dy: -inputPanelFrame!.height) + } + } if self.dismissedAsOverlay { inputPanelFrame!.origin.y = layout.size.height } @@ -1362,6 +1383,12 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { inputPanelsHeight = 0.0 } + if let inputNode = self.inputNode { + if inputNode.hideInput { + inputPanelsHeight = 0.0 + } + } + let inputBackgroundInset: CGFloat if cleanInsets.bottom < insets.bottom { inputBackgroundInset = 0.0 @@ -1557,8 +1584,10 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { if immediatelyLayoutInputNodeAndAnimateAppearance { inputPanelUpdateTransition = .immediate } + self.inputPanelBackgroundNode.update(size: CGSize(width: intrinsicInputPanelBackgroundNodeSize.width, height: intrinsicInputPanelBackgroundNodeSize.height + inputPanelBackgroundExtension), transition: inputPanelUpdateTransition) - transition.updateFrame(node: self.inputPanelBottomBackgroundSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: self.inputPanelBackgroundNode.frame.maxY + inputPanelBackgroundExtension), size: CGSize(width: self.inputPanelBackgroundNode.bounds.width, height: UIScreenPixel))) + self.inputPanelBottomBackgroundSeparatorBaseOffset = intrinsicInputPanelBackgroundNodeSize.height + inputPanelUpdateTransition.updateFrame(node: self.inputPanelBottomBackgroundSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: intrinsicInputPanelBackgroundNodeSize.height + inputPanelBackgroundExtension), size: CGSize(width: intrinsicInputPanelBackgroundNodeSize.width, height: UIScreenPixel)), beginWithCurrentState: true) transition.updateFrame(node: self.inputPanelBackgroundSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: apparentInputBackgroundFrame.origin.y), size: CGSize(width: apparentInputBackgroundFrame.size.width, height: UIScreenPixel))) transition.updateFrame(node: self.navigateButtons, frame: apparentNavigateButtonsFrame) @@ -1670,9 +1699,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { }) } - if let inputPanelNode = self.inputPanelNode, - let apparentInputPanelFrame = apparentInputPanelFrame, - !inputPanelNode.frame.equalTo(apparentInputPanelFrame) { + if let inputPanelNode = self.inputPanelNode, let apparentInputPanelFrame = apparentInputPanelFrame, !inputPanelNode.frame.equalTo(apparentInputPanelFrame) { if immediatelyLayoutInputPanelAndAnimateAppearance { inputPanelNode.frame = apparentInputPanelFrame.offsetBy(dx: 0.0, dy: apparentInputPanelFrame.height + previousInputPanelBackgroundFrame.maxY - apparentInputBackgroundFrame.maxY) inputPanelNode.alpha = 0.0 @@ -1929,10 +1956,26 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } self.inputPanelBackgroundNode.update(size: CGSize(width: intrinsicInputPanelBackgroundNodeSize.width, height: intrinsicInputPanelBackgroundNodeSize.height + extensionValue), transition: transition) - transition.updateFrame(node: self.inputPanelBottomBackgroundSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: self.inputPanelBackgroundNode.frame.maxY + extensionValue), size: CGSize(width: self.inputPanelBackgroundNode.bounds.width, height: UIScreenPixel))) + transition.updateFrame(node: self.inputPanelBottomBackgroundSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: self.inputPanelBottomBackgroundSeparatorBaseOffset + extensionValue), size: CGSize(width: self.inputPanelBottomBackgroundSeparatorNode.bounds.width, height: UIScreenPixel)), beginWithCurrentState: true) } + private var storedHideInputExpanded: Bool? + private func updateInputPanelBackgroundExpansion(transition: ContainedViewLayoutTransition) { + if let inputNode = self.inputNode { + if inputNode.hideInput { + self.storedHideInputExpanded = self.inputPanelContainerNode.expansionFraction == 1.0 + self.inputPanelContainerNode.expand() + } else { + if let storedHideInputExpanded = self.storedHideInputExpanded { + self.storedHideInputExpanded = nil + if !storedHideInputExpanded { + self.inputPanelContainerNode.collapse() + } + } + } + } + self.requestLayout(transition) } @@ -2222,6 +2265,18 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.view.window?.endEditing(true) } + func collapseInput() { + if self.inputPanelContainerNode.expansionFraction != 0.0 { + self.inputPanelContainerNode.collapse() + if let inputNode = self.inputNode { + inputNode.hideInput = false + if let inputNode = inputNode as? ChatEntityKeyboardInputNode { + inputNode.markInputCollapsed() + } + } + } + } + private func scheduleLayoutTransitionRequest(_ transition: ContainedViewLayoutTransition) { let requestId = self.scheduledLayoutTransitionRequestId self.scheduledLayoutTransitionRequestId += 1 @@ -2248,7 +2303,13 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } let _ = peerId - let inputNode = ChatEntityKeyboardInputNode(context: self.context, currentInputData: inputMediaNodeData, updatedInputData: self.inputMediaNodeDataPromise.get(), defaultToEmojiTab: !self.chatPresentationInterfaceState.interfaceState.effectiveInputState.inputText.string.isEmpty) + let inputNode = ChatEntityKeyboardInputNode( + context: self.context, + currentInputData: inputMediaNodeData, + updatedInputData: self.inputMediaNodeDataPromise.get(), + defaultToEmojiTab: !self.chatPresentationInterfaceState.interfaceState.effectiveInputState.inputText.string.isEmpty, + controllerInteraction: self.controllerInteraction + ) return inputNode } diff --git a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift index d3e258ebe2..ec3ec5f6db 100644 --- a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift @@ -206,6 +206,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { } return EmojiPagerContentComponent( + id: "emoji", context: context, animationCache: animationCache, animationRenderer: animationRenderer, @@ -386,6 +387,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { } return EmojiPagerContentComponent( + id: "stickers", context: context, animationCache: animationCache, animationRenderer: animationRenderer, @@ -393,7 +395,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { itemGroups: itemGroups.map { group -> EmojiPagerContentComponent.ItemGroup in var title: String? if group.id == AnyHashable("saved") { - title = nil + //TODO:localize + title = "Saved".uppercased() } else if group.id == AnyHashable("recent") { //TODO:localize title = "Recently Used".uppercased() @@ -444,18 +447,30 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { } } + private let context: AccountContext private let entityKeyboardView: ComponentHostView private let defaultToEmojiTab: Bool private var currentInputData: InputData private var inputDataDisposable: Disposable? + private let controllerInteraction: ChatControllerInteraction + + private var inputNodeInteraction: ChatMediaInputNodeInteraction? + + private let trendingGifsPromise = Promise(nil) + + 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)? - init(context: AccountContext, currentInputData: InputData, updatedInputData: Signal, defaultToEmojiTab: Bool) { + init(context: AccountContext, currentInputData: InputData, updatedInputData: Signal, defaultToEmojiTab: Bool, controllerInteraction: ChatControllerInteraction) { + self.context = context self.currentInputData = currentInputData self.defaultToEmojiTab = defaultToEmojiTab + self.controllerInteraction = controllerInteraction + self.entityKeyboardView = ComponentHostView() super.init() @@ -472,12 +487,48 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { strongSelf.currentInputData = inputData strongSelf.performLayout() }) + + self.inputNodeInteraction = ChatMediaInputNodeInteraction( + navigateToCollectionId: { _ in + }, + navigateBackToStickers: { + }, + setGifMode: { _ in + }, + openSettings: { + }, + openTrending: { _ in + }, + dismissTrendingPacks: { _ in + }, + toggleSearch: { _, _, _ in + }, + openPeerSpecificSettings: { + }, + dismissPeerSpecificSettings: { + }, + clearRecentlyUsedStickers: { + } + ) + + self.trendingGifsPromise.set(paneGifSearchForQuery(context: context, query: "", offset: nil, incompleteResults: true, delayRequest: false, updateActivity: nil) + |> map { items -> ChatMediaInputGifPaneTrendingState? in + if let items = items { + return ChatMediaInputGifPaneTrendingState(files: items.files, nextOffset: items.nextOffset) + } else { + return nil + } + }) } deinit { self.inputDataDisposable?.dispose() } + func markInputCollapsed() { + self.isMarkInputCollapsed = true + } + private func performLayout() { guard let (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, deviceMetrics, isVisible) = self.currentState else { return @@ -488,10 +539,24 @@ final class ChatEntityKeyboardInputNode: 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) { self.currentState = (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, deviceMetrics, isVisible) + let wasMarkedInputCollapsed = self.isMarkInputCollapsed + self.isMarkInputCollapsed = false + let expandedHeight = standardInputHeight + self.expansionFraction * (maximumHeight - standardInputHeight) + let context = self.context + let controllerInteraction = self.controllerInteraction + let inputNodeInteraction = self.inputNodeInteraction! + let trendingGifsPromise = self.trendingGifsPromise + + var mappedTransition = Transition(transition) + + if wasMarkedInputCollapsed { + mappedTransition = mappedTransition.withUserData(EntityKeyboardComponent.MarkInputCollapsed()) + } + let entityKeyboardSize = self.entityKeyboardView.update( - transition: Transition(transition), + transition: mappedTransition, component: AnyComponent(EntityKeyboardComponent( theme: interfaceState.theme, bottomInset: bottomInset, @@ -508,7 +573,39 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { strongSelf.topBackgroundExtension = topPanelExtension strongSelf.topBackgroundExtensionUpdated?(transition.containedViewLayoutTransition) } - } + }, + hideInputUpdated: { [weak self] hideInput, transition in + guard let strongSelf = self else { + return + } + if strongSelf.hideInput != hideInput { + strongSelf.hideInput = hideInput + strongSelf.hideInputUpdated?(transition.containedViewLayoutTransition) + } + }, + makeSearchContainerNode: { content in + let mappedMode: ChatMediaInputSearchMode + switch content { + case .stickers: + mappedMode = .sticker + case .gifs: + mappedMode = .gif + } + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + return PaneSearchContainerNode( + context: context, + theme: presentationData.theme, + strings: presentationData.strings, + controllerInteraction: controllerInteraction, + inputNodeInteraction: inputNodeInteraction, + mode: mappedMode, + trendingGifsPromise: trendingGifsPromise, + cancel: { + } + ) + }, + deviceMetrics: deviceMetrics )), environment: {}, containerSize: CGSize(width: width, height: expandedHeight) diff --git a/submodules/TelegramUI/Sources/ChatInputNode.swift b/submodules/TelegramUI/Sources/ChatInputNode.swift index da22776da3..cf978d6891 100644 --- a/submodules/TelegramUI/Sources/ChatInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatInputNode.swift @@ -16,6 +16,9 @@ class ChatInputNode: ASDisplayNode { var topBackgroundExtension: CGFloat = 41.0 var topBackgroundExtensionUpdated: ((ContainedViewLayoutTransition) -> Void)? + var hideInput: Bool = false + var hideInputUpdated: ((ContainedViewLayoutTransition) -> Void)? + var expansionFraction: CGFloat = 0.0 var expansionFractionUpdated: ((ContainedViewLayoutTransition) -> Void)? diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index e7ba397f5e..dbccddb807 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -2454,10 +2454,12 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } else { switch presentationInterfaceState.inputMode { - case .text, .media: + case .text: self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId { _ in return (.none, nil) } + case .media: + break default: break } diff --git a/submodules/TelegramUI/Sources/PaneSearchContainerNode.swift b/submodules/TelegramUI/Sources/PaneSearchContainerNode.swift index d00c0d977c..13244d5c08 100644 --- a/submodules/TelegramUI/Sources/PaneSearchContainerNode.swift +++ b/submodules/TelegramUI/Sources/PaneSearchContainerNode.swift @@ -8,6 +8,7 @@ import TelegramCore import TelegramPresentationData import AccountContext import ChatPresentationInterfaceState +import EntityKeyboard private let searchBarHeight: CGFloat = 52.0 @@ -27,7 +28,7 @@ protocol PaneSearchContentNode { func itemAt(point: CGPoint) -> (ASDisplayNode, Any)? } -final class PaneSearchContainerNode: ASDisplayNode { +final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainerNode { private let context: AccountContext private let mode: ChatMediaInputSearchMode public private(set) var contentNode: PaneSearchContentNode & ASDisplayNode @@ -39,6 +40,8 @@ final class PaneSearchContainerNode: ASDisplayNode { private var validLayout: CGSize? + var onCancel: (() -> Void)? + var openGifContextMenu: ((MultiplexedVideoNodeFile, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void)? var ready: Signal { @@ -75,8 +78,11 @@ final class PaneSearchContainerNode: ASDisplayNode { self?.searchBar.activity = active } - self.searchBar.cancel = { + self.searchBar.cancel = { [weak self] in cancel() + + self?.searchBar.view.endEditing(true) + self?.onCancel?() } self.searchBar.activate()