Sticker input rewrite continued

This commit is contained in:
Ali 2022-07-02 00:04:43 +02:00
parent baad43fb1f
commit 087bf3352e
23 changed files with 1299 additions and 187 deletions

View File

@ -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,

View File

@ -118,3 +118,93 @@ public final class ComponentHostView<EnvironmentType>: UIView {
return findTaggedViewImpl(view: componentView, tag: tag)
}
}
public final class ComponentView<EnvironmentType> {
private var currentComponent: AnyComponent<EnvironmentType>?
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<EnvironmentType>, @EnvironmentBuilder environment: () -> Environment<EnvironmentType>, 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<EnvironmentType>, maybeEnvironment: () -> Environment<EnvironmentType>, 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<EnvironmentType>, 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)
}
}

View File

@ -15,7 +15,7 @@ public final class Action<Arguments> {
public final class ActionSlot<Arguments>: Equatable {
private var target: ((Arguments) -> Void)?
init() {
public init() {
}
public static func ==(lhs: ActionSlot<Arguments>, rhs: ActionSlot<Arguments>) -> Bool {

View File

@ -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<TopPanelEnvironment>: Equatable {
public let contentOffset: CGFloat
public let contentTopPanels: [AnyComponentWithIdentity<Empty>]
public let contentTopPanels: [AnyComponentWithIdentity<TopPanelEnvironment>]
public let contentIcons: [AnyComponentWithIdentity<Empty>]
public let contentAccessoryLeftButtons: [AnyComponentWithIdentity<Empty>]
public let contentAccessoryRightButtons: [AnyComponentWithIdentity<Empty>]
public let activeContentId: AnyHashable?
public let navigateToContentId: (AnyHashable) -> Void
public let visibilityFractionUpdated: ActionSlot<(CGFloat, Transition)>
init(
contentOffset: CGFloat,
contentTopPanels: [AnyComponentWithIdentity<Empty>],
contentTopPanels: [AnyComponentWithIdentity<TopPanelEnvironment>],
contentIcons: [AnyComponentWithIdentity<Empty>],
contentAccessoryLeftButtons: [AnyComponentWithIdentity<Empty>],
contentAccessoryRightButtons: [AnyComponentWithIdentity<Empty>],
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<ChildEnvironmentType: Equatable>: Component {
public final class PagerComponentViewTag {
public init() {
}
}
public final class PagerComponent<ChildEnvironmentType: Equatable, TopPanelEnvironment: Equatable>: Component {
public typealias EnvironmentType = ChildEnvironmentType
public let contentInsets: UIEdgeInsets
public let contents: [AnyComponentWithIdentity<(ChildEnvironmentType, PagerComponentChildEnvironment)>]
public let contentTopPanels: [AnyComponentWithIdentity<Empty>]
public let contentTopPanels: [AnyComponentWithIdentity<TopPanelEnvironment>]
public let contentIcons: [AnyComponentWithIdentity<Empty>]
public let contentAccessoryLeftButtons:[AnyComponentWithIdentity<Empty>]
public let contentAccessoryRightButtons:[AnyComponentWithIdentity<Empty>]
public let defaultId: AnyHashable?
public let contentBackground: AnyComponent<Empty>?
public let topPanel: AnyComponent<PagerComponentPanelEnvironment>?
public let topPanel: AnyComponent<PagerComponentPanelEnvironment<TopPanelEnvironment>>?
public let externalTopPanelContainer: UIView?
public let bottomPanel: AnyComponent<PagerComponentPanelEnvironment>?
public let bottomPanel: AnyComponent<PagerComponentPanelEnvironment<TopPanelEnvironment>>?
public let panelStateUpdated: ((PagerComponentPanelState, Transition) -> Void)?
public let hidePanels: Bool
public init(
contentInsets: UIEdgeInsets,
contents: [AnyComponentWithIdentity<(ChildEnvironmentType, PagerComponentChildEnvironment)>],
contentTopPanels: [AnyComponentWithIdentity<Empty>],
contentTopPanels: [AnyComponentWithIdentity<TopPanelEnvironment>],
contentIcons: [AnyComponentWithIdentity<Empty>],
contentAccessoryRightButtons:[AnyComponentWithIdentity<Empty>],
contentAccessoryLeftButtons: [AnyComponentWithIdentity<Empty>],
contentAccessoryRightButtons: [AnyComponentWithIdentity<Empty>],
defaultId: AnyHashable?,
contentBackground: AnyComponent<Empty>?,
topPanel: AnyComponent<PagerComponentPanelEnvironment>?,
topPanel: AnyComponent<PagerComponentPanelEnvironment<TopPanelEnvironment>>?,
externalTopPanelContainer: UIView?,
bottomPanel: AnyComponent<PagerComponentPanelEnvironment>?,
panelStateUpdated: ((PagerComponentPanelState, Transition) -> Void)?
bottomPanel: AnyComponent<PagerComponentPanelEnvironment<TopPanelEnvironment>>?,
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<ChildEnvironmentType: Equatable>: 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<ChildEnvironmentType: Equatable>: 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<Empty>?
private var topPanelView: ComponentHostView<PagerComponentPanelEnvironment>?
private var bottomPanelView: ComponentHostView<PagerComponentPanelEnvironment>?
private let topPanelVisibilityFractionUpdated = ActionSlot<(CGFloat, Transition)>()
private var topPanelView: ComponentHostView<PagerComponentPanelEnvironment<TopPanelEnvironment>>?
private let bottomPanelVisibilityFractionUpdated = ActionSlot<(CGFloat, Transition)>()
private var bottomPanelView: ComponentHostView<PagerComponentPanelEnvironment<TopPanelEnvironment>>?
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<ChildEnvironmentType>?
private var component: PagerComponent<ChildEnvironmentType, TopPanelEnvironment>?
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<ChildEnvironmentType: Equatable>: 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<ChildEnvironmentType: Equatable>: Component {
}
}
func update(component: PagerComponent<ChildEnvironmentType>, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
func update(component: PagerComponent<ChildEnvironmentType, TopPanelEnvironment>, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
self.component = component
self.state = state
@ -288,22 +344,22 @@ public final class PagerComponent<ChildEnvironmentType: Equatable>: 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<PagerComponentPanelEnvironment>
let topPanelView: ComponentHostView<PagerComponentPanelEnvironment<TopPanelEnvironment>>
var topPanelTransition = transition
if let current = self.topPanelView {
topPanelView = current
} else {
topPanelTransition = .immediate
topPanelView = ComponentHostView<PagerComponentPanelEnvironment>()
topPanelView = ComponentHostView<PagerComponentPanelEnvironment<TopPanelEnvironment>>()
topPanelView.clipsToBounds = true
self.topPanelView = topPanelView
}
@ -321,20 +377,38 @@ public final class PagerComponent<ChildEnvironmentType: Equatable>: 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<ChildEnvironmentType: Equatable>: 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<PagerComponentPanelEnvironment>
let bottomPanelView: ComponentHostView<PagerComponentPanelEnvironment<TopPanelEnvironment>>
var bottomPanelTransition = transition
if let current = self.bottomPanelView {
bottomPanelView = current
} else {
bottomPanelTransition = .immediate
bottomPanelView = ComponentHostView<PagerComponentPanelEnvironment>()
bottomPanelView = ComponentHostView<PagerComponentPanelEnvironment<TopPanelEnvironment>>()
self.bottomPanelView = bottomPanelView
self.addSubview(bottomPanelView)
}
@ -365,19 +441,26 @@ public final class PagerComponent<ChildEnvironmentType: Equatable>: Component {
transition: bottomPanelTransition,
component: bottomPanel,
environment: {
PagerComponentPanelEnvironment(
PagerComponentPanelEnvironment<TopPanelEnvironment>(
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<ChildEnvironmentType: Equatable>: Component {
bottomPanelView.removeFromSuperview()
}
self.bottomPanelHeight = 0.0
}
let effectiveTopPanelHeight: CGFloat = component.hidePanels ? 0.0 : topPanelHeight
if let contentBackground = component.contentBackground {
let contentBackgroundView: ComponentHostView<Empty>
var contentBackgroundTransition = transition
@ -405,9 +492,9 @@ public final class PagerComponent<ChildEnvironmentType: Equatable>: 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<ChildEnvironmentType: Equatable>: 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)
}
}

View File

@ -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)

View File

@ -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
}
}

View File

@ -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 {

View File

@ -13,6 +13,7 @@ swift_library(
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/Components/PagerComponent:PagerComponent",
],
visibility = [
"//visibility:public",

View File

@ -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<UITouch>, with event: UIEvent) {
override public func touchesBegan(_ touches: Set<UITouch>, 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<UITouch>, 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<UITouch>, with event: UIEvent) {
super.touchesEnded(touches, with: event)
self.state = .ended
}
override public func touchesCancelled(_ touches: Set<UITouch>, 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
}
}

View File

@ -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<Int>)] {
var result: [(groupIndex: Int, groupItems: Range<Int>)] = []
func visibleItems(for rect: CGRect) -> [(id: AnyHashable, groupIndex: Int, groupItems: Range<Int>)] {
var result: [(id: AnyHashable, groupIndex: Int, groupItems: Range<Int>)] = []
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<Empty>] = [:]
private var visibleGroupHeaders: [AnyHashable: ComponentView<Empty>] = [:]
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<ItemLayer.Key>()
var validGroupHeaderIds = Set<AnyHashable>()
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<Empty>
let groupHeaderView: ComponentView<Empty>
if let current = self.visibleGroupHeaders[itemGroup.id] {
groupHeaderView = current
} else {
groupHeaderView = ComponentHostView<Empty>()
groupHeaderView = ComponentView<Empty>()
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<EnvironmentType>, 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)

View File

@ -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<EntityKeyboardChildEnvironment>
private var component: EntityKeyboardComponent?
private weak var state: EmptyComponentState?
private var searchView: ComponentHostView<EntitySearchContentEnvironment>?
private var searchComponent: EntitySearchContentComponent?
private var topPanelExtension: CGFloat?
override init(frame: CGRect) {
self.pagerView = ComponentHostView<EntityKeyboardChildEnvironment>()
@ -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<Empty>, transition: Transition) -> CGSize {
self.state = state
var contents: [AnyComponentWithIdentity<(EntityKeyboardChildEnvironment, PagerComponentChildEnvironment)>] = []
var contentTopPanels: [AnyComponentWithIdentity<Empty>] = []
var contentTopPanels: [AnyComponentWithIdentity<EntityKeyboardTopContainerPanelEnvironment>] = []
var contentIcons: [AnyComponentWithIdentity<Empty>] = []
var contentAccessoryLeftButtons: [AnyComponentWithIdentity<Empty>] = []
var contentAccessoryRightButtons: [AnyComponentWithIdentity<Empty>] = []
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<EntitySearchContentEnvironment>
var searchViewTransition = transition
if let current = self.searchView {
searchView = current
} else {
searchViewTransition = .immediate
searchView = ComponentHostView<EntitySearchContentEnvironment>()
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<EntityKeyboardChildEnvironment, EntityKeyboardTopContainerPanelEnvironment>.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 {

View File

@ -81,7 +81,7 @@ private final class BottomPanelIconComponent: Component {
}
final class EntityKeyboardBottomPanelComponent: Component {
typealias EnvironmentType = PagerComponentPanelEnvironment
typealias EnvironmentType = PagerComponentPanelEnvironment<EntityKeyboardTopContainerPanelEnvironment>
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<Empty>
let view: ComponentHostView<Empty>
init(id: AnyHashable, view: ComponentHostView<Empty>) {
init(id: AnyHashable, component: AnyComponent<Empty>, view: ComponentHostView<Empty>) {
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<Empty>] = [:]
@ -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<EntityKeyboardTopContainerPanelEnvironment>.self].value
let activeContentId = panelEnvironment.activeContentId
var leftAccessoryButtonComponent: AnyComponentWithIdentity<Empty>?
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<Empty>())
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<Empty>?
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<Empty>())
rightAccessoryButton = AccessoryButtonView(id: rightAccessoryButtonComponent.id, component: rightAccessoryButtonComponent.component, view: ComponentHostView<Empty>())
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 {

View File

@ -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<EntityKeyboardTopContainerPanelEnvironment>
let theme: PresentationTheme
@ -26,13 +43,20 @@ final class EntityKeyboardTopContainerPanelComponent: Component {
return true
}
private final class PanelView {
let view = ComponentHostView<EntityKeyboardTopContainerPanelEnvironment>()
let visibilityFractionUpdated = ActionSlot<(CGFloat, Transition)>()
}
final class View: UIView {
private var panelViews: [AnyHashable: ComponentHostView<Empty>] = [:]
private var panelViews: [AnyHashable: PanelView] = [:]
private var component: EntityKeyboardTopContainerPanelComponent?
private var panelEnvironment: PagerComponentPanelEnvironment?
private var panelEnvironment: PagerComponentPanelEnvironment<EntityKeyboardTopContainerPanelEnvironment>?
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<Empty>
let panelView: PanelView
if let current = self.panelViews[panel.id] {
panelView = current
} else {
panelTransition = .immediate
panelView = ComponentHostView<Empty>()
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 {

View File

@ -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<EnvironmentType>, 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 {

View File

@ -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<EnvironmentType>, 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<EnvironmentType>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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
}

View File

@ -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<Empty>
private let defaultToEmojiTab: Bool
private var currentInputData: InputData
private var inputDataDisposable: Disposable?
private let controllerInteraction: ChatControllerInteraction
private var inputNodeInteraction: ChatMediaInputNodeInteraction?
private let trendingGifsPromise = Promise<ChatMediaInputGifPaneTrendingState?>(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<InputData, NoError>, defaultToEmojiTab: Bool) {
init(context: AccountContext, currentInputData: InputData, updatedInputData: Signal<InputData, NoError>, defaultToEmojiTab: Bool, controllerInteraction: ChatControllerInteraction) {
self.context = context
self.currentInputData = currentInputData
self.defaultToEmojiTab = defaultToEmojiTab
self.controllerInteraction = controllerInteraction
self.entityKeyboardView = ComponentHostView<Empty>()
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)

View File

@ -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)?

View File

@ -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
}

View File

@ -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<Void, NoError> {
@ -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()