import UIKit import ComponentFlow import Display import AsyncDisplayKit import TelegramCore import Postbox import AccountContext import AvatarNode import TextFormat import Markdown import WallpaperBackgroundNode import EmojiStatusComponent import TelegramPresentationData import TextNodeWithEntities 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: ComponentTransition) -> 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, transition: ComponentTransition) -> 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: ComponentTransition) -> 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, transition: ComponentTransition) -> 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: ComponentTransition) -> 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, transition: ComponentTransition) -> 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 } } } public struct ChatOverscrollThreadData: Equatable { public var id: Int64 public var data: MessageHistoryThreadData public init(id: Int64, data: MessageHistoryThreadData) { self.id = id self.data = data } } 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 threadData: ChatOverscrollThreadData? let badge: Badge? let rect: CGRect let withinSize: CGSize let wallpaperNode: WallpaperBackgroundNode? init( context: AccountContext, peer: EnginePeer, threadData: ChatOverscrollThreadData?, badge: Badge?, rect: CGRect, withinSize: CGSize, wallpaperNode: WallpaperBackgroundNode? ) { self.context = context self.peer = peer self.threadData = threadData 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.threadData != rhs.threadData { 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 avatarContainer: UIView private var avatarNode: AvatarNode? private var avatarIcon: ComponentView? private let avatarMask: CAShapeLayer private var badgeView: ComponentHostView? init() { self.avatarContainer = UIView() self.avatarMask = CAShapeLayer() super.init(frame: CGRect()) self.addSubview(self.avatarContainer) } required init?(coder aDecoder: NSCoder) { preconditionFailure() } func update(component: AvatarComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { self.avatarContainer.frame = CGRect(origin: CGPoint(), size: availableSize) let theme = component.context.sharedContext.currentPresentationData.with({ $0 }).theme if let threadData = component.threadData { if let avatarNode = self.avatarNode { self.avatarNode = nil avatarNode.view.removeFromSuperview() } let avatarIconContent: EmojiStatusComponent.Content if threadData.id == 1 { avatarIconContent = .image(image: PresentationResourcesChatList.generalTopicIcon(theme)) } else if let fileId = threadData.data.info.icon, fileId != 0 { avatarIconContent = .animation(content: .customEmoji(fileId: fileId), size: CGSize(width: 48.0, height: 48.0), placeholderColor: theme.list.mediaPlaceholderColor, themeColor: theme.list.itemAccentColor, loopMode: .count(0)) } else { avatarIconContent = .topic(title: String(threadData.data.info.title.prefix(1)), color: threadData.data.info.iconColor, size: CGSize(width: 32.0, height: 32.0)) } let avatarIcon: ComponentView if let current = self.avatarIcon { avatarIcon = current } else { avatarIcon = ComponentView() self.avatarIcon = avatarIcon } let avatarIconComponent = EmojiStatusComponent( context: component.context, animationCache: component.context.animationCache, animationRenderer: component.context.animationRenderer, content: avatarIconContent, isVisibleForAnimations: true, action: nil ) let iconSize = avatarIcon.update( transition: .immediate, component: AnyComponent(avatarIconComponent), environment: {}, containerSize: availableSize ) let avatarIconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) / 2.0), y: floor((availableSize.height - iconSize.height) / 2.0)), size: iconSize) if let avatarIconView = avatarIcon.view { if avatarIconView.superview == nil { self.avatarContainer.addSubview(avatarIconView) } avatarIconView.frame = avatarIconFrame } } else { if let avatarIcon = self.avatarIcon { self.avatarIcon = nil avatarIcon.view?.removeFromSuperview() } let avatarNode: AvatarNode if let current = self.avatarNode { avatarNode = current } else { avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0)) self.avatarNode = avatarNode self.avatarContainer.addSubview(avatarNode.view) } avatarNode.frame = CGRect(origin: CGPoint(), size: availableSize) 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.avatarContainer.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.avatarContainer.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.avatarContainer.layer.mask = nil } return availableSize } } func makeView() -> View { return View() } func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> 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: ComponentTransition) -> 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, transition: ComponentTransition) -> 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 threadData: ChatOverscrollThreadData? let isForumThread: Bool 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?, threadData: ChatOverscrollThreadData?, isForumThread: Bool, 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.threadData = threadData self.isForumThread = isForumThread 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.threadData != rhs.threadData { return false } if lhs.isForumThread != rhs.isForumThread { 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: ComponentTransition) -> 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 threadData = component.threadData { titleText = threadData.data.info.title } else if let peer = component.peer { titleText = peer.compactDisplayTitle } else if component.isForumThread { titleText = component.context.sharedContext.currentPresentationData.with({ $0 }).strings.Chat_NavigationNoTopics } 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: ComponentTransition(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: ComponentTransition(animation: transformTransition.isAnimated ? .curve(duration: 0.2, curve: .easeInOut) : .none), component: AnyComponent(AvatarComponent( context: component.context, peer: peer, threadData: component.threadData, badge: (isFullyExpanded && component.unreadCount != 0) ? 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, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } public final class ChatOverscrollControl: CombinedComponent { let backgroundColor: UIColor let foregroundColor: UIColor let peer: EnginePeer? let threadData: ChatOverscrollThreadData? let isForumThread: Bool 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? public init( backgroundColor: UIColor, foregroundColor: UIColor, peer: EnginePeer?, threadData: ChatOverscrollThreadData?, isForumThread: Bool, 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.threadData = threadData self.isForumThread = isForumThread self.unreadCount = unreadCount self.location = location self.context = context self.expandDistance = expandDistance self.freezeProgress = freezeProgress self.absoluteRect = absoluteRect self.absoluteSize = absoluteSize self.wallpaperNode = wallpaperNode } public 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.threadData != rhs.threadData { return false } if lhs.isForumThread != rhs.isForumThread { 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 } public 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, threadData: context.component.threadData, isForumThread: context.component.isForumThread, 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 } } } public final class ChatInputPanelOverscrollNode: ASDisplayNode { public let text: NSAttributedString public let priority: Int private let titleNode: ImmediateTextNodeWithEntities public init(context: AccountContext, text: NSAttributedString, color: UIColor, priority: Int) { self.text = text self.priority = priority self.titleNode = ImmediateTextNodeWithEntities() super.init() let attributedText = NSMutableAttributedString(string: text.string) attributedText.addAttribute(.font, value: Font.regular(14.0), range: NSRange(location: 0, length: text.length)) attributedText.addAttribute(.foregroundColor, value: color, range: NSRange(location: 0, length: text.length)) text.enumerateAttributes(in: NSRange(location: 0, length: text.length), using: { attributes, range, _ in for (key, value) in attributes { if key == ChatTextInputAttributes.bold { attributedText.addAttribute(.font, value: Font.bold(14.0), range: range) } else if key == ChatTextInputAttributes.italic { attributedText.addAttribute(.font, value: Font.italic(14.0), range: range) } else if key == ChatTextInputAttributes.monospace { attributedText.addAttribute(.font, value: Font.monospace(14.0), range: range) } else { attributedText.addAttribute(key, value: value, range: range) } } }) self.titleNode.attributedText = attributedText self.titleNode.visibility = true self.titleNode.arguments = TextNodeWithEntities.Arguments( context: context, cache: context.animationCache, renderer: context.animationRenderer, placeholderColor: color.withMultipliedAlpha(0.1), attemptSynchronous: true ) self.addSubnode(self.titleNode) } public func update(size: CGSize) { let titleSize = self.titleNode.updateLayout(size) self.titleNode.frame = titleSize.centered(in: CGRect(origin: CGPoint(), size: size)) } }