import Foundation import UIKit import AsyncDisplayKit import TelegramPresentationData import ChatPresentationInterfaceState import ComponentFlow import AccountContext import ViewControllerComponent import TelegramCore import SwiftSignalKit import Display import MultilineTextComponent import ButtonComponent import PlainButtonComponent import Markdown import EmojiStatusComponent import SliderComponent import RoundedRectWithTailPath import AvatarNode import BundleIconComponent private final class BalanceComponent: CombinedComponent { let context: AccountContext let theme: PresentationTheme let strings: PresentationStrings let balance: Int64? init( context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, balance: Int64? ) { self.context = context self.theme = theme self.strings = strings self.balance = balance } static func ==(lhs: BalanceComponent, rhs: BalanceComponent) -> Bool { if lhs.context !== rhs.context { return false } if lhs.theme !== rhs.theme { return false } if lhs.strings !== rhs.strings { return false } if lhs.balance != rhs.balance { return false } return true } static var body: Body { let title = Child(MultilineTextComponent.self) let balance = Child(MultilineTextComponent.self) let icon = Child(EmojiStatusComponent.self) return { context in var size = CGSize(width: 0.0, height: 0.0) //TODO:localize let title = title.update( component: MultilineTextComponent( text: .plain(NSAttributedString(string: "Balance", font: Font.regular(14.0), textColor: context.component.theme.list.itemPrimaryTextColor)) ), availableSize: context.availableSize, transition: .immediate ) size.width = max(size.width, title.size.width) size.height += title.size.height let balanceText: String if let value = context.component.balance { balanceText = "\(value)" } else { balanceText = "..." } let balance = balance.update( component: MultilineTextComponent( text: .plain(NSAttributedString(string: balanceText, font: Font.medium(15.0), textColor: context.component.theme.list.itemPrimaryTextColor)) ), availableSize: context.availableSize, transition: .immediate ) let iconSize = CGSize(width: 18.0, height: 18.0) let icon = icon.update( component: EmojiStatusComponent( context: context.component.context, animationCache: context.component.context.animationCache, animationRenderer: context.component.context.animationRenderer, content: .animation( content: .customEmoji(fileId: MessageReaction.starsReactionId), size: iconSize, placeholderColor: .gray, themeColor: nil, loopMode: .count(0) ), isVisibleForAnimations: true, action: nil ), availableSize: iconSize, transition: context.transition ) let titleSpacing: CGFloat = 1.0 let iconSpacing: CGFloat = 2.0 size.height += titleSpacing size.width = max(size.width, icon.size.width + iconSpacing + balance.size.width) size.height += balance.size.height context.add( title.position( title.size.centered(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: title.size)).center ) ) context.add( balance.position( balance.size.centered(in: CGRect(origin: CGPoint(x: icon.size.width + iconSpacing, y: title.size.height + titleSpacing), size: balance.size)).center ) ) context.add( icon.position( icon.size.centered(in: CGRect(origin: CGPoint(x: 0.0, y: title.size.height + titleSpacing), size: icon.size)).center ) ) return size } } } private final class BadgeComponent: Component { enum Direction { case left case right } let theme: PresentationTheme let title: String let inertiaDirection: Direction? init( theme: PresentationTheme, title: String, inertiaDirection: Direction? ) { self.theme = theme self.title = title self.inertiaDirection = inertiaDirection } static func ==(lhs: BadgeComponent, rhs: BadgeComponent) -> Bool { if lhs.theme !== rhs.theme { return false } if lhs.title != rhs.title { return false } if lhs.inertiaDirection != rhs.inertiaDirection { return false } return true } final class View: UIView { private let badgeView: UIView private let badgeMaskView: UIView private let badgeShapeLayer = SimpleShapeLayer() private let badgeForeground: SimpleLayer private let badgeIcon: UIImageView private let badgeLabel: BadgeLabelView private let badgeLabelMaskView = UIImageView() private var badgeTailPosition: CGFloat = 0.0 private var badgeShapeArguments: (Double, Double, CGSize, CGFloat, CGFloat)? private var component: BadgeComponent? private var previousAvailableSize: CGSize? private var previousInertiaDirection: BadgeComponent.Direction? override init(frame: CGRect) { self.badgeView = UIView() self.badgeView.alpha = 0.0 self.badgeShapeLayer.fillColor = UIColor.white.cgColor self.badgeShapeLayer.transform = CATransform3DMakeScale(1.0, -1.0, 1.0) self.badgeMaskView = UIView() self.badgeMaskView.layer.addSublayer(self.badgeShapeLayer) self.badgeView.mask = self.badgeMaskView self.badgeForeground = SimpleLayer() self.badgeIcon = UIImageView() self.badgeIcon.contentMode = .center self.badgeLabel = BadgeLabelView() let _ = self.badgeLabel.update(value: "0", transition: .immediate) self.badgeLabel.mask = self.badgeLabelMaskView super.init(frame: frame) self.addSubview(self.badgeView) self.badgeView.layer.addSublayer(self.badgeForeground) self.badgeView.addSubview(self.badgeIcon) self.badgeView.addSubview(self.badgeLabel) self.badgeLabelMaskView.contentMode = .scaleToFill self.badgeLabelMaskView.image = generateImage(CGSize(width: 2.0, height: 36.0), rotatedContext: { size, context in let bounds = CGRect(origin: .zero, size: size) context.clear(bounds) let colorsArray: [CGColor] = [ UIColor(rgb: 0xffffff, alpha: 0.0).cgColor, UIColor(rgb: 0xffffff).cgColor, UIColor(rgb: 0xffffff).cgColor, UIColor(rgb: 0xffffff, alpha: 0.0).cgColor, ] var locations: [CGFloat] = [0.0, 0.24, 0.76, 1.0] let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray as CFArray, locations: &locations)! context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) }) self.isUserInteractionEnabled = false } required init(coder: NSCoder) { preconditionFailure() } func update(component: BadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { if self.component == nil { self.badgeIcon.image = UIImage(bundleImageName: "Premium/SendStarsStarSliderIcon")?.withRenderingMode(.alwaysTemplate) } self.component = component self.badgeIcon.tintColor = .white self.badgeLabel.color = .white let badgeLabelSize = self.badgeLabel.update(value: component.title, transition: .easeInOut(duration: 0.12)) let countWidth: CGFloat = badgeLabelSize.width + 3.0 let badgeWidth: CGFloat = countWidth + 54.0 let badgeSize = CGSize(width: badgeWidth, height: 48.0) let badgeFullSize = CGSize(width: badgeWidth, height: 48.0 + 12.0) self.badgeMaskView.frame = CGRect(origin: .zero, size: badgeFullSize) self.badgeShapeLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -4.0), size: badgeFullSize) self.badgeView.bounds = CGRect(origin: .zero, size: badgeFullSize) transition.setAnchorPoint(layer: self.badgeView.layer, anchorPoint: CGPoint(x: 0.5, y: 1.0)) if component.inertiaDirection != self.previousInertiaDirection { self.previousInertiaDirection = component.inertiaDirection var angle: CGFloat = 0.0 let transition: ContainedViewLayoutTransition if let inertiaDirection = component.inertiaDirection { switch inertiaDirection { case .left: angle = 0.22 case .right: angle = -0.22 } transition = .animated(duration: 0.45, curve: .spring) } else { transition = .animated(duration: 0.45, curve: .customSpring(damping: 65.0, initialVelocity: 0.0)) } transition.updateTransformRotation(view: self.badgeView, angle: angle) } self.badgeForeground.bounds = CGRect(origin: CGPoint(), size: CGSize(width: badgeFullSize.width * 3.0, height: badgeFullSize.height)) if self.badgeForeground.animation(forKey: "movement") == nil { self.badgeForeground.position = CGPoint(x: badgeSize.width * 3.0 / 2.0 - self.badgeForeground.frame.width * 0.35, y: badgeFullSize.height / 2.0) } self.badgeIcon.frame = CGRect(x: 10.0, y: 9.0, width: 30.0, height: 30.0) self.badgeLabelMaskView.frame = CGRect(x: 0.0, y: 0.0, width: 100.0, height: 36.0) self.badgeView.alpha = 1.0 let size = badgeSize transition.setFrame(view: self.badgeLabel, frame: CGRect(origin: CGPoint(x: 14.0 + floorToScreenPixels((badgeFullSize.width - badgeLabelSize.width) / 2.0), y: 5.0), size: badgeLabelSize)) if self.previousAvailableSize != availableSize { self.previousAvailableSize = availableSize let activeColors: [UIColor] = [ UIColor(rgb: 0xFFAB03), UIColor(rgb: 0xFFCB37) ] var locations: [CGFloat] = [] let delta = 1.0 / CGFloat(activeColors.count - 1) for i in 0 ..< activeColors.count { locations.append(delta * CGFloat(i)) } let gradient = generateGradientImage(size: CGSize(width: 200.0, height: 60.0), colors: activeColors, locations: locations, direction: .horizontal) self.badgeForeground.contentsGravity = .resizeAspectFill self.badgeForeground.contents = gradient?.cgImage self.setupGradientAnimations() } return size } func adjustTail(size: CGSize, overflowWidth: CGFloat) { var tailPosition = size.width * 0.5 tailPosition += overflowWidth tailPosition = max(0.0, min(size.width, tailPosition)) self.badgeShapeLayer.path = generateRoundedRectWithTailPath(rectSize: size, tailPosition: tailPosition / size.width).cgPath } private func setupGradientAnimations() { guard let _ = self.component else { return } if let _ = self.badgeForeground.animation(forKey: "movement") { } else { CATransaction.begin() let badgeOffset = (self.badgeForeground.frame.width - self.badgeView.bounds.width) / 2.0 let badgePreviousValue = self.badgeForeground.position.x var badgeNewValue: CGFloat = badgeOffset if badgeOffset - badgePreviousValue < self.badgeForeground.frame.width * 0.25 { badgeNewValue -= self.badgeForeground.frame.width * 0.35 } self.badgeForeground.position = CGPoint(x: badgeNewValue, y: self.badgeForeground.bounds.size.height / 2.0) let badgeAnimation = CABasicAnimation(keyPath: "position.x") badgeAnimation.duration = 4.5 badgeAnimation.fromValue = badgePreviousValue badgeAnimation.toValue = badgeNewValue badgeAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) CATransaction.setCompletionBlock { [weak self] in self?.setupGradientAnimations() } self.badgeForeground.add(badgeAnimation, forKey: "movement") CATransaction.commit() } } } func makeView() -> View { return View(frame: CGRect()) } func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } private final class PeerBadgeComponent: Component { let theme: PresentationTheme let title: String init( theme: PresentationTheme, title: String ) { self.theme = theme self.title = title } static func ==(lhs: PeerBadgeComponent, rhs: PeerBadgeComponent) -> Bool { if lhs.theme !== rhs.theme { return false } if lhs.title != rhs.title { return false } return true } final class View: UIView { private let backgroundMaskLayer = SimpleLayer() private let backgroundLayer = SimpleLayer() private let title = ComponentView() private let icon = ComponentView() private var component: PeerBadgeComponent? override init(frame: CGRect) { super.init(frame: frame) self.layer.addSublayer(self.backgroundMaskLayer) self.layer.addSublayer(self.backgroundLayer) } required init(coder: NSCoder) { preconditionFailure() } func update(component: PeerBadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component let iconSize = self.icon.update( transition: .immediate, component: AnyComponent(BundleIconComponent( name: "Premium/SendStarsPeerBadgeStarIcon", tintColor: .white) ), environment: {}, containerSize: CGSize(width: 100.0, height: 100.0) ) let sideInset: CGFloat = 3.0 let titleSpacing: CGFloat = 1.0 let titleSize = self.title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString(string: component.title, font: Font.bold(9.0), textColor: .white)) )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - titleSpacing - iconSize.width, height: 100.0) ) let contentSize = CGSize(width: iconSize.width + titleSpacing + titleSize.width, height: titleSize.height) let size = CGSize(width: contentSize.width + sideInset * 2.0, height: contentSize.height + 3.0 * 2.0) self.backgroundMaskLayer.backgroundColor = component.theme.list.plainBackgroundColor.cgColor self.backgroundLayer.backgroundColor = UIColor(rgb: 0xFFB10D).cgColor let backgroundFrame = CGRect(origin: CGPoint(), size: size) self.backgroundLayer.frame = backgroundFrame let badkgroundMaskFrame = backgroundFrame.insetBy(dx: -1.0 - UIScreenPixel, dy: -1.0 - UIScreenPixel) self.backgroundMaskLayer.frame = badkgroundMaskFrame self.backgroundLayer.cornerRadius = backgroundFrame.height * 0.5 self.backgroundMaskLayer.cornerRadius = badkgroundMaskFrame.height * 0.5 let titleFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + sideInset + iconSize.width + titleSpacing, y: floor((backgroundFrame.height - titleSize.height) * 0.5)), size: titleSize) if let titleView = self.title.view { if titleView.superview == nil { self.addSubview(titleView) } titleView.frame = titleFrame } let iconFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + sideInset + 1.0, y: floor((backgroundFrame.height - iconSize.height) * 0.5)), size: iconSize) if let iconView = self.icon.view { if iconView.superview == nil { self.addSubview(iconView) } iconView.frame = iconFrame } return size } } func makeView() -> View { return View(frame: CGRect()) } func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } private final class PeerComponent: Component { let context: AccountContext let theme: PresentationTheme let strings: PresentationStrings let peer: EnginePeer init( context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, peer: EnginePeer ) { self.context = context self.theme = theme self.strings = strings self.peer = peer } static func ==(lhs: PeerComponent, rhs: PeerComponent) -> Bool { if lhs.context !== rhs.context { return false } if lhs.theme !== rhs.theme { return false } if lhs.strings !== rhs.strings { return false } if lhs.peer != rhs.peer { return false } return true } final class View: UIView { private var avatarNode: AvatarNode? private let badge = ComponentView() private let title = ComponentView() private var component: PeerComponent? override init(frame: CGRect) { super.init(frame: frame) } required init(coder: NSCoder) { preconditionFailure() } func update(component: PeerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component let avatarNode: AvatarNode if let current = self.avatarNode { avatarNode = current } else { avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 24.0)) self.avatarNode = avatarNode self.addSubview(avatarNode.view) } let avatarSize = CGSize(width: 60.0, height: 60.0) let avatarFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: avatarSize) avatarNode.frame = avatarFrame avatarNode.setPeer(context: component.context, theme: component.theme, peer: component.peer) avatarNode.updateSize(size: avatarFrame.size) let badgeSize = self.badge.update( transition: .immediate, component: AnyComponent(PeerBadgeComponent( theme: component.theme, title: "800" )), environment: {}, containerSize: CGSize(width: 200.0, height: 200.0) ) let badgeFrame = CGRect(origin: CGPoint(x: avatarFrame.minX + floor((avatarFrame.width - badgeSize.width) * 0.5), y: avatarFrame.maxY - badgeSize.height + 3.0), size: badgeSize) if let badgeView = self.badge.view { if badgeView.superview == nil { self.addSubview(badgeView) } badgeView.frame = badgeFrame } let titleSpacing: CGFloat = 8.0 let titleSize = self.title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString(string: component.peer.compactDisplayTitle, font: Font.regular(11.0), textColor: component.theme.list.itemPrimaryTextColor)) )), environment: {}, containerSize: CGSize(width: avatarSize.width + 10.0 * 2.0, height: 100.0) ) let titleFrame = CGRect(origin: CGPoint(x: floor((avatarSize.width - titleSize.width) * 0.5), y: avatarSize.height + titleSpacing), size: titleSize) if let titleView = self.title.view { if titleView.superview == nil { self.addSubview(titleView) } titleView.frame = titleFrame } return CGSize(width: avatarSize.width, height: avatarSize.height + titleSpacing + titleSize.height) } } func makeView() -> View { return View(frame: CGRect()) } func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } private final class ChatSendStarsScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext let peer: EnginePeer let balance: Int64? let topPeers: [EnginePeer] let completion: (Int64) -> Void init( context: AccountContext, peer: EnginePeer, balance: Int64?, topPeers: [EnginePeer], completion: @escaping (Int64) -> Void ) { self.context = context self.peer = peer self.balance = balance self.topPeers = topPeers self.completion = completion } static func ==(lhs: ChatSendStarsScreenComponent, rhs: ChatSendStarsScreenComponent) -> Bool { if lhs.context !== rhs.context { return false } if lhs.peer != rhs.peer { return false } if lhs.balance != rhs.balance { return false } if lhs.topPeers != rhs.topPeers { return false } return true } private struct ItemLayout: Equatable { var containerSize: CGSize var containerInset: CGFloat var bottomInset: CGFloat var topInset: CGFloat init(containerSize: CGSize, containerInset: CGFloat, bottomInset: CGFloat, topInset: CGFloat) { self.containerSize = containerSize self.containerInset = containerInset self.bottomInset = bottomInset self.topInset = topInset } } private final class ScrollView: UIScrollView { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { return super.hitTest(point, with: event) } } final class View: UIView, UIScrollViewDelegate { private let dimView: UIView private let backgroundLayer: SimpleLayer private let navigationBarContainer: SparseContainerView private let scrollView: ScrollView private let scrollContentClippingView: SparseContainerView private let scrollContentView: UIView private let leftButton = ComponentView() private let closeButton = ComponentView() private let title = ComponentView() private let descriptionText = ComponentView() private let badgeStars = BadgeStarsView() private let slider = ComponentView() private let sliderBackground = UIView() private let sliderForeground = UIView() private let sliderStars = SliderStarsView() private let badge = ComponentView() private var topPeersLeftSeparator: SimpleLayer? private var topPeersRightSeparator: SimpleLayer? private var topPeersTitleBackground: SimpleLayer? private var topPeersTitle: ComponentView? private var topPeerItems: [EnginePeer.Id: ComponentView] = [:] private let actionButton = ComponentView() private let buttonDescriptionText = ComponentView() private let bottomOverscrollLimit: CGFloat private var ignoreScrolling: Bool = false private var component: ChatSendStarsScreenComponent? private weak var state: EmptyComponentState? private var environment: ViewControllerComponentContainer.Environment? private var itemLayout: ItemLayout? private var topOffsetDistance: CGFloat? private var amount: Int64 = 1 private var cachedStarImage: (UIImage, PresentationTheme)? private var cachedCloseImage: UIImage? override init(frame: CGRect) { self.bottomOverscrollLimit = 200.0 self.dimView = UIView() self.backgroundLayer = SimpleLayer() self.backgroundLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] self.backgroundLayer.cornerRadius = 10.0 self.navigationBarContainer = SparseContainerView() self.scrollView = ScrollView() self.scrollContentClippingView = SparseContainerView() self.scrollContentClippingView.clipsToBounds = true self.scrollContentView = UIView() super.init(frame: frame) self.addSubview(self.dimView) self.layer.addSublayer(self.backgroundLayer) self.scrollView.delaysContentTouches = true self.scrollView.canCancelContentTouches = true self.scrollView.clipsToBounds = false if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { self.scrollView.contentInsetAdjustmentBehavior = .never } if #available(iOS 13.0, *) { self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false } self.scrollView.showsVerticalScrollIndicator = false self.scrollView.showsHorizontalScrollIndicator = false self.scrollView.alwaysBounceHorizontal = false self.scrollView.alwaysBounceVertical = true self.scrollView.scrollsToTop = false self.scrollView.delegate = self self.scrollView.clipsToBounds = true self.addSubview(self.scrollContentClippingView) self.scrollContentClippingView.addSubview(self.scrollView) self.scrollView.addSubview(self.scrollContentView) self.sliderForeground.clipsToBounds = true self.sliderForeground.addSubview(self.sliderStars) self.addSubview(self.navigationBarContainer) self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func scrollViewDidScroll(_ scrollView: UIScrollView) { if !self.ignoreScrolling { self.updateScrolling(transition: .immediate) } } func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { guard let itemLayout = self.itemLayout, let topOffsetDistance = self.topOffsetDistance else { return } var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset topOffset = max(0.0, topOffset) if topOffset < topOffsetDistance { targetContentOffset.pointee.y = scrollView.contentOffset.y scrollView.setContentOffset(CGPoint(x: 0.0, y: itemLayout.topInset), animated: true) } } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if !self.bounds.contains(point) { return nil } if !self.backgroundLayer.frame.contains(point) { return self.dimView } if let result = self.navigationBarContainer.hitTest(self.convert(point, to: self.navigationBarContainer), with: event) { return result } let result = super.hitTest(point, with: event) return result } @objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { guard let environment = self.environment, let controller = environment.controller() else { return } controller.dismiss() } } private func updateScrolling(transition: ComponentTransition) { guard let environment = self.environment, let controller = environment.controller(), let itemLayout = self.itemLayout else { return } var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset topOffset = max(0.0, topOffset) transition.setTransform(layer: self.backgroundLayer, transform: CATransform3DMakeTranslation(0.0, topOffset + itemLayout.containerInset, 0.0)) transition.setPosition(view: self.navigationBarContainer, position: CGPoint(x: 0.0, y: topOffset + itemLayout.containerInset)) let topOffsetDistance: CGFloat = min(200.0, floor(itemLayout.containerSize.height * 0.25)) self.topOffsetDistance = topOffsetDistance var topOffsetFraction = topOffset / topOffsetDistance topOffsetFraction = max(0.0, min(1.0, topOffsetFraction)) let transitionFactor: CGFloat = 1.0 - topOffsetFraction controller.updateModalStyleOverlayTransitionFactor(transitionFactor, transition: transition.containedViewLayoutTransition) } func animateIn() { self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY self.scrollContentClippingView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) self.backgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) self.navigationBarContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) if let actionButtonView = self.actionButton.view { actionButtonView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) } if let buttonDescriptionTextView = self.buttonDescriptionText.view { buttonDescriptionTextView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) } } func animateOut(completion: @escaping () -> Void) { let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY self.dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) self.scrollContentClippingView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in completion() }) self.backgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) self.navigationBarContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) if let actionButtonView = self.actionButton.view { actionButtonView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) } if let buttonDescriptionTextView = self.buttonDescriptionText.view { buttonDescriptionTextView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) } } private var previousSliderValue: Float = 0.0 private var previousTimestamp: Double? private var inertiaDirection: BadgeComponent.Direction? func update(component: ChatSendStarsScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let environment = environment[ViewControllerComponentContainer.Environment.self].value let themeUpdated = self.environment?.theme !== environment.theme let resetScrolling = self.scrollView.bounds.width != availableSize.width let sideInset: CGFloat = 16.0 if self.component == nil { self.amount = 1 } self.component = component self.state = state self.environment = environment if themeUpdated { self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5) self.backgroundLayer.backgroundColor = environment.theme.list.plainBackgroundColor.cgColor var locations: [NSNumber] = [] var colors: [CGColor] = [] let numStops = 6 for i in 0 ..< numStops { let step = CGFloat(i) / CGFloat(numStops - 1) locations.append(step as NSNumber) colors.append(environment.theme.list.blocksBackgroundColor.withAlphaComponent(1.0 - step * step).cgColor) } } transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize)) var contentHeight: CGFloat = 0.0 let sliderInset: CGFloat = sideInset + 8.0 let sliderSize = self.slider.update( transition: transition, component: AnyComponent(SliderComponent( valueCount: 1000, value: 0, markPositions: false, trackBackgroundColor: .clear, trackForegroundColor: .clear, knobSize: 26.0, knobColor: .white, valueUpdated: { [weak self] value in guard let self else { return } self.amount = 1 + Int64(value) self.state?.updated(transition: .immediate) let sliderValue = Float(value) / 1000.0 let currentTimestamp = CACurrentMediaTime() if let previousTimestamp { let deltaTime = currentTimestamp - previousTimestamp let delta = sliderValue - self.previousSliderValue let deltaValue = abs(sliderValue - self.previousSliderValue) let speed = deltaValue / Float(deltaTime) let newSpeed = max(0, min(65.0, speed * 70.0)) var inertiaDirection: BadgeComponent.Direction? if newSpeed >= 1.0 { if delta > 0.0 { inertiaDirection = .right } else { inertiaDirection = .left } } if inertiaDirection != self.inertiaDirection { self.inertiaDirection = inertiaDirection self.state?.updated(transition: .immediate) } if newSpeed < 0.01 && deltaValue < 0.001 { } else { self.badgeStars.update(speed: newSpeed, delta: delta) } } self.previousSliderValue = sliderValue self.previousTimestamp = currentTimestamp }, isTrackingUpdated: { [weak self] isTracking in guard let self else { return } if !isTracking { self.previousTimestamp = nil self.badgeStars.update(speed: 0.0) } if self.inertiaDirection != nil { self.inertiaDirection = nil self.state?.updated(transition: .immediate) } } )), environment: {}, containerSize: CGSize(width: availableSize.width - sliderInset * 2.0, height: 30.0) ) let sliderFrame = CGRect(origin: CGPoint(x: sliderInset, y: contentHeight + 127.0), size: sliderSize) if let sliderView = self.slider.view { if sliderView.superview == nil { self.scrollContentView.addSubview(self.badgeStars) self.scrollContentView.addSubview(self.sliderBackground) self.scrollContentView.addSubview(self.sliderForeground) self.scrollContentView.addSubview(sliderView) } transition.setFrame(view: sliderView, frame: sliderFrame) self.sliderBackground.backgroundColor = UIColor(rgb: 0xEEEEEF) self.sliderForeground.backgroundColor = UIColor(rgb: 0xFFB10D) let sliderBackgroundFrame = CGRect(origin: CGPoint(x: sliderFrame.minX - 8.0, y: sliderFrame.minY + 7.0), size: CGSize(width: sliderFrame.width + 16.0, height: sliderFrame.height - 14.0)) transition.setFrame(view: self.sliderBackground, frame: sliderBackgroundFrame) let progressFraction: CGFloat = CGFloat(self.amount) / CGFloat(1000 - 1) let sliderMinWidth = sliderBackgroundFrame.height let sliderAreaWidth: CGFloat = sliderBackgroundFrame.width - sliderMinWidth let sliderForegroundFrame = CGRect(origin: CGPoint(x: sliderBackgroundFrame.minX, y: sliderBackgroundFrame.minY), size: CGSize(width: sliderMinWidth + floorToScreenPixels(sliderAreaWidth * progressFraction), height: sliderBackgroundFrame.height)) transition.setFrame(view: self.sliderForeground, frame: sliderForegroundFrame) self.sliderBackground.layer.cornerRadius = sliderBackgroundFrame.height * 0.5 self.sliderForeground.layer.cornerRadius = sliderBackgroundFrame.height * 0.5 self.sliderStars.frame = CGRect(origin: .zero, size: sliderBackgroundFrame.size) self.sliderStars.update(size: sliderBackgroundFrame.size, value: progressFraction) self.sliderForeground.isHidden = sliderForegroundFrame.width <= sliderMinWidth var effectiveInertiaDirection = self.inertiaDirection if progressFraction <= 0.03 || progressFraction >= 0.97 { effectiveInertiaDirection = nil } let badgeSize = self.badge.update( transition: transition, component: AnyComponent(BadgeComponent( theme: environment.theme, title: "\(self.amount)", inertiaDirection: effectiveInertiaDirection )), environment: {}, containerSize: CGSize(width: 200.0, height: 200.0) ) var badgeFrame = CGRect(origin: CGPoint(x: sliderForegroundFrame.minX + sliderForegroundFrame.width - floorToScreenPixels(sliderMinWidth * 0.5), y: sliderForegroundFrame.minY - 8.0), size: badgeSize) if let badgeView = self.badge.view as? BadgeComponent.View { if badgeView.superview == nil { self.scrollContentView.insertSubview(badgeView, belowSubview: self.badgeStars) } let badgeSideInset = sideInset + 15.0 let badgeOverflowWidth: CGFloat if badgeFrame.minX - badgeSize.width * 0.5 < badgeSideInset { badgeOverflowWidth = badgeSideInset - (badgeFrame.minX - badgeSize.width * 0.5) } else if badgeFrame.minX + badgeSize.width * 0.5 > availableSize.width - badgeSideInset { badgeOverflowWidth = availableSize.width - badgeSideInset - (badgeFrame.minX + badgeSize.width * 0.5) } else { badgeOverflowWidth = 0.0 } badgeFrame.origin.x += badgeOverflowWidth badgeView.frame = badgeFrame badgeView.adjustTail(size: badgeSize, overflowWidth: -badgeOverflowWidth) } let starsRect = CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: sliderForegroundFrame.midY)) self.badgeStars.frame = starsRect self.badgeStars.update(size: starsRect.size, emitterPosition: CGPoint(x: badgeFrame.minX, y: badgeFrame.midY - 64.0)) } contentHeight += 123.0 let leftButtonSize = self.leftButton.update( transition: transition, component: AnyComponent(BalanceComponent( context: component.context, theme: environment.theme, strings: environment.strings, balance: component.balance )), environment: {}, containerSize: CGSize(width: 120.0, height: 100.0) ) let leftButtonFrame = CGRect(origin: CGPoint(x: 16.0, y: floor((56.0 - leftButtonSize.height) * 0.5)), size: leftButtonSize) if let leftButtonView = self.leftButton.view { if leftButtonView.superview == nil { self.navigationBarContainer.addSubview(leftButtonView) } transition.setFrame(view: leftButtonView, frame: leftButtonFrame) } if themeUpdated { self.cachedCloseImage = nil } let closeImage: UIImage if let current = self.cachedCloseImage { closeImage = current } else { closeImage = generateCloseButtonImage(backgroundColor: UIColor(rgb: 0x808084, alpha: 0.1), foregroundColor: environment.theme.actionSheet.inputClearButtonColor)! self.cachedCloseImage = closeImage } let closeButtonSize = self.closeButton.update( transition: .immediate, component: AnyComponent(PlainButtonComponent( content: AnyComponent(Image(image: closeImage)), effectAlignment: .center, action: { [weak self] in guard let self else { return } self.environment?.controller()?.dismiss() } )), environment: {}, containerSize: CGSize(width: 30.0, height: 30.0) ) let closeButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - sideInset - closeButtonSize.width, y: floor((56.0 - leftButtonSize.height) * 0.5)), size: closeButtonSize) if let closeButtonView = self.closeButton.view { if closeButtonView.superview == nil { self.navigationBarContainer.addSubview(closeButtonView) } transition.setFrame(view: closeButtonView, frame: closeButtonFrame) } let containerInset: CGFloat = environment.statusBarHeight + 10.0 var initialContentHeight = contentHeight let clippingY: CGFloat let title = self.title let descriptionText = self.descriptionText let actionButton = self.actionButton let titleSize = title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString(string: "React with Stars", font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor)) )), environment: {}, containerSize: CGSize(width: availableSize.width - leftButtonFrame.maxX * 2.0, height: 100.0) ) let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: floor((56.0 - titleSize.height) * 0.5)), size: titleSize) if let titleView = title.view { if titleView.superview == nil { self.navigationBarContainer.addSubview(titleView) } transition.setFrame(view: titleView, frame: titleFrame) } contentHeight += 56.0 contentHeight += 8.0 let text = "Choose how many stars you want to send to **\(component.peer.debugDisplayTitle)** to support this post." let body = MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor) let bold = MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.itemPrimaryTextColor) let descriptionTextSize = descriptionText.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( text: .markdown(text: text, attributes: MarkdownAttributes( body: body, bold: bold, link: body, linkAttribute: { _ in nil } )), horizontalAlignment: .center, maximumNumberOfLines: 0 )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 16.0 * 2.0, height: 1000.0) ) let descriptionTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - descriptionTextSize.width) * 0.5), y: contentHeight), size: descriptionTextSize) if let descriptionTextView = descriptionText.view { if descriptionTextView.superview == nil { self.scrollContentView.addSubview(descriptionTextView) } transition.setFrame(view: descriptionTextView, frame: descriptionTextFrame) } contentHeight += descriptionTextFrame.height contentHeight += 22.0 contentHeight += 2.0 if !component.topPeers.isEmpty { contentHeight += 3.0 let topPeersLeftSeparator: SimpleLayer if let current = self.topPeersLeftSeparator { topPeersLeftSeparator = current } else { topPeersLeftSeparator = SimpleLayer() self.topPeersLeftSeparator = topPeersLeftSeparator self.scrollContentView.layer.addSublayer(topPeersLeftSeparator) } let topPeersRightSeparator: SimpleLayer if let current = self.topPeersRightSeparator { topPeersRightSeparator = current } else { topPeersRightSeparator = SimpleLayer() self.topPeersRightSeparator = topPeersRightSeparator self.scrollContentView.layer.addSublayer(topPeersRightSeparator) } let topPeersTitleBackground: SimpleLayer if let current = self.topPeersTitleBackground { topPeersTitleBackground = current } else { topPeersTitleBackground = SimpleLayer() self.topPeersTitleBackground = topPeersTitleBackground self.scrollContentView.layer.addSublayer(topPeersTitleBackground) } let topPeersTitle: ComponentView if let current = self.topPeersTitle { topPeersTitle = current } else { topPeersTitle = ComponentView() self.topPeersTitle = topPeersTitle } topPeersLeftSeparator.backgroundColor = environment.theme.list.itemPlainSeparatorColor.cgColor topPeersRightSeparator.backgroundColor = environment.theme.list.itemPlainSeparatorColor.cgColor //TODO:localize let topPeersTitleSize = topPeersTitle.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString(string: "Top Senders", font: Font.semibold(15.0), textColor: .white)) )), environment: {}, containerSize: CGSize(width: 300.0, height: 100.0) ) let topPeersBackgroundSize = CGSize(width: topPeersTitleSize.width + 16.0 * 2.0, height: topPeersTitleSize.height + 9.0 * 2.0) let topPeersBackgroundFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - topPeersBackgroundSize.width) * 0.5), y: contentHeight), size: topPeersBackgroundSize) topPeersTitleBackground.backgroundColor = UIColor(rgb: 0xFFB10D).cgColor topPeersTitleBackground.cornerRadius = topPeersBackgroundFrame.height * 0.5 transition.setFrame(layer: topPeersTitleBackground, frame: topPeersBackgroundFrame) let topPeersTitleFrame = CGRect(origin: CGPoint(x: topPeersBackgroundFrame.minX + floor((topPeersBackgroundFrame.width - topPeersTitleSize.width) * 0.5), y: topPeersBackgroundFrame.minY + floor((topPeersBackgroundFrame.height - topPeersTitleSize.height) * 0.5)), size: topPeersTitleSize) if let topPeersTitleView = topPeersTitle.view { if topPeersTitleView.superview == nil { self.scrollContentView.addSubview(topPeersTitleView) } transition.setFrame(view: topPeersTitleView, frame: topPeersTitleFrame) } let separatorY = topPeersBackgroundFrame.midY let separatorSpacing: CGFloat = 10.0 transition.setFrame(layer: topPeersLeftSeparator, frame: CGRect(origin: CGPoint(x: sideInset, y: separatorY), size: CGSize(width: max(0.0, topPeersBackgroundFrame.minX - separatorSpacing - sideInset), height: UIScreenPixel))) transition.setFrame(layer: topPeersRightSeparator, frame: CGRect(origin: CGPoint(x: topPeersBackgroundFrame.maxX + separatorSpacing, y: separatorY), size: CGSize(width: max(0.0, availableSize.width - sideInset - (topPeersBackgroundFrame.maxX + separatorSpacing)), height: UIScreenPixel))) var validIds: [EnginePeer.Id] = [] var items: [(itemView: ComponentView, size: CGSize)] = [] for topPeer in component.topPeers { validIds.append(topPeer.id) let itemView: ComponentView if let current = self.topPeerItems[topPeer.id] { itemView = current } else { itemView = ComponentView() self.topPeerItems[topPeer.id] = itemView } let itemSize = itemView.update( transition: .immediate, component: AnyComponent(PeerComponent( context: component.context, theme: environment.theme, strings: environment.strings, peer: topPeer )), environment: {}, containerSize: CGSize(width: 200.0, height: 200.0) ) items.append((itemView, itemSize)) } var removedIds: [EnginePeer.Id] = [] for (id, itemView) in self.topPeerItems { if !validIds.contains(id) { removedIds.append(id) itemView.view?.removeFromSuperview() } } for id in removedIds { self.topPeerItems.removeValue(forKey: id) } var itemsWidth: CGFloat = 0.0 for (_, itemSize) in items { itemsWidth += itemSize.width } let maxItemSpacing = 48.0 var itemSpacing = floor((availableSize.width - itemsWidth) / CGFloat(items.count + 1)) itemSpacing = min(itemSpacing, maxItemSpacing) let totalWidth = itemsWidth + itemSpacing * CGFloat(items.count + 1) var itemX: CGFloat = floor((availableSize.width - totalWidth) * 0.5) + itemSpacing for (itemView, itemSize) in items { if let itemComponentView = itemView.view { if itemComponentView.superview == nil { self.scrollContentView.addSubview(itemComponentView) } itemComponentView.frame = CGRect(origin: CGPoint(x: itemX, y: contentHeight + 56.0), size: itemSize) } itemX += itemSize.width + itemSpacing } contentHeight += 161.0 } initialContentHeight = contentHeight if self.cachedStarImage == nil || self.cachedStarImage?.1 !== environment.theme { self.cachedStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: .white)!, environment.theme) } let buttonString = "Send # \(self.amount)" let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: .white, paragraphAlignment: .center) if let range = buttonAttributedString.string.range(of: "#"), let starImage = self.cachedStarImage?.0 { buttonAttributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: buttonAttributedString.string)) buttonAttributedString.addAttribute(.foregroundColor, value: UIColor(rgb: 0xffffff), range: NSRange(range, in: buttonAttributedString.string)) buttonAttributedString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: buttonAttributedString.string)) } let actionButtonSize = actionButton.update( transition: transition, component: AnyComponent(ButtonComponent( background: ButtonComponent.Background( color: environment.theme.list.itemCheckColors.fillColor, foreground: environment.theme.list.itemCheckColors.foregroundColor, pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9), cornerRadius: 10.0 ), content: AnyComponentWithIdentity( id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString))) ), isEnabled: true, displaysProgress: false, action: { [weak self] in guard let self, let component = self.component else { return } component.completion(self.amount) self.environment?.controller()?.dismiss() } )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) ) let buttonDescriptionTextSize = self.buttonDescriptionText.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( text: .markdown(text: "By sending Stars you agree to the [Terms of Service]()", attributes: MarkdownAttributes( body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemSecondaryTextColor), bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: environment.theme.list.itemSecondaryTextColor), link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemAccentColor), linkAttribute: { url in return ("URL", url) } )), horizontalAlignment: .center, maximumNumberOfLines: 0 )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset, height: 1000.0) ) let buttonDescriptionSpacing: CGFloat = 14.0 let bottomPanelHeight = 13.0 + environment.safeInsets.bottom + actionButtonSize.height + buttonDescriptionSpacing + buttonDescriptionTextSize.height let actionButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - bottomPanelHeight), size: actionButtonSize) if let actionButtonView = actionButton.view { if actionButtonView.superview == nil { self.addSubview(actionButtonView) } transition.setFrame(view: actionButtonView, frame: actionButtonFrame) } let buttonDescriptionTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - buttonDescriptionTextSize.width) * 0.5), y: actionButtonFrame.maxY + buttonDescriptionSpacing), size: buttonDescriptionTextSize) if let buttonDescriptionTextView = buttonDescriptionText.view { if buttonDescriptionTextView.superview == nil { self.addSubview(buttonDescriptionTextView) } transition.setFrame(view: buttonDescriptionTextView, frame: buttonDescriptionTextFrame) } contentHeight += bottomPanelHeight initialContentHeight += bottomPanelHeight clippingY = actionButtonFrame.minY - 24.0 let topInset: CGFloat = max(0.0, availableSize.height - containerInset - initialContentHeight) let scrollContentHeight = max(topInset + contentHeight + containerInset, availableSize.height - containerInset) self.scrollContentClippingView.layer.cornerRadius = 10.0 self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, bottomInset: environment.safeInsets.bottom, topInset: topInset) transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + containerInset), size: CGSize(width: availableSize.width, height: contentHeight))) transition.setPosition(layer: self.backgroundLayer, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)) transition.setBounds(layer: self.backgroundLayer, bounds: CGRect(origin: CGPoint(), size: availableSize)) let scrollClippingFrame = CGRect(origin: CGPoint(x: 0.0, y: containerInset), size: CGSize(width: availableSize.width, height: clippingY - containerInset)) transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center) transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size)) self.ignoreScrolling = true transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height))) let contentSize = CGSize(width: availableSize.width, height: scrollContentHeight) if contentSize != self.scrollView.contentSize { self.scrollView.contentSize = contentSize } if resetScrolling { self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: availableSize) } self.ignoreScrolling = false self.updateScrolling(transition: transition) return availableSize } } func makeView() -> View { return View(frame: CGRect()) } func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } public class ChatSendStarsScreen: ViewControllerComponentContainer { public final class InitialData { let peer: EnginePeer let balance: Int64? let topPeers: [EnginePeer] fileprivate init( peer: EnginePeer, balance: Int64?, topPeers: [EnginePeer] ) { self.peer = peer self.balance = balance self.topPeers = topPeers } } private let context: AccountContext private var isDismissed: Bool = false private var presenceDisposable: Disposable? public init(context: AccountContext, initialData: InitialData, completion: @escaping (Int64) -> Void) { self.context = context super.init(context: context, component: ChatSendStarsScreenComponent( context: context, peer: initialData.peer, balance: initialData.balance, topPeers: initialData.topPeers, completion: completion ), navigationBarAppearance: .none) self.statusBar.statusBarStyle = .Ignore self.navigationPresentation = .flatModal self.blocksBackgroundWhenInOverlay = true } required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { self.presenceDisposable?.dispose() } override public func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) self.view.disablesInteractiveModalDismiss = true if let componentView = self.node.hostView.componentView as? ChatSendStarsScreenComponent.View { componentView.animateIn() } } public static func initialData(context: AccountContext, peerId: EnginePeer.Id) -> Signal { let balance: Signal if let starsContext = context.starsContext { balance = starsContext.state |> map { state in return state?.balance } |> take(1) } else { balance = .single(nil) } return combineLatest( context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)), context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)), balance ) |> map { peer, accountPeer, balance -> InitialData? in guard let peer, let accountPeer else { return nil } return InitialData( peer: peer, balance: balance, topPeers: [accountPeer, peer] ) } } override public func dismiss(completion: (() -> Void)? = nil) { if !self.isDismissed { self.isDismissed = true if let componentView = self.node.hostView.componentView as? ChatSendStarsScreenComponent.View { componentView.animateOut(completion: { [weak self] in completion?() self?.dismiss(animated: false) }) } else { self.dismiss(animated: false) } } } } private func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? { return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(backgroundColor.cgColor) context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) context.setLineWidth(2.0) context.setLineCap(.round) context.setStrokeColor(foregroundColor.cgColor) context.move(to: CGPoint(x: 10.0, y: 10.0)) context.addLine(to: CGPoint(x: 20.0, y: 20.0)) context.strokePath() context.move(to: CGPoint(x: 20.0, y: 10.0)) context.addLine(to: CGPoint(x: 10.0, y: 20.0)) context.strokePath() }) } private final class BadgeStarsView: UIView { private let staticEmitterLayer = CAEmitterLayer() private let dynamicEmitterLayer = CAEmitterLayer() override init(frame: CGRect) { super.init(frame: frame) self.layer.addSublayer(self.staticEmitterLayer) self.layer.addSublayer(self.dynamicEmitterLayer) } required init(coder: NSCoder) { preconditionFailure() } private func setupEmitter() { let color = UIColor(rgb: 0xffbe27) self.staticEmitterLayer.emitterShape = .circle self.staticEmitterLayer.emitterSize = CGSize(width: 10.0, height: 5.0) self.staticEmitterLayer.emitterMode = .outline self.layer.addSublayer(self.staticEmitterLayer) self.dynamicEmitterLayer.birthRate = 0.0 self.dynamicEmitterLayer.emitterShape = .circle self.dynamicEmitterLayer.emitterSize = CGSize(width: 10.0, height: 55.0) self.dynamicEmitterLayer.emitterMode = .surface self.layer.addSublayer(self.dynamicEmitterLayer) let staticEmitter = CAEmitterCell() staticEmitter.name = "emitter" staticEmitter.contents = UIImage(bundleImageName: "Premium/Stars/Particle")?.cgImage staticEmitter.birthRate = 20.0 staticEmitter.lifetime = 2.7 staticEmitter.velocity = 30.0 staticEmitter.velocityRange = 3 staticEmitter.scale = 0.15 staticEmitter.scaleRange = 0.08 staticEmitter.emissionRange = .pi * 2.0 staticEmitter.setValue(3.0, forKey: "mass") staticEmitter.setValue(2.0, forKey: "massRange") let dynamicEmitter = CAEmitterCell() dynamicEmitter.name = "emitter" dynamicEmitter.contents = UIImage(bundleImageName: "Premium/Stars/Particle")?.cgImage dynamicEmitter.birthRate = 0.0 dynamicEmitter.lifetime = 2.7 dynamicEmitter.velocity = 30.0 dynamicEmitter.velocityRange = 3 dynamicEmitter.scale = 0.15 dynamicEmitter.scaleRange = 0.08 dynamicEmitter.emissionRange = .pi / 3.0 dynamicEmitter.setValue(3.0, forKey: "mass") dynamicEmitter.setValue(2.0, forKey: "massRange") let staticColors: [Any] = [ UIColor.white.withAlphaComponent(0.0).cgColor, UIColor.white.withAlphaComponent(0.35).cgColor, color.cgColor, color.cgColor, color.withAlphaComponent(0.0).cgColor ] let staticColorBehavior = CAEmitterCell.createEmitterBehavior(type: "colorOverLife") staticColorBehavior.setValue(staticColors, forKey: "colors") staticEmitter.setValue([staticColorBehavior], forKey: "emitterBehaviors") let dynamicColors: [Any] = [ UIColor.white.withAlphaComponent(0.35).cgColor, color.withAlphaComponent(0.85).cgColor, color.cgColor, color.cgColor, color.withAlphaComponent(0.0).cgColor ] let dynamicColorBehavior = CAEmitterCell.createEmitterBehavior(type: "colorOverLife") dynamicColorBehavior.setValue(dynamicColors, forKey: "colors") dynamicEmitter.setValue([dynamicColorBehavior], forKey: "emitterBehaviors") let attractor = CAEmitterCell.createEmitterBehavior(type: "simpleAttractor") attractor.setValue("attractor", forKey: "name") attractor.setValue(20, forKey: "falloff") attractor.setValue(35, forKey: "radius") self.staticEmitterLayer.setValue([attractor], forKey: "emitterBehaviors") self.staticEmitterLayer.setValue(4.0, forKeyPath: "emitterBehaviors.attractor.stiffness") self.staticEmitterLayer.setValue(false, forKeyPath: "emitterBehaviors.attractor.enabled") self.staticEmitterLayer.emitterCells = [staticEmitter] self.dynamicEmitterLayer.emitterCells = [dynamicEmitter] } func update(speed: Float, delta: Float? = nil) { if speed > 0.0 { if self.dynamicEmitterLayer.birthRate.isZero { self.dynamicEmitterLayer.beginTime = CACurrentMediaTime() } self.dynamicEmitterLayer.setValue(Float(20.0 + speed * 1.4), forKeyPath: "emitterCells.emitter.birthRate") self.dynamicEmitterLayer.setValue(2.7 - min(1.1, 1.5 * speed / 120.0), forKeyPath: "emitterCells.emitter.lifetime") self.dynamicEmitterLayer.setValue(30.0 + CGFloat(speed / 80.0), forKeyPath: "emitterCells.emitter.velocity") if let delta, speed > 15.0 { self.dynamicEmitterLayer.setValue(delta > 0 ? .pi : 0, forKeyPath: "emitterCells.emitter.emissionLongitude") self.dynamicEmitterLayer.setValue(.pi / 2.0, forKeyPath: "emitterCells.emitter.emissionRange") } else { self.dynamicEmitterLayer.setValue(0.0, forKeyPath: "emitterCells.emitter.emissionLongitude") self.dynamicEmitterLayer.setValue(.pi * 2.0, forKeyPath: "emitterCells.emitter.emissionRange") } self.staticEmitterLayer.setValue(true, forKeyPath: "emitterBehaviors.attractor.enabled") self.dynamicEmitterLayer.birthRate = 1.0 self.staticEmitterLayer.birthRate = 0.0 } else { self.dynamicEmitterLayer.birthRate = 0.0 if let staticEmitter = self.staticEmitterLayer.emitterCells?.first { staticEmitter.beginTime = CACurrentMediaTime() } self.staticEmitterLayer.birthRate = 1.0 self.staticEmitterLayer.setValue(false, forKeyPath: "emitterBehaviors.attractor.enabled") } } func update(size: CGSize, emitterPosition: CGPoint) { if self.staticEmitterLayer.emitterCells == nil { self.setupEmitter() } self.staticEmitterLayer.frame = CGRect(origin: .zero, size: size) self.staticEmitterLayer.emitterPosition = emitterPosition self.dynamicEmitterLayer.frame = CGRect(origin: .zero, size: size) self.dynamicEmitterLayer.emitterPosition = emitterPosition self.staticEmitterLayer.setValue(emitterPosition, forKeyPath: "emitterBehaviors.attractor.position") } } private final class SliderStarsView: UIView { private let emitterLayer = CAEmitterLayer() override init(frame: CGRect) { super.init(frame: frame) self.layer.addSublayer(self.emitterLayer) } required init(coder: NSCoder) { preconditionFailure() } private func setupEmitter() { self.emitterLayer.emitterShape = .rectangle self.emitterLayer.emitterMode = .surface self.layer.addSublayer(self.emitterLayer) let emitter = CAEmitterCell() emitter.name = "emitter" emitter.contents = UIImage(bundleImageName: "Premium/Stars/Particle")?.cgImage emitter.birthRate = 20.0 emitter.lifetime = 2.0 emitter.velocity = 15.0 emitter.velocityRange = 10 emitter.scale = 0.15 emitter.scaleRange = 0.08 emitter.emissionRange = .pi / 4.0 emitter.setValue(3.0, forKey: "mass") emitter.setValue(2.0, forKey: "massRange") self.emitterLayer.emitterCells = [emitter] let colors: [Any] = [ UIColor.white.withAlphaComponent(0.0).cgColor, UIColor.white.withAlphaComponent(0.38).cgColor, UIColor.white.withAlphaComponent(0.38).cgColor, UIColor.white.withAlphaComponent(0.0).cgColor, UIColor.white.withAlphaComponent(0.38).cgColor, UIColor.white.withAlphaComponent(0.38).cgColor, UIColor.white.withAlphaComponent(0.0).cgColor ] let colorBehavior = CAEmitterCell.createEmitterBehavior(type: "colorOverLife") colorBehavior.setValue(colors, forKey: "colors") emitter.setValue([colorBehavior], forKey: "emitterBehaviors") } func update(size: CGSize, value: CGFloat) { if self.emitterLayer.emitterCells == nil { self.setupEmitter() } self.emitterLayer.setValue(20.0 + Float(value * 40.0), forKeyPath: "emitterCells.emitter.birthRate") self.emitterLayer.setValue(15.0 + value * 75.0, forKeyPath: "emitterCells.emitter.velocity") self.emitterLayer.frame = CGRect(origin: .zero, size: size) self.emitterLayer.emitterPosition = CGPoint(x: size.width / 2.0, y: size.height / 2.0) self.emitterLayer.emitterSize = size } }