Swiftgram/submodules/TelegramUI/Sources/ChatOverscrollControl.swift
2023-03-13 16:06:07 +04:00

1121 lines
44 KiB
Swift

import UIKit
import ComponentFlow
import Display
import AsyncDisplayKit
import TelegramCore
import Postbox
import AccountContext
import AvatarNode
import TextFormat
import Markdown
import WallpaperBackgroundNode
final class BlurredRoundedRectangle: Component {
let color: UIColor
init(color: UIColor) {
self.color = color
}
static func ==(lhs: BlurredRoundedRectangle, rhs: BlurredRoundedRectangle) -> Bool {
if !lhs.color.isEqual(rhs.color) {
return false
}
return true
}
final class View: UIView {
private let background: NavigationBackgroundNode
init() {
self.background = NavigationBackgroundNode(color: .clear)
super.init(frame: CGRect())
self.addSubview(self.background.view)
}
required init?(coder aDecoder: NSCoder) {
preconditionFailure()
}
func update(component: BlurredRoundedRectangle, availableSize: CGSize, transition: Transition) -> CGSize {
transition.setFrame(view: self.background.view, frame: CGRect(origin: CGPoint(), size: availableSize))
self.background.updateColor(color: component.color, transition: .immediate)
self.background.update(size: availableSize, cornerRadius: min(availableSize.width, availableSize.height) / 2.0, transition: .immediate)
return availableSize
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
final class RadialProgressComponent: Component {
let color: UIColor
let lineWidth: CGFloat
let value: CGFloat
init(
color: UIColor,
lineWidth: CGFloat,
value: CGFloat
) {
self.color = color
self.lineWidth = lineWidth
self.value = value
}
static func ==(lhs: RadialProgressComponent, rhs: RadialProgressComponent) -> Bool {
if !lhs.color.isEqual(rhs.color) {
return false
}
if lhs.lineWidth != rhs.lineWidth {
return false
}
if lhs.value != rhs.value {
return false
}
return true
}
final class View: UIView {
init() {
super.init(frame: CGRect())
}
required init?(coder aDecoder: NSCoder) {
preconditionFailure()
}
func update(component: RadialProgressComponent, availableSize: CGSize, transition: Transition) -> CGSize {
func draw(context: CGContext) {
let diameter = availableSize.width
context.saveGState()
context.setBlendMode(.normal)
context.setFillColor(component.color.cgColor)
context.setStrokeColor(component.color.cgColor)
var progress: CGFloat
var startAngle: CGFloat
var endAngle: CGFloat
let value = component.value
progress = value
startAngle = -CGFloat.pi / 2.0
endAngle = CGFloat(progress) * 2.0 * CGFloat.pi + startAngle
if progress > 1.0 {
progress = 2.0 - progress
let tmp = startAngle
startAngle = endAngle
endAngle = tmp
}
progress = min(1.0, progress)
let lineWidth: CGFloat = component.lineWidth
let pathDiameter: CGFloat
pathDiameter = diameter - lineWidth
var angle: Double = 0.0
angle *= 4.0
context.translateBy(x: diameter / 2.0, y: diameter / 2.0)
context.rotate(by: CGFloat(angle.truncatingRemainder(dividingBy: Double.pi * 2.0)))
context.translateBy(x: -diameter / 2.0, y: -diameter / 2.0)
let path = UIBezierPath(arcCenter: CGPoint(x: diameter / 2.0, y: diameter / 2.0), radius: pathDiameter / 2.0, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.lineWidth = lineWidth
path.lineCapStyle = .round
path.stroke()
context.restoreGState()
}
if #available(iOS 10.0, *) {
let renderer = UIGraphicsImageRenderer(bounds: CGRect(origin: CGPoint(), size: availableSize))
let image = renderer.image { context in
UIGraphicsPushContext(context.cgContext)
draw(context: context.cgContext)
UIGraphicsPopContext()
}
self.layer.contents = image.cgImage
} else {
UIGraphicsBeginImageContextWithOptions(availableSize, false, 0.0)
draw(context: UIGraphicsGetCurrentContext()!)
self.layer.contents = UIGraphicsGetImageFromCurrentImageContext()?.cgImage
UIGraphicsEndImageContext()
}
return availableSize
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
final class CheckComponent: Component {
let color: UIColor
let lineWidth: CGFloat
let value: CGFloat
init(
color: UIColor,
lineWidth: CGFloat,
value: CGFloat
) {
self.color = color
self.lineWidth = lineWidth
self.value = value
}
static func ==(lhs: CheckComponent, rhs: CheckComponent) -> Bool {
if !lhs.color.isEqual(rhs.color) {
return false
}
if lhs.lineWidth != rhs.lineWidth {
return false
}
if lhs.value != rhs.value {
return false
}
return true
}
final class View: UIView {
private var currentValue: CGFloat?
private var animator: DisplayLinkAnimator?
init() {
super.init(frame: CGRect())
}
required init?(coder aDecoder: NSCoder) {
preconditionFailure()
}
private func updateContent(size: CGSize, color: UIColor, lineWidth: CGFloat, value: CGFloat) {
func draw(context: CGContext) {
let diameter = size.width
let factor = diameter / 50.0
context.saveGState()
context.setBlendMode(.normal)
context.setFillColor(color.cgColor)
context.setStrokeColor(color.cgColor)
let center = CGPoint(x: diameter / 2.0, y: diameter / 2.0)
context.setLineWidth(max(1.7, lineWidth * factor))
context.setLineCap(.round)
context.setLineJoin(.round)
context.setMiterLimit(10.0)
let progress = value
let firstSegment: CGFloat = max(0.0, min(1.0, progress * 3.0))
var s = CGPoint(x: center.x - 10.0 * factor, y: center.y + 1.0 * factor)
var p1 = CGPoint(x: 7.0 * factor, y: 7.0 * factor)
var p2 = CGPoint(x: 13.0 * factor, y: -15.0 * factor)
if diameter < 36.0 {
s = CGPoint(x: center.x - 7.0 * factor, y: center.y + 1.0 * factor)
p1 = CGPoint(x: 4.5 * factor, y: 4.5 * factor)
p2 = CGPoint(x: 10.0 * factor, y: -11.0 * factor)
}
if !firstSegment.isZero {
if firstSegment < 1.0 {
context.move(to: CGPoint(x: s.x + p1.x * firstSegment, y: s.y + p1.y * firstSegment))
context.addLine(to: s)
} else {
let secondSegment = (progress - 0.33) * 1.5
context.move(to: CGPoint(x: s.x + p1.x + p2.x * secondSegment, y: s.y + p1.y + p2.y * secondSegment))
context.addLine(to: CGPoint(x: s.x + p1.x, y: s.y + p1.y))
context.addLine(to: s)
}
}
context.strokePath()
context.restoreGState()
}
if #available(iOS 10.0, *) {
let renderer = UIGraphicsImageRenderer(bounds: CGRect(origin: CGPoint(), size: size))
let image = renderer.image { context in
UIGraphicsPushContext(context.cgContext)
draw(context: context.cgContext)
UIGraphicsPopContext()
}
self.layer.contents = image.cgImage
} else {
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
draw(context: UIGraphicsGetCurrentContext()!)
self.layer.contents = UIGraphicsGetImageFromCurrentImageContext()?.cgImage
UIGraphicsEndImageContext()
}
}
func update(component: CheckComponent, availableSize: CGSize, transition: Transition) -> CGSize {
if let currentValue = self.currentValue, currentValue != component.value, case .curve = transition.animation {
self.animator?.invalidate()
let animator = DisplayLinkAnimator(duration: 0.15, from: currentValue, to: component.value, update: { [weak self] value in
guard let strongSelf = self else {
return
}
strongSelf.updateContent(size: availableSize, color: component.color, lineWidth: component.lineWidth, value: value)
}, completion: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.animator?.invalidate()
strongSelf.animator = nil
})
self.animator = animator
} else {
if self.animator == nil {
self.updateContent(size: availableSize, color: component.color, lineWidth: component.lineWidth, value: component.value)
}
}
self.currentValue = component.value
return availableSize
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
final class BadgeComponent: CombinedComponent {
let count: Int
let backgroundColor: UIColor
let foregroundColor: UIColor
let rect: CGRect
let withinSize: CGSize
let wallpaperNode: WallpaperBackgroundNode?
init(
count: Int,
backgroundColor: UIColor,
foregroundColor: UIColor,
rect: CGRect,
withinSize: CGSize,
wallpaperNode: WallpaperBackgroundNode?
) {
self.count = count
self.backgroundColor = backgroundColor
self.foregroundColor = foregroundColor
self.rect = rect
self.withinSize = withinSize
self.wallpaperNode = wallpaperNode
}
static func ==(lhs: BadgeComponent, rhs: BadgeComponent) -> Bool {
if lhs.count != rhs.count {
return false
}
if !lhs.backgroundColor.isEqual(rhs.backgroundColor) {
return false
}
if !lhs.foregroundColor.isEqual(rhs.foregroundColor) {
return false
}
if lhs.rect != rhs.rect {
return false
}
if lhs.withinSize != rhs.withinSize {
return false
}
if lhs.wallpaperNode !== rhs.wallpaperNode {
return false
}
return true
}
static var body: Body {
let background = Child(WallpaperBlurComponent.self)
let text = Child(Text.self)
return { context in
let text = text.update(
component: Text(
text: "\(context.component.count)",
font: Font.regular(13.0),
color: context.component.foregroundColor
),
availableSize: CGSize(width: 100.0, height: 100.0),
transition: .immediate
)
let height = text.size.height + 4.0
let backgroundSize = CGSize(width: max(height, text.size.width + 8.0), height: height)
let background = background.update(
component: WallpaperBlurComponent(
rect: CGRect(origin: context.component.rect.origin, size: backgroundSize),
withinSize: context.component.withinSize,
color: context.component.backgroundColor,
wallpaperNode: context.component.wallpaperNode
),
availableSize: backgroundSize,
transition: .immediate
)
context.add(background
.position(CGPoint(x: backgroundSize.width / 2.0, y: backgroundSize.height / 2.0))
.cornerRadius(min(backgroundSize.width, backgroundSize.height) / 2.0)
.clipsToBounds(true)
)
context.add(text
.position(CGPoint(x: backgroundSize.width / 2.0, y: backgroundSize.height / 2.0))
)
return backgroundSize
}
}
}
final class AvatarComponent: Component {
final class Badge: Equatable {
let count: Int
let backgroundColor: UIColor
let foregroundColor: UIColor
init(count: Int, backgroundColor: UIColor, foregroundColor: UIColor) {
self.count = count
self.backgroundColor = backgroundColor
self.foregroundColor = foregroundColor
}
static func ==(lhs: Badge, rhs: Badge) -> Bool {
if lhs.count != rhs.count {
return false
}
if !lhs.backgroundColor.isEqual(rhs.backgroundColor) {
return false
}
if !lhs.foregroundColor.isEqual(rhs.foregroundColor) {
return false
}
return true
}
}
let context: AccountContext
let peer: EnginePeer
let badge: Badge?
let rect: CGRect
let withinSize: CGSize
let wallpaperNode: WallpaperBackgroundNode?
init(
context: AccountContext,
peer: EnginePeer,
badge: Badge?,
rect: CGRect,
withinSize: CGSize,
wallpaperNode: WallpaperBackgroundNode?
) {
self.context = context
self.peer = peer
self.badge = badge
self.rect = rect
self.withinSize = withinSize
self.wallpaperNode = wallpaperNode
}
static func ==(lhs: AvatarComponent, rhs: AvatarComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.peer != rhs.peer {
return false
}
if lhs.badge != rhs.badge {
return false
}
if lhs.rect != rhs.rect {
return false
}
if lhs.withinSize != rhs.withinSize {
return false
}
if lhs.wallpaperNode !== rhs.wallpaperNode {
return false
}
return true
}
final class View: UIView {
private let avatarNode: AvatarNode
private let avatarMask: CAShapeLayer
private var badgeView: ComponentHostView<Empty>?
init() {
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0))
self.avatarMask = CAShapeLayer()
super.init(frame: CGRect())
self.addSubview(self.avatarNode.view)
}
required init?(coder aDecoder: NSCoder) {
preconditionFailure()
}
func update(component: AvatarComponent, availableSize: CGSize, transition: Transition) -> CGSize {
self.avatarNode.frame = CGRect(origin: CGPoint(), size: availableSize)
let theme = component.context.sharedContext.currentPresentationData.with({ $0 }).theme
self.avatarNode.setPeer(context: component.context, theme: theme, peer: component.peer, emptyColor: theme.list.mediaPlaceholderColor, synchronousLoad: true)
if let badge = component.badge {
let badgeView: ComponentHostView<Empty>
let animateIn = self.badgeView == nil
if let current = self.badgeView {
badgeView = current
} else {
badgeView = ComponentHostView<Empty>()
self.badgeView = badgeView
self.addSubview(badgeView)
}
let badgeSize = badgeView.update(
transition: .immediate,
component: AnyComponent(BadgeComponent(
count: badge.count,
backgroundColor: badge.backgroundColor,
foregroundColor: badge.foregroundColor,
rect: CGRect(origin: component.rect.offsetBy(dx: 0.0, dy: 0.0).origin, size: CGSize()),
withinSize: component.withinSize,
wallpaperNode: component.wallpaperNode
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0
))
let badgeDiameter = min(badgeSize.width, badgeSize.height)
let circlePoint = CGPoint(
x: availableSize.width / 2.0 + cos(CGFloat.pi / 4) * availableSize.width / 2.0,
y: availableSize.height / 2.0 - sin(CGFloat.pi / 4) * availableSize.width / 2.0
)
badgeView.frame = CGRect(origin: CGPoint(x: circlePoint.x - badgeDiameter / 2.0, y: circlePoint.y - badgeDiameter / 2.0), size: badgeSize)
self.avatarMask.frame = self.avatarNode.bounds
self.avatarMask.fillRule = .evenOdd
let path = UIBezierPath(rect: self.avatarMask.bounds)
path.append(UIBezierPath(roundedRect: badgeView.frame.insetBy(dx: -2.0, dy: -2.0), cornerRadius: badgeDiameter / 2.0))
self.avatarMask.path = path.cgPath
self.avatarNode.view.layer.mask = self.avatarMask
if animateIn {
badgeView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.14)
}
} else if let badgeView = self.badgeView {
self.badgeView = nil
badgeView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.14, removeOnCompletion: false, completion: { [weak badgeView] _ in
badgeView?.removeFromSuperview()
})
self.avatarNode.view.layer.mask = nil
}
return availableSize
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
private final class WallpaperBlurNode: ASDisplayNode {
private var backgroundNode: WallpaperBubbleBackgroundNode?
private let colorNode: ASDisplayNode
override init() {
self.colorNode = ASDisplayNode()
super.init()
//self.addSubnode(self.colorNode)
}
func update(rect: CGRect, within size: CGSize, color: UIColor, wallpaperNode: WallpaperBackgroundNode?, transition: ContainedViewLayoutTransition) {
var transition = transition
if self.backgroundNode == nil {
if let backgroundNode = wallpaperNode?.makeBubbleBackground(for: .free) {
self.backgroundNode = backgroundNode
self.insertSubnode(backgroundNode, at: 0)
transition = .immediate
}
}
self.colorNode.backgroundColor = color
transition.updateFrame(node: self.colorNode, frame: CGRect(origin: CGPoint(), size: rect.size))
if let backgroundNode = self.backgroundNode {
transition.updateFrame(node: backgroundNode, frame: CGRect(origin: CGPoint(), size: rect.size))
backgroundNode.update(rect: rect, within: size, transition: transition)
}
}
}
private final class WallpaperBlurComponent: Component {
let rect: CGRect
let withinSize: CGSize
let color: UIColor
let wallpaperNode: WallpaperBackgroundNode?
init(
rect: CGRect,
withinSize: CGSize,
color: UIColor,
wallpaperNode: WallpaperBackgroundNode?
) {
self.rect = rect
self.withinSize = withinSize
self.color = color
self.wallpaperNode = wallpaperNode
}
static func ==(lhs: WallpaperBlurComponent, rhs: WallpaperBlurComponent) -> Bool {
if lhs.rect != rhs.rect {
return false
}
if lhs.withinSize != rhs.withinSize {
return false
}
if !lhs.color.isEqual(rhs.color) {
return false
}
if lhs.wallpaperNode !== rhs.wallpaperNode {
return false
}
return true
}
final class View: UIView {
private let background: WallpaperBlurNode
init() {
self.background = WallpaperBlurNode()
super.init(frame: CGRect())
self.addSubview(self.background.view)
}
required init?(coder aDecoder: NSCoder) {
preconditionFailure()
}
func update(component: WallpaperBlurComponent, availableSize: CGSize, transition: Transition) -> CGSize {
transition.setFrame(view: self.background.view, frame: CGRect(origin: CGPoint(), size: availableSize))
self.background.update(rect: component.rect, within: component.withinSize, color: component.color, wallpaperNode: component.wallpaperNode, transition: .immediate)
return availableSize
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
final class OverscrollContentsComponent: Component {
let context: AccountContext
let backgroundColor: UIColor
let foregroundColor: UIColor
let peer: EnginePeer?
let unreadCount: Int
let location: TelegramEngine.NextUnreadChannelLocation
let expandOffset: CGFloat
let freezeProgress: Bool
let absoluteRect: CGRect
let absoluteSize: CGSize
let wallpaperNode: WallpaperBackgroundNode?
init(
context: AccountContext,
backgroundColor: UIColor,
foregroundColor: UIColor,
peer: EnginePeer?,
unreadCount: Int,
location: TelegramEngine.NextUnreadChannelLocation,
expandOffset: CGFloat,
freezeProgress: Bool,
absoluteRect: CGRect,
absoluteSize: CGSize,
wallpaperNode: WallpaperBackgroundNode?
) {
self.context = context
self.backgroundColor = backgroundColor
self.foregroundColor = foregroundColor
self.peer = peer
self.unreadCount = unreadCount
self.location = location
self.expandOffset = expandOffset
self.freezeProgress = freezeProgress
self.absoluteRect = absoluteRect
self.absoluteSize = absoluteSize
self.wallpaperNode = wallpaperNode
}
static func ==(lhs: OverscrollContentsComponent, rhs: OverscrollContentsComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if !lhs.backgroundColor.isEqual(rhs.backgroundColor) {
return false
}
if !lhs.foregroundColor.isEqual(rhs.foregroundColor) {
return false
}
if lhs.peer != rhs.peer {
return false
}
if lhs.unreadCount != rhs.unreadCount {
return false
}
if lhs.location != rhs.location {
return false
}
if lhs.expandOffset != rhs.expandOffset {
return false
}
if lhs.freezeProgress != rhs.freezeProgress {
return false
}
if lhs.absoluteRect != rhs.absoluteRect {
return false
}
if lhs.absoluteSize != rhs.absoluteSize {
return false
}
if lhs.wallpaperNode !== rhs.wallpaperNode {
return false
}
return true
}
final class View: UIView {
private let backgroundScalingContainer: ASDisplayNode
private let backgroundNode: WallpaperBlurNode
private let backgroundFolderMask: UIImageView
private let backgroundClippingNode: ASDisplayNode
private let avatarView = ComponentHostView<Empty>()
private let checkView = ComponentHostView<Empty>()
private let arrowNode: ASImageNode
private let avatarScalingContainer: ASDisplayNode
private let avatarExtraScalingContainer: ASDisplayNode
private let avatarOffsetContainer: ASDisplayNode
private let arrowOffsetContainer: ASDisplayNode
private let titleOffsetContainer: ASDisplayNode
private let titleBackgroundNode: WallpaperBlurNode
private let titleNode: ImmediateTextNode
private var isFullyExpanded: Bool = false
private var validForegroundColor: UIColor?
init() {
self.backgroundScalingContainer = ASDisplayNode()
self.backgroundNode = WallpaperBlurNode()
self.backgroundNode.clipsToBounds = true
self.backgroundFolderMask = UIImageView()
self.backgroundFolderMask.image = UIImage(bundleImageName: "Chat/OverscrollFolder")?.stretchableImage(withLeftCapWidth: 0, topCapHeight: 40)
self.backgroundClippingNode = ASDisplayNode()
self.backgroundClippingNode.clipsToBounds = true
self.arrowNode = ASImageNode()
self.avatarScalingContainer = ASDisplayNode()
self.avatarExtraScalingContainer = ASDisplayNode()
self.avatarOffsetContainer = ASDisplayNode()
self.arrowOffsetContainer = ASDisplayNode()
self.titleOffsetContainer = ASDisplayNode()
self.titleBackgroundNode = WallpaperBlurNode()
self.titleBackgroundNode.clipsToBounds = true
self.titleNode = ImmediateTextNode()
super.init(frame: CGRect())
self.addSubview(self.backgroundScalingContainer.view)
self.backgroundClippingNode.addSubnode(self.backgroundNode)
self.backgroundScalingContainer.addSubnode(self.backgroundClippingNode)
self.avatarScalingContainer.view.addSubview(self.avatarView)
self.avatarScalingContainer.view.addSubview(self.checkView)
self.avatarExtraScalingContainer.addSubnode(self.avatarScalingContainer)
self.avatarOffsetContainer.addSubnode(self.avatarExtraScalingContainer)
self.arrowOffsetContainer.addSubnode(self.arrowNode)
self.backgroundNode.addSubnode(self.arrowOffsetContainer)
self.addSubnode(self.avatarOffsetContainer)
self.titleOffsetContainer.addSubnode(self.titleBackgroundNode)
self.titleOffsetContainer.addSubnode(self.titleNode)
self.addSubnode(self.titleOffsetContainer)
}
required init?(coder aDecoder: NSCoder) {
preconditionFailure()
}
func update(component: OverscrollContentsComponent, availableSize: CGSize, transition: Transition) -> CGSize {
if let _ = component.peer {
self.avatarView.isHidden = false
self.checkView.isHidden = true
} else {
self.avatarView.isHidden = true
self.checkView.isHidden = false
}
let fullHeight: CGFloat = 94.0
let backgroundWidth: CGFloat = 56.0
let minBackgroundHeight: CGFloat = backgroundWidth + 5.0
let avatarInset: CGFloat = 6.0
let apparentExpandOffset: CGFloat
if component.freezeProgress {
apparentExpandOffset = fullHeight
} else {
apparentExpandOffset = component.expandOffset
}
let isFullyExpanded = apparentExpandOffset >= fullHeight
let isFolderMask: Bool
switch component.location {
case .archived, .folder:
isFolderMask = true
default:
isFolderMask = false
}
let expandProgress: CGFloat = max(0.1, min(1.0, apparentExpandOffset / fullHeight))
let trueExpandProgress: CGFloat = max(0.1, min(1.0, component.expandOffset / fullHeight))
func interpolate(from: CGFloat, to: CGFloat, value: CGFloat) -> CGFloat {
return (1.0 - value) * from + value * to
}
let backgroundHeight: CGFloat = interpolate(from: minBackgroundHeight, to: fullHeight, value: trueExpandProgress)
let backgroundFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - backgroundWidth) / 2.0), y: fullHeight - backgroundHeight), size: CGSize(width: backgroundWidth, height: backgroundHeight))
let alphaProgress: CGFloat = max(0.0, min(1.0, apparentExpandOffset / 10.0))
let maxAvatarScale: CGFloat = 1.0
var avatarExpandProgress: CGFloat = max(0.01, min(maxAvatarScale, apparentExpandOffset / fullHeight))
avatarExpandProgress *= expandProgress
let avatarOffsetProgress = interpolate(from: 0.1, to: 1.0, value: avatarExpandProgress)
transition.setAlpha(view: self.backgroundScalingContainer.view, alpha: alphaProgress)
transition.setFrame(view: self.backgroundScalingContainer.view, frame: CGRect(origin: CGPoint(x: floor(availableSize.width / 2.0), y: fullHeight), size: CGSize(width: 0.0, height: 0.0)))
transition.setSublayerTransform(view: self.backgroundScalingContainer.view, transform: CATransform3DMakeScale(expandProgress, expandProgress, 1.0))
transition.setFrame(view: self.backgroundNode.view, frame: CGRect(origin: CGPoint(x: 0.0, y: fullHeight - backgroundFrame.size.height), size: backgroundFrame.size))
self.backgroundNode.update(rect: backgroundFrame.offsetBy(dx: component.absoluteRect.minX, dy: component.absoluteRect.minY), within: component.absoluteSize, color: component.backgroundColor, wallpaperNode: component.wallpaperNode, transition: .immediate)
self.backgroundFolderMask.frame = CGRect(origin: CGPoint(), size: backgroundFrame.size)
let avatarFrame = CGRect(origin: CGPoint(x: floor(-backgroundWidth / 2.0), y: floor(-backgroundWidth / 2.0)), size: CGSize(width: backgroundWidth, height: backgroundWidth))
self.avatarView.frame = avatarFrame
transition.setFrame(view: self.avatarOffsetContainer.view, frame: CGRect())
transition.setFrame(view: self.avatarScalingContainer.view, frame: CGRect())
transition.setFrame(view: self.avatarExtraScalingContainer.view, frame: CGRect(origin: CGPoint(x: availableSize.width / 2.0, y: fullHeight - backgroundWidth / 2.0), size: CGSize()).offsetBy(dx: 0.0, dy: (1.0 - avatarOffsetProgress) * backgroundWidth * 0.5))
transition.setSublayerTransform(view: self.avatarScalingContainer.view, transform: CATransform3DMakeScale(avatarExpandProgress, avatarExpandProgress, 1.0))
let titleText: String
if let peer = component.peer {
titleText = peer.compactDisplayTitle
} else {
titleText = component.context.sharedContext.currentPresentationData.with({ $0 }).strings.Chat_NavigationNoChannels
}
self.titleNode.attributedText = NSAttributedString(string: titleText, font: Font.semibold(13.0), textColor: component.foregroundColor)
let titleSize = self.titleNode.updateLayout(CGSize(width: availableSize.width - 32.0, height: 100.0))
let titleBackgroundSize = CGSize(width: titleSize.width + 18.0, height: titleSize.height + 8.0)
let titleBackgroundFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleBackgroundSize.width) / 2.0), y: fullHeight - titleBackgroundSize.height - 8.0), size: titleBackgroundSize)
self.titleBackgroundNode.frame = titleBackgroundFrame
self.titleBackgroundNode.update(rect: titleBackgroundFrame.offsetBy(dx: component.absoluteRect.minX, dy: component.absoluteRect.minY), within: component.absoluteSize, color: component.backgroundColor, wallpaperNode: component.wallpaperNode, transition: .immediate)
self.titleBackgroundNode.cornerRadius = min(titleBackgroundFrame.width, titleBackgroundFrame.height) / 2.0
self.titleNode.frame = titleSize.centered(in: titleBackgroundFrame)
let backgroundClippingFrame = CGRect(origin: CGPoint(x: floor(-backgroundWidth / 2.0), y: -fullHeight), size: CGSize(width: backgroundWidth, height: isFullyExpanded ? backgroundWidth : fullHeight))
self.backgroundClippingNode.cornerRadius = isFolderMask ? 10.0 : backgroundWidth / 2.0
self.backgroundNode.cornerRadius = isFolderMask ? 0.0 : backgroundWidth / 2.0
self.backgroundNode.view.mask = isFolderMask ? self.backgroundFolderMask : nil
if !(self.validForegroundColor?.isEqual(component.foregroundColor) ?? false) {
self.validForegroundColor = component.foregroundColor
self.arrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/OverscrollArrow"), color: component.foregroundColor)
}
if let arrowImage = self.arrowNode.image {
self.arrowNode.frame = CGRect(origin: CGPoint(x: floor((backgroundWidth - arrowImage.size.width) / 2.0), y: floor((backgroundWidth - arrowImage.size.width) / 2.0)), size: arrowImage.size)
}
let transformTransition: ContainedViewLayoutTransition
if self.isFullyExpanded != isFullyExpanded {
self.isFullyExpanded = isFullyExpanded
transformTransition = .animated(duration: 0.12, curve: .easeInOut)
if isFullyExpanded {
func animateBounce(layer: CALayer) {
layer.animateScale(from: 1.0, to: 1.1, duration: 0.1, removeOnCompletion: false, completion: { [weak layer] _ in
layer?.animateScale(from: 1.1, to: 1.0, duration: 0.14, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue)
})
}
animateBounce(layer: self.backgroundClippingNode.layer)
animateBounce(layer: self.avatarExtraScalingContainer.layer)
func animateOffsetBounce(layer: CALayer) {
let firstAnimation = layer.makeAnimation(from: 0.0 as NSNumber, to: -5.0 as NSNumber, keyPath: "transform.translation.y", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.1, removeOnCompletion: false, additive: true, completion: { [weak layer] _ in
guard let layer = layer else {
return
}
let secondAnimation = layer.makeAnimation(from: -5.0 as NSNumber, to: 0.0 as NSNumber, keyPath: "transform.translation.y", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.14, removeOnCompletion: true, additive: true)
layer.add(secondAnimation, forKey: "bounceY")
})
layer.add(firstAnimation, forKey: "bounceY")
}
animateOffsetBounce(layer: self.layer)
}
} else {
transformTransition = .immediate
}
let checkSize: CGFloat = 56.0
self.checkView.frame = CGRect(origin: CGPoint(x: floor(-checkSize / 2.0), y: floor(-checkSize / 2.0)), size: CGSize(width: checkSize, height: checkSize))
let _ = self.checkView.update(
transition: Transition(animation: transformTransition.isAnimated ? .curve(duration: 0.2, curve: .easeInOut) : .none),
component: AnyComponent(CheckComponent(
color: component.foregroundColor,
lineWidth: 3.0,
value: isFullyExpanded ? 1.0 : 0.0
)),
environment: {},
containerSize: CGSize(width: checkSize, height: checkSize)
)
if let peer = component.peer {
let _ = self.avatarView.update(
transition: Transition(animation: transformTransition.isAnimated ? .curve(duration: 0.2, curve: .easeInOut) : .none),
component: AnyComponent(AvatarComponent(
context: component.context,
peer: peer,
badge: isFullyExpanded ? AvatarComponent.Badge(count: component.unreadCount, backgroundColor: component.backgroundColor, foregroundColor: component.foregroundColor) : nil,
rect: avatarFrame.offsetBy(dx: self.avatarExtraScalingContainer.frame.midX + component.absoluteRect.minX, dy: self.avatarExtraScalingContainer.frame.midY + component.absoluteRect.minY),
withinSize: component.absoluteSize,
wallpaperNode: component.wallpaperNode
)),
environment: {},
containerSize: self.avatarView.bounds.size
)
}
transformTransition.updateAlpha(node: self.backgroundNode, alpha: (isFullyExpanded && component.peer != nil) ? 0.0 : 1.0)
transformTransition.updateAlpha(node: self.arrowNode, alpha: isFullyExpanded ? 0.0 : 1.0)
transformTransition.updateSublayerTransformOffset(layer: self.avatarOffsetContainer.layer, offset: CGPoint(x: 0.0, y: isFullyExpanded ? -(fullHeight - backgroundWidth) : 0.0))
transformTransition.updateSublayerTransformOffset(layer: self.arrowOffsetContainer.layer, offset: CGPoint(x: 0.0, y: isFullyExpanded ? -(fullHeight - backgroundWidth) : 0.0))
transformTransition.updateSublayerTransformOffset(layer: self.titleOffsetContainer.layer, offset: CGPoint(x: 0.0, y: isFullyExpanded ? 0.0 : (titleBackgroundSize.height + 50.0)))
transformTransition.updateSublayerTransformScale(node: self.avatarExtraScalingContainer, scale: isFullyExpanded ? 1.0 : ((backgroundWidth - avatarInset * 2.0) / backgroundWidth))
transformTransition.updateFrame(node: self.backgroundClippingNode, frame: backgroundClippingFrame)
return CGSize(width: availableSize.width, height: fullHeight)
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
final class ChatOverscrollControl: CombinedComponent {
let backgroundColor: UIColor
let foregroundColor: UIColor
let peer: EnginePeer?
let unreadCount: Int
let location: TelegramEngine.NextUnreadChannelLocation
let context: AccountContext
let expandDistance: CGFloat
let freezeProgress: Bool
let absoluteRect: CGRect
let absoluteSize: CGSize
let wallpaperNode: WallpaperBackgroundNode?
init(
backgroundColor: UIColor,
foregroundColor: UIColor,
peer: EnginePeer?,
unreadCount: Int,
location: TelegramEngine.NextUnreadChannelLocation,
context: AccountContext,
expandDistance: CGFloat,
freezeProgress: Bool,
absoluteRect: CGRect,
absoluteSize: CGSize,
wallpaperNode: WallpaperBackgroundNode?
) {
self.backgroundColor = backgroundColor
self.foregroundColor = foregroundColor
self.peer = peer
self.unreadCount = unreadCount
self.location = location
self.context = context
self.expandDistance = expandDistance
self.freezeProgress = freezeProgress
self.absoluteRect = absoluteRect
self.absoluteSize = absoluteSize
self.wallpaperNode = wallpaperNode
}
static func ==(lhs: ChatOverscrollControl, rhs: ChatOverscrollControl) -> Bool {
if !lhs.backgroundColor.isEqual(rhs.backgroundColor) {
return false
}
if !lhs.foregroundColor.isEqual(rhs.foregroundColor) {
return false
}
if lhs.peer != rhs.peer {
return false
}
if lhs.unreadCount != rhs.unreadCount {
return false
}
if lhs.location != rhs.location {
return false
}
if lhs.context !== rhs.context {
return false
}
if lhs.expandDistance != rhs.expandDistance {
return false
}
if lhs.freezeProgress != rhs.freezeProgress {
return false
}
if lhs.absoluteRect != rhs.absoluteRect {
return false
}
if lhs.absoluteSize != rhs.absoluteSize {
return false
}
if lhs.wallpaperNode !== rhs.wallpaperNode {
return false
}
return true
}
static var body: Body {
let contents = Child(OverscrollContentsComponent.self)
return { context in
let contents = contents.update(
component: OverscrollContentsComponent(
context: context.component.context,
backgroundColor: context.component.backgroundColor,
foregroundColor: context.component.foregroundColor,
peer: context.component.peer,
unreadCount: context.component.unreadCount,
location: context.component.location,
expandOffset: context.component.expandDistance,
freezeProgress: context.component.freezeProgress,
absoluteRect: context.component.absoluteRect,
absoluteSize: context.component.absoluteSize,
wallpaperNode: context.component.wallpaperNode
),
availableSize: context.availableSize,
transition: context.transition
)
let size = CGSize(width: context.availableSize.width, height: contents.size.height)
context.add(contents
.position(CGPoint(x: size.width / 2.0, y: size.height / 2.0))
)
return size
}
}
}
final class ChatInputPanelOverscrollNode: ASDisplayNode {
let text: (String, [(Int, NSRange)])
let priority: Int
private let titleNode: ImmediateTextNode
init(text: (String, [(Int, NSRange)]), color: UIColor, priority: Int) {
self.text = text
self.priority = priority
self.titleNode = ImmediateTextNode()
super.init()
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: color)
let bold = MarkdownAttributeSet(font: Font.bold(14.0), textColor: color)
self.titleNode.attributedText = addAttributesToStringWithRanges(text, body: body, argumentAttributes: [0: bold])
self.addSubnode(self.titleNode)
}
func update(size: CGSize) {
let titleSize = self.titleNode.updateLayout(size)
self.titleNode.frame = titleSize.centered(in: CGRect(origin: CGPoint(), size: size))
}
}