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, 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, 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, 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? 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 let animateIn = self.badgeView == nil if let current = self.badgeView { badgeView = current } else { badgeView = ComponentHostView() 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, transition: Transition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } private final class WallpaperBlurNode: ASDisplayNode { private var backgroundNode: WallpaperBackgroundNode.BubbleBackgroundNode? 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, 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() private let checkView = ComponentHostView() 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, 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)) } }