Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
@@ -0,0 +1,179 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import ComponentFlow
import TelegramCore
import AccountContext
import TelegramPresentationData
import AccountContext
import AppBundle
final class AppIconsDemoComponent: Component {
public typealias EnvironmentType = DemoPageEnvironment
let context: AccountContext
let appIcons: [PresentationAppIcon]
public init(
context: AccountContext,
appIcons: [PresentationAppIcon]
) {
self.context = context
self.appIcons = appIcons
}
public static func ==(lhs: AppIconsDemoComponent, rhs: AppIconsDemoComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.appIcons != rhs.appIcons {
return false
}
return true
}
public final class View: UIView {
private var component: AppIconsDemoComponent?
private var containerView: UIView
private var imageViews: [UIImageView] = []
private var isVisible = false
public override init(frame: CGRect) {
self.containerView = UIView()
self.containerView.clipsToBounds = true
super.init(frame: frame)
self.addSubview(self.containerView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func update(component: AppIconsDemoComponent, availableSize: CGSize, environment: Environment<DemoPageEnvironment>, transition: ComponentTransition) -> CGSize {
let isDisplaying = environment[DemoPageEnvironment.self].isDisplaying
self.component = component
self.containerView.frame = CGRect(origin: CGPoint(x: -availableSize.width / 2.0, y: 0.0), size: CGSize(width: availableSize.width * 2.0, height: availableSize.height))
if self.imageViews.isEmpty {
for icon in component.appIcons {
let image: UIImage?
switch icon.imageName {
case "Premium":
image = UIImage(bundleImageName: "Premium/Icons/Premium")
case "PremiumBlack":
image = UIImage(bundleImageName: "Premium/Icons/Black")
case "PremiumTurbo":
image = UIImage(bundleImageName: "Premium/Icons/Turbo")
default:
image = nil
}
if let image = image {
let imageView = UIImageView(frame: CGRect(origin: .zero, size: CGSize(width: 90.0, height: 90.0)))
imageView.clipsToBounds = true
imageView.layer.cornerRadius = 24.0
if #available(iOS 13.0, *) {
imageView.layer.cornerCurve = .continuous
}
imageView.image = image
self.containerView.addSubview(imageView)
self.imageViews.append(imageView)
}
}
}
var i = 0
for view in self.imageViews {
let position: CGPoint
switch i {
case 0:
position = CGPoint(x: availableSize.width * 0.5, y: availableSize.height * 0.333)
case 1:
position = CGPoint(x: availableSize.width * 0.333, y: availableSize.height * 0.667)
case 2:
position = CGPoint(x: availableSize.width * 0.667, y: availableSize.height * 0.667)
default:
position = CGPoint(x: availableSize.width * 0.5, y: availableSize.height * 0.5)
}
if !self.animating {
view.center = position.offsetBy(dx: availableSize.width / 2.0, dy: 0.0)
}
i += 1
}
var mappedPosition = environment[DemoPageEnvironment.self].position
mappedPosition *= abs(mappedPosition)
if let _ = transition.userData(DemoAnimateInTransition.self), abs(mappedPosition) < .ulpOfOne {
Queue.mainQueue().after(0.1) {
self.animateIn(availableSize: availableSize)
}
}
if isDisplaying && !self.isVisible {
self.animateIn(availableSize: availableSize)
}
self.isVisible = isDisplaying
return availableSize
}
private var animating = false
func animateIn(availableSize: CGSize) {
self.animating = true
var i = 0
for view in self.imageViews {
let from: CGPoint
let delay: Double
switch i {
case 0:
from = CGPoint(x: -availableSize.width * 0.333, y: -availableSize.height * 0.8)
delay = 0.1
case 1:
from = CGPoint(x: -availableSize.width * 0.55, y: availableSize.height * 0.75)
delay = 0.15
case 2:
from = CGPoint(x: availableSize.width * 0.9, y: availableSize.height * 0.75)
delay = 0.0
default:
from = CGPoint(x: availableSize.width * 0.5, y: availableSize.height * 0.5)
delay = 0.0
}
let initialPosition = view.layer.position
view.layer.position = initialPosition.offsetBy(dx: from.x, dy: from.y)
Queue.mainQueue().after(delay) {
view.layer.position = initialPosition
view.layer.animateScale(from: 3.0, to: 1.0, duration: 0.5, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring)
view.layer.animatePosition(from: from, to: CGPoint(), duration: 0.5, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
if i == 2 {
self.animating = false
}
}
i += 1
}
}
}
public func makeView() -> View {
return View()
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<DemoPageEnvironment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition)
}
}
@@ -0,0 +1,62 @@
import Foundation
import UIKit
import SceneKit
import Display
import AppBundle
import PremiumStarComponent
private let sceneVersion: Int = 1
final class BadgeBusinessView: UIView, PhoneDemoDecorationView {
private let sceneView: SCNView
private var leftParticles: SCNNode?
private var rightParticles: SCNNode?
override init(frame: CGRect) {
self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size))
self.sceneView.backgroundColor = .clear
if let scene = loadCompressedScene(name: "business", version: sceneVersion) {
self.sceneView.scene = scene
}
self.sceneView.isUserInteractionEnabled = false
self.sceneView.preferredFramesPerSecond = 60
super.init(frame: frame)
self.alpha = 0.0
self.addSubview(self.sceneView)
self.leftParticles = self.sceneView.scene?.rootNode.childNode(withName: "leftParticles", recursively: false)
self.rightParticles = self.sceneView.scene?.rootNode.childNode(withName: "rightParticles", recursively: false)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setVisible(_ visible: Bool) {
if visible, let leftParticles = self.leftParticles, let rightParticles = self.rightParticles, leftParticles.parent == nil {
self.sceneView.scene?.rootNode.addChildNode(leftParticles)
self.sceneView.scene?.rootNode.addChildNode(rightParticles)
}
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear)
transition.updateAlpha(layer: self.layer, alpha: visible ? 0.5 : 0.0, completion: { [weak self] finished in
if let strongSelf = self, finished && !visible && strongSelf.leftParticles?.parent != nil {
strongSelf.leftParticles?.removeFromParentNode()
strongSelf.rightParticles?.removeFromParentNode()
}
})
}
func resetAnimation() {
}
override func layoutSubviews() {
super.layoutSubviews()
self.sceneView.frame = CGRect(origin: .zero, size: frame.size)
}
}
@@ -0,0 +1,187 @@
import Foundation
import UIKit
import Display
import ComponentFlow
private let labelWidth: CGFloat = 16.0
private let labelHeight: CGFloat = 36.0
private let labelSize = CGSize(width: labelWidth, height: labelHeight)
private let font = Font.with(size: 24.0, design: .round, weight: .semibold, traits: [])
private let suffixFont = Font.with(size: 22.0, design: .regular, weight: .regular, traits: [])
final class BadgeLabelView: UIView {
private class StackView: UIView {
var labels: [UILabel] = []
var currentValue: Int32 = 0
var color: UIColor = .white {
didSet {
for view in self.labels {
view.textColor = self.color
}
}
}
init() {
super.init(frame: CGRect(origin: .zero, size: labelSize))
var height: CGFloat = -labelHeight
for i in -1 ..< 10 {
let label = UILabel()
if i == -1 {
label.text = "9"
} else {
label.text = "\(i)"
}
label.textColor = self.color
label.font = font
label.textAlignment = .center
label.frame = CGRect(x: 0, y: height, width: labelWidth, height: labelHeight)
self.addSubview(label)
self.labels.append(label)
height += labelHeight
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(value: Int32, isFirst: Bool, isLast: Bool, transition: ComponentTransition) {
let previousValue = self.currentValue
self.currentValue = value
self.labels[1].alpha = isFirst && !isLast ? 0.0 : 1.0
if previousValue == 9 && value < 9 {
self.bounds = CGRect(
origin: CGPoint(
x: 0.0,
y: -1.0 * labelSize.height
),
size: labelSize
)
}
let bounds = CGRect(
origin: CGPoint(
x: 0.0,
y: CGFloat(value) * labelSize.height
),
size: labelSize
)
transition.setBounds(view: self, bounds: bounds)
}
}
private var itemViews: [Int: StackView] = [:]
private var staticLabel = ImmediateTextNode()
private var params: (value: String, suffix: String?)?
init() {
super.init(frame: .zero)
self.clipsToBounds = true
self.isUserInteractionEnabled = false
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var color: UIColor = .white {
didSet {
if let params {
self.staticLabel.attributedText = BadgeLabelView.makeText(value: params.value, suffix: params.suffix, color: self.color)
let _ = self.staticLabel.updateLayout(CGSize(width: 1000.0, height: 1000.0))
}
for (_, view) in self.itemViews {
view.color = self.color
}
}
}
static func makeText(value: String, suffix: String?, color: UIColor) -> NSAttributedString {
let string = NSMutableAttributedString()
string.append(NSAttributedString(string: value, font: font, textColor: color))
if let suffix {
string.append(NSAttributedString(string: suffix, font: suffixFont, textColor: color.withMultipliedAlpha(0.6)))
}
return string
}
static func calculateSize(value: String, suffix: String?) -> CGSize {
let textView = ImmediateTextView()
textView.attributedText = BadgeLabelView.makeText(value: value, suffix: suffix, color: .black)
return textView.updateLayout(CGSize(width: 1000.0, height: 1000.0))
}
func update(value: String, suffix: String?, transition: ComponentTransition) -> CGSize {
self.params = (value, suffix)
if value.contains(" ") || value.contains(".") || value.contains(where: { !$0.isNumber }) || suffix != nil {
for (_, view) in self.itemViews {
view.isHidden = true
}
if self.staticLabel.view.superview == nil {
self.addSubview(self.staticLabel.view)
}
self.staticLabel.attributedText = BadgeLabelView.makeText(value: value, suffix: suffix, color: self.color)
let size = self.staticLabel.updateLayout(CGSize(width: 1000.0, height: 1000.0))
self.staticLabel.frame = CGRect(origin: CGPoint(x: 0.0, y: 3.0), size: CGSize(width: size.width, height: labelHeight))
return size
}
let string = value
let stringArray = Array(string.map { String($0) }.reversed())
let totalWidth = CGFloat(stringArray.count) * labelWidth
var validIds: [Int] = []
for i in 0 ..< stringArray.count {
validIds.append(i)
let itemView: StackView
var itemTransition = transition
if let current = self.itemViews[i] {
itemView = current
} else {
itemTransition = transition.withAnimation(.none)
itemView = StackView()
itemView.color = self.color
self.itemViews[i] = itemView
self.addSubview(itemView)
}
let digit = Int32(stringArray[i]) ?? 0
itemView.update(value: digit, isFirst: i == stringArray.count - 1, isLast: i == 0, transition: transition)
itemTransition.setFrame(
view: itemView,
frame: CGRect(x: totalWidth - labelWidth * CGFloat(i + 1), y: 0.0, width: labelWidth, height: labelHeight)
)
}
var removeIds: [Int] = []
for (id, itemView) in self.itemViews {
if !validIds.contains(id) {
removeIds.append(id)
transition.setAlpha(view: itemView, alpha: 0.0, completion: { _ in
itemView.removeFromSuperview()
})
}
}
for id in removeIds {
self.itemViews.removeValue(forKey: id)
}
return CGSize(width: totalWidth, height: labelHeight)
}
}
@@ -0,0 +1,168 @@
import Foundation
import UIKit
import SceneKit
import Display
import AppBundle
import PremiumStarComponent
final class BadgeStarsView: UIView, PhoneDemoDecorationView {
private let sceneView: SCNView
private var leftParticles: SCNNode?
private var rightParticles: SCNNode?
override init(frame: CGRect) {
self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size))
self.sceneView.backgroundColor = .clear
if let scene = loadCompressedScene(name: "badge", version: 1) {
self.sceneView.scene = scene
}
self.sceneView.isUserInteractionEnabled = false
self.sceneView.preferredFramesPerSecond = 60
super.init(frame: frame)
self.alpha = 0.0
self.addSubview(self.sceneView)
self.leftParticles = self.sceneView.scene?.rootNode.childNode(withName: "leftParticles", recursively: false)
self.rightParticles = self.sceneView.scene?.rootNode.childNode(withName: "rightParticles", recursively: false)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setVisible(_ visible: Bool) {
if visible, let leftParticles = self.leftParticles, let rightParticles = self.rightParticles, leftParticles.parent == nil {
self.sceneView.scene?.rootNode.addChildNode(leftParticles)
self.sceneView.scene?.rootNode.addChildNode(rightParticles)
}
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear)
transition.updateAlpha(layer: self.layer, alpha: visible ? 0.5 : 0.0, completion: { [weak self] finished in
if let strongSelf = self, finished && !visible && strongSelf.leftParticles?.parent != nil {
strongSelf.leftParticles?.removeFromParentNode()
strongSelf.rightParticles?.removeFromParentNode()
}
})
}
func resetAnimation() {
}
override func layoutSubviews() {
super.layoutSubviews()
self.sceneView.frame = CGRect(origin: .zero, size: frame.size)
}
}
final class EmojiStarsView: UIView, PhoneDemoDecorationView {
private let sceneView: SCNView
private var leftParticles: SCNNode?
private var rightParticles: SCNNode?
override init(frame: CGRect) {
self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size))
self.sceneView.backgroundColor = .clear
if let scene = loadCompressedScene(name: "emoji", version: 1) {
self.sceneView.scene = scene
}
self.sceneView.isUserInteractionEnabled = false
self.sceneView.preferredFramesPerSecond = 60
super.init(frame: frame)
self.alpha = 0.0
self.addSubview(self.sceneView)
self.leftParticles = self.sceneView.scene?.rootNode.childNode(withName: "leftParticles", recursively: false)
self.rightParticles = self.sceneView.scene?.rootNode.childNode(withName: "rightParticles", recursively: false)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setVisible(_ visible: Bool) {
if visible, let leftParticles = self.leftParticles, let rightParticles = self.rightParticles, leftParticles.parent == nil {
self.sceneView.scene?.rootNode.addChildNode(leftParticles)
self.sceneView.scene?.rootNode.addChildNode(rightParticles)
}
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear)
transition.updateAlpha(layer: self.layer, alpha: visible ? 0.5 : 0.0, completion: { [weak self] finished in
if let strongSelf = self, finished && !visible && strongSelf.leftParticles?.parent != nil {
strongSelf.leftParticles?.removeFromParentNode()
strongSelf.rightParticles?.removeFromParentNode()
}
})
}
func resetAnimation() {
}
override func layoutSubviews() {
super.layoutSubviews()
self.sceneView.frame = CGRect(origin: .zero, size: frame.size)
}
}
final class TagStarsView: UIView, PhoneDemoDecorationView {
private let sceneView: SCNView
private var leftParticles: SCNNode?
private var rightParticles: SCNNode?
override init(frame: CGRect) {
self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size))
self.sceneView.backgroundColor = .clear
if let scene = loadCompressedScene(name: "tag", version: 1) {
self.sceneView.scene = scene
}
self.sceneView.isUserInteractionEnabled = false
self.sceneView.preferredFramesPerSecond = 60
super.init(frame: frame)
self.alpha = 0.0
self.addSubview(self.sceneView)
self.leftParticles = self.sceneView.scene?.rootNode.childNode(withName: "leftParticles", recursively: false)
self.rightParticles = self.sceneView.scene?.rootNode.childNode(withName: "rightParticles", recursively: false)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setVisible(_ visible: Bool) {
if visible, let leftParticles = self.leftParticles, let rightParticles = self.rightParticles, leftParticles.parent == nil {
self.sceneView.scene?.rootNode.addChildNode(leftParticles)
self.sceneView.scene?.rootNode.addChildNode(rightParticles)
}
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear)
transition.updateAlpha(layer: self.layer, alpha: visible ? 0.5 : 0.0, completion: { [weak self] finished in
if let strongSelf = self, finished && !visible && strongSelf.leftParticles?.parent != nil {
strongSelf.leftParticles?.removeFromParentNode()
strongSelf.rightParticles?.removeFromParentNode()
}
})
}
func resetAnimation() {
}
override func layoutSubviews() {
super.layoutSubviews()
self.sceneView.frame = CGRect(origin: .zero, size: frame.size)
}
}
@@ -0,0 +1,102 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import SwiftSignalKit
import SceneKit
import GZip
import AppBundle
import LegacyComponents
import AvatarNode
import TelegramCore
import MultilineTextComponent
import TelegramPresentationData
import PremiumStarComponent
private let sceneVersion: Int = 1
public final class BoostHeaderBackgroundComponent: Component {
let isVisible: Bool
let hasIdleAnimations: Bool
public init(isVisible: Bool, hasIdleAnimations: Bool) {
self.isVisible = isVisible
self.hasIdleAnimations = hasIdleAnimations
}
public static func ==(lhs: BoostHeaderBackgroundComponent, rhs: BoostHeaderBackgroundComponent) -> Bool {
return lhs.isVisible == rhs.isVisible && lhs.hasIdleAnimations == rhs.hasIdleAnimations
}
public final class View: UIView, SCNSceneRendererDelegate {
private var _ready = Promise<Bool>()
var ready: Signal<Bool, NoError> {
return self._ready.get()
}
private let sceneView: SCNView
private var previousInteractionTimestamp: Double = 0.0
private var hasIdleAnimations = false
override init(frame: CGRect) {
self.sceneView = SCNView(frame: CGRect(origin: .zero, size: CGSize(width: 64.0, height: 64.0)))
self.sceneView.backgroundColor = .clear
self.sceneView.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
self.sceneView.isUserInteractionEnabled = false
self.sceneView.preferredFramesPerSecond = 60
super.init(frame: frame)
self.addSubview(self.sceneView)
self.setup()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setup() {
guard let scene = loadCompressedScene(name: "boost", version: sceneVersion) else {
return
}
self.sceneView.scene = scene
self.sceneView.delegate = self
let _ = self.sceneView.snapshot()
}
private var didSetReady = false
public func renderer(_ renderer: SCNSceneRenderer, didRenderScene scene: SCNScene, atTime time: TimeInterval) {
if !self.didSetReady {
self.didSetReady = true
Queue.mainQueue().justDispatch {
self._ready.set(.single(true))
}
}
}
func update(component: BoostHeaderBackgroundComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
self.sceneView.bounds = CGRect(origin: .zero, size: CGSize(width: availableSize.width * 2.0, height: availableSize.height))
if self.sceneView.superview == self {
self.sceneView.center = CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)
}
self.hasIdleAnimations = component.hasIdleAnimations
return availableSize
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
@@ -0,0 +1,682 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import SwiftSignalKit
import TelegramCore
import AccountContext
import MultilineTextComponent
import BlurredBackgroundComponent
import Markdown
import TelegramPresentationData
import BundleIconComponent
import ScrollComponent
import PremiumCoinComponent
private final class HeaderComponent: Component {
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings) {
self.context = context
self.theme = theme
self.strings = strings
}
static func ==(lhs: HeaderComponent, rhs: HeaderComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
return true
}
final class View: UIView {
private let coin = ComponentView<Empty>()
private let text = ComponentView<Empty>()
private var component: HeaderComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: HeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let containerSize = CGSize(width: min(414.0, availableSize.width), height: 220.0)
let coinSize = self.coin.update(
transition: .immediate,
component: AnyComponent(PremiumCoinComponent(mode: .business, isIntro: true, isVisible: true, hasIdleAnimations: true)),
environment: {},
containerSize: containerSize
)
if let view = self.coin.view {
if view.superview == nil {
self.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - coinSize.width) / 2.0), y: -84.0), size: coinSize)
}
let textSize = self.text.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: component.strings.Premium_Business_Description, font: Font.regular(15.0), textColor: component.theme.list.itemPrimaryTextColor)),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.2
)
),
environment: {},
containerSize: CGSize(width: availableSize.width - 32.0, height: 1000.0)
)
if let view = self.text.view {
if view.superview == nil {
self.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - textSize.width) / 2.0), y: 139.0), size: textSize)
}
return CGSize(width: availableSize.width, height: 210.0)
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private final class ParagraphComponent: CombinedComponent {
let title: String
let titleColor: UIColor
let text: String
let textColor: UIColor
let iconName: String
let iconColor: UIColor
public init(
title: String,
titleColor: UIColor,
text: String,
textColor: UIColor,
iconName: String,
iconColor: UIColor
) {
self.title = title
self.titleColor = titleColor
self.text = text
self.textColor = textColor
self.iconName = iconName
self.iconColor = iconColor
}
static func ==(lhs: ParagraphComponent, rhs: ParagraphComponent) -> Bool {
if lhs.title != rhs.title {
return false
}
if lhs.titleColor != rhs.titleColor {
return false
}
if lhs.text != rhs.text {
return false
}
if lhs.textColor != rhs.textColor {
return false
}
if lhs.iconName != rhs.iconName {
return false
}
if lhs.iconColor != rhs.iconColor {
return false
}
return true
}
static var body: Body {
let title = Child(MultilineTextComponent.self)
let text = Child(MultilineTextComponent.self)
let icon = Child(BundleIconComponent.self)
return { context in
let component = context.component
let leftInset: CGFloat = 64.0
let rightInset: CGFloat = 32.0
let textSideInset: CGFloat = leftInset + 8.0
let spacing: CGFloat = 5.0
let textTopInset: CGFloat = 9.0
let title = title.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(
string: component.title,
font: Font.semibold(15.0),
textColor: component.titleColor,
paragraphAlignment: .natural
)),
horizontalAlignment: .center,
maximumNumberOfLines: 1
),
availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
let textFont = Font.regular(15.0)
let boldTextFont = Font.semibold(15.0)
let textColor = component.textColor
let markdownAttributes = MarkdownAttributes(
body: MarkdownAttributeSet(font: textFont, textColor: textColor),
bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor),
link: MarkdownAttributeSet(font: textFont, textColor: textColor),
linkAttribute: { _ in
return nil
}
)
let text = text.update(
component: MultilineTextComponent(
text: .markdown(text: component.text, attributes: markdownAttributes),
horizontalAlignment: .natural,
maximumNumberOfLines: 0,
lineSpacing: 0.2
),
availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: context.availableSize.height),
transition: .immediate
)
let icon = icon.update(
component: BundleIconComponent(
name: component.iconName,
tintColor: component.iconColor
),
availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height),
transition: .immediate
)
context.add(title
.position(CGPoint(x: textSideInset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0))
)
context.add(text
.position(CGPoint(x: textSideInset + text.size.width / 2.0, y: textTopInset + title.size.height + spacing + text.size.height / 2.0))
)
context.add(icon
.position(CGPoint(x: 47.0, y: textTopInset + 18.0))
)
return CGSize(width: context.availableSize.width, height: textTopInset + title.size.height + text.size.height + 25.0)
}
}
}
private final class BusinessListComponent: CombinedComponent {
typealias EnvironmentType = (Empty, ScrollChildEnvironment)
let context: AccountContext
let theme: PresentationTheme
let topInset: CGFloat
let bottomInset: CGFloat
init(context: AccountContext, theme: PresentationTheme, topInset: CGFloat, bottomInset: CGFloat) {
self.context = context
self.theme = theme
self.topInset = topInset
self.bottomInset = bottomInset
}
static func ==(lhs: BusinessListComponent, rhs: BusinessListComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.topInset != rhs.topInset {
return false
}
if lhs.bottomInset != rhs.bottomInset {
return false
}
return true
}
final class State: ComponentState {
private let context: AccountContext
private var disposable: Disposable?
var limits: EngineConfiguration.UserLimits = .defaultValue
var premiumLimits: EngineConfiguration.UserLimits = .defaultValue
var accountPeer: EnginePeer?
init(context: AccountContext) {
self.context = context
super.init()
self.disposable = (context.engine.data.get(
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false),
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true),
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)
)
|> deliverOnMainQueue).start(next: { [weak self] limits, premiumLimits, accountPeer in
if let strongSelf = self {
strongSelf.limits = limits
strongSelf.premiumLimits = premiumLimits
strongSelf.accountPeer = accountPeer
strongSelf.updated(transition: .immediate)
}
})
}
deinit {
self.disposable?.dispose()
}
}
func makeState() -> State {
return State(context: self.context)
}
static var body: Body {
let list = Child(List<Empty>.self)
return { context in
let theme = context.component.theme
let strings = context.component.context.sharedContext.currentPresentationData.with { $0 }.strings
let colors = [
UIColor(rgb: 0xef6922),
UIColor(rgb: 0xe54937),
UIColor(rgb: 0xdb374b),
UIColor(rgb: 0xbc4395),
UIColor(rgb: 0x9b4fed),
UIColor(rgb: 0x8958ff),
UIColor(rgb: 0x676bff),
UIColor(rgb: 0x0088ff)
]
let titleColor = theme.list.itemPrimaryTextColor
let textColor = theme.list.itemSecondaryTextColor
var items: [AnyComponentWithIdentity<Empty>] = []
items.append(
AnyComponentWithIdentity(
id: "header",
component: AnyComponent(HeaderComponent(
context: context.component.context,
theme: theme,
strings: strings
))
)
)
items.append(
AnyComponentWithIdentity(
id: "location",
component: AnyComponent(ParagraphComponent(
title: strings.Premium_Business_Location_Title,
titleColor: titleColor,
text: strings.Premium_Business_Location_Text,
textColor: textColor,
iconName: "Premium/Business/Location",
iconColor: colors[0]
))
)
)
items.append(
AnyComponentWithIdentity(
id: "hours",
component: AnyComponent(ParagraphComponent(
title: strings.Premium_Business_Hours_Title,
titleColor: titleColor,
text: strings.Premium_Business_Hours_Text,
textColor: textColor,
iconName: "Premium/Business/Hours",
iconColor: colors[1]
))
)
)
items.append(
AnyComponentWithIdentity(
id: "replies",
component: AnyComponent(ParagraphComponent(
title: strings.Premium_Business_Replies_Title,
titleColor: titleColor,
text: strings.Premium_Business_Replies_Text,
textColor: textColor,
iconName: "Premium/Business/Replies",
iconColor: colors[2]
))
)
)
items.append(
AnyComponentWithIdentity(
id: "greetings",
component: AnyComponent(ParagraphComponent(
title: strings.Premium_Business_Greetings_Title,
titleColor: titleColor,
text: strings.Premium_Business_Greetings_Text,
textColor: textColor,
iconName: "Premium/Business/Greetings",
iconColor: colors[3]
))
)
)
items.append(
AnyComponentWithIdentity(
id: "away",
component: AnyComponent(ParagraphComponent(
title: strings.Premium_Business_Away_Title,
titleColor: titleColor,
text: strings.Premium_Business_Away_Text,
textColor: textColor,
iconName: "Premium/Business/Away",
iconColor: colors[4]
))
)
)
items.append(
AnyComponentWithIdentity(
id: "links",
component: AnyComponent(ParagraphComponent(
title: strings.Premium_Business_Links_Title,
titleColor: titleColor,
text: strings.Premium_Business_Links_Text,
textColor: textColor,
iconName: "Premium/Business/Links",
iconColor: colors[5]
))
)
)
items.append(
AnyComponentWithIdentity(
id: "intro",
component: AnyComponent(ParagraphComponent(
title: strings.Premium_Business_Intro_Title,
titleColor: titleColor,
text: strings.Premium_Business_Intro_Text,
textColor: textColor,
iconName: "Premium/Business/Intro",
iconColor: colors[6]
))
)
)
items.append(
AnyComponentWithIdentity(
id: "chatbots",
component: AnyComponent(ParagraphComponent(
title: strings.Premium_Business_Chatbots_Title,
titleColor: titleColor,
text: strings.Premium_Business_Chatbots_Text,
textColor: textColor,
iconName: "Premium/Business/Chatbots",
iconColor: colors[7]
))
)
)
let list = list.update(
component: List(items),
availableSize: CGSize(width: context.availableSize.width, height: 10000.0),
transition: context.transition
)
let contentHeight = context.component.topInset - 56.0 + list.size.height + context.component.bottomInset
context.add(list
.position(CGPoint(x: list.size.width / 2.0, y: context.component.topInset + list.size.height / 2.0))
)
return CGSize(width: context.availableSize.width, height: contentHeight)
}
}
}
final class BusinessPageComponent: CombinedComponent {
typealias EnvironmentType = DemoPageEnvironment
let context: AccountContext
let theme: PresentationTheme
let neighbors: PageNeighbors
let bottomInset: CGFloat
let updatedBottomAlpha: (CGFloat) -> Void
let updatedDismissOffset: (CGFloat) -> Void
let updatedIsDisplaying: (Bool) -> Void
init(context: AccountContext, theme: PresentationTheme, neighbors: PageNeighbors, bottomInset: CGFloat, updatedBottomAlpha: @escaping (CGFloat) -> Void, updatedDismissOffset: @escaping (CGFloat) -> Void, updatedIsDisplaying: @escaping (Bool) -> Void) {
self.context = context
self.theme = theme
self.neighbors = neighbors
self.bottomInset = bottomInset
self.updatedBottomAlpha = updatedBottomAlpha
self.updatedDismissOffset = updatedDismissOffset
self.updatedIsDisplaying = updatedIsDisplaying
}
static func ==(lhs: BusinessPageComponent, rhs: BusinessPageComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.neighbors != rhs.neighbors {
return false
}
if lhs.bottomInset != rhs.bottomInset {
return false
}
return true
}
final class State: ComponentState {
let updateBottomAlpha: (CGFloat) -> Void
let updateDismissOffset: (CGFloat) -> Void
let updatedIsDisplaying: (Bool) -> Void
var resetScroll: ActionSlot<CGPoint?>?
var topContentOffset: CGFloat = 0.0
var bottomContentOffset: CGFloat = 100.0 {
didSet {
self.updateAlpha()
}
}
var position: CGFloat? {
didSet {
self.updateAlpha()
}
}
var isDisplaying = false {
didSet {
if oldValue != self.isDisplaying {
self.updatedIsDisplaying(self.isDisplaying)
if !self.isDisplaying {
self.resetScroll?.invoke(nil)
}
}
}
}
var neighbors = PageNeighbors(leftIsList: false, rightIsList: false)
init(updateBottomAlpha: @escaping (CGFloat) -> Void, updateDismissOffset: @escaping (CGFloat) -> Void, updateIsDisplaying: @escaping (Bool) -> Void) {
self.updateBottomAlpha = updateBottomAlpha
self.updateDismissOffset = updateDismissOffset
self.updatedIsDisplaying = updateIsDisplaying
super.init()
}
func updateAlpha() {
var dismissToLeft = false
if let position = self.position, position > 0.0 {
dismissToLeft = true
}
var dismissPosition = min(1.0, abs(self.position ?? 0.0) / 1.3333)
var position = min(1.0, abs(self.position ?? 0.0))
if position > 0.001, (dismissToLeft && self.neighbors.leftIsList) || (!dismissToLeft && self.neighbors.rightIsList) {
dismissPosition = 0.0
position = 1.0
}
self.updateDismissOffset(dismissPosition)
let verticalPosition = 1.0 - min(30.0, self.bottomContentOffset) / 30.0
let backgroundAlpha: CGFloat = max(position, verticalPosition)
self.updateBottomAlpha(backgroundAlpha)
}
}
func makeState() -> State {
return State(updateBottomAlpha: self.updatedBottomAlpha, updateDismissOffset: self.updatedDismissOffset, updateIsDisplaying: self.updatedIsDisplaying)
}
static var body: Body {
let background = Child(Rectangle.self)
let scroll = Child(ScrollComponent<Empty>.self)
let topPanel = Child(BlurredBackgroundComponent.self)
let topSeparator = Child(Rectangle.self)
let title = Child(MultilineTextComponent.self)
let resetScroll = ActionSlot<CGPoint?>()
return { context in
let state = context.state
let environment = context.environment[DemoPageEnvironment.self].value
state.neighbors = context.component.neighbors
state.resetScroll = resetScroll
state.position = environment.position
state.isDisplaying = environment.isDisplaying
let theme = context.component.theme
let strings = context.component.context.sharedContext.currentPresentationData.with { $0 }.strings
let topInset: CGFloat = 56.0
let scroll = scroll.update(
component: ScrollComponent<Empty>(
content: AnyComponent(
BusinessListComponent(
context: context.component.context,
theme: theme,
topInset: topInset,
bottomInset: context.component.bottomInset + 110.0
)
),
contentInsets: UIEdgeInsets(top: topInset, left: 0.0, bottom: 0.0, right: 0.0),
contentOffsetUpdated: { [weak state] topContentOffset, bottomContentOffset in
state?.topContentOffset = topContentOffset
state?.bottomContentOffset = bottomContentOffset
Queue.mainQueue().justDispatch {
state?.updated(transition: .immediate)
}
},
contentOffsetWillCommit: { _ in },
resetScroll: resetScroll
),
availableSize: context.availableSize,
transition: context.transition
)
let background = background.update(
component: Rectangle(color: theme.overallDarkAppearance ? theme.list.blocksBackgroundColor : theme.list.plainBackgroundColor),
availableSize: scroll.size,
transition: context.transition
)
context.add(background
.position(CGPoint(x: context.availableSize.width / 2.0, y: background.size.height / 2.0))
)
context.add(scroll
.position(CGPoint(x: context.availableSize.width / 2.0, y: scroll.size.height / 2.0))
)
let topPanel = topPanel.update(
component: BlurredBackgroundComponent(
color: theme.rootController.navigationBar.blurredBackgroundColor
),
availableSize: CGSize(width: context.availableSize.width, height: topInset),
transition: context.transition
)
let topSeparator = topSeparator.update(
component: Rectangle(
color: theme.rootController.navigationBar.separatorColor
),
availableSize: CGSize(width: context.availableSize.width, height: UIScreenPixel),
transition: context.transition
)
let title = title.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(string: strings.Premium_Business, font: Font.semibold(20.0), textColor: theme.rootController.navigationBar.primaryTextColor)),
horizontalAlignment: .center,
truncationType: .end,
maximumNumberOfLines: 1
),
availableSize: context.availableSize,
transition: context.transition
)
let topPanelAlpha: CGFloat
if state.topContentOffset > 78.0 {
topPanelAlpha = min(30.0, state.topContentOffset - 78.0) / 30.0
} else {
topPanelAlpha = 0.0
}
context.add(topPanel
.position(CGPoint(x: context.availableSize.width / 2.0, y: topPanel.size.height / 2.0))
.opacity(topPanelAlpha)
)
context.add(topSeparator
.position(CGPoint(x: context.availableSize.width / 2.0, y: topPanel.size.height))
.opacity(topPanelAlpha)
)
let titleTopOriginY = topPanel.size.height / 2.0
let titleBottomOriginY: CGFloat = 176.0
let titleOriginDelta = titleTopOriginY - titleBottomOriginY
let fraction = min(1.0, state.topContentOffset / abs(titleOriginDelta))
let titleOriginY: CGFloat = titleBottomOriginY + fraction * titleOriginDelta
let titleScale = 1.0 - max(0.0, fraction * 0.2)
context.add(title
.position(CGPoint(x: context.availableSize.width / 2.0, y: titleOriginY))
.scale(titleScale)
)
return scroll.size
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,168 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ComponentFlow
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import ButtonComponent
final class CreateGiveawayFooterItem: ItemListControllerFooterItem {
let theme: PresentationTheme
let title: String
let badgeCount: Int32
let isLoading: Bool
let action: () -> Void
init(theme: PresentationTheme, title: String, badgeCount: Int32, isLoading: Bool, action: @escaping () -> Void) {
self.theme = theme
self.title = title
self.badgeCount = badgeCount
self.isLoading = isLoading
self.action = action
}
func isEqual(to: ItemListControllerFooterItem) -> Bool {
if let item = to as? CreateGiveawayFooterItem {
return self.theme === item.theme && self.title == item.title && self.badgeCount == item.badgeCount && self.isLoading == item.isLoading
} else {
return false
}
}
func node(current: ItemListControllerFooterItemNode?) -> ItemListControllerFooterItemNode {
if let current = current as? CreateGiveawayFooterItemNode {
current.item = self
return current
} else {
return CreateGiveawayFooterItemNode(item: self)
}
}
}
final class CreateGiveawayFooterItemNode: ItemListControllerFooterItemNode {
private let backgroundNode: NavigationBackgroundNode
private let separatorNode: ASDisplayNode
private let button = ComponentView<Empty>()
private var validLayout: ContainerViewLayout?
private var currentIsLoading = false
var item: CreateGiveawayFooterItem {
didSet {
self.updateItem()
if let layout = self.validLayout {
let _ = self.updateLayout(layout: layout, transition: .immediate)
}
}
}
init(item: CreateGiveawayFooterItem) {
self.item = item
self.backgroundNode = NavigationBackgroundNode(color: item.theme.rootController.tabBar.backgroundColor)
self.separatorNode = ASDisplayNode()
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.separatorNode)
self.updateItem()
}
private func updateItem() {
self.backgroundNode.updateColor(color: self.item.theme.rootController.tabBar.backgroundColor, transition: .immediate)
self.separatorNode.backgroundColor = self.item.theme.rootController.tabBar.separatorColor
}
override func updateBackgroundAlpha(_ alpha: CGFloat, transition: ContainedViewLayoutTransition) {
transition.updateAlpha(node: self.backgroundNode, alpha: alpha)
transition.updateAlpha(node: self.separatorNode, alpha: alpha)
}
override func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) -> CGFloat {
let hadLayout = self.validLayout != nil
self.validLayout = layout
let buttonInset: CGFloat = 16.0
let buttonWidth = layout.size.width - layout.safeInsets.left - layout.safeInsets.right - buttonInset * 2.0
let inset: CGFloat = 9.0
let insets = layout.insets(options: [.input])
var panelHeight: CGFloat = 50.0 + inset * 2.0
let totalPanelHeight: CGFloat
if let inputHeight = layout.inputHeight, inputHeight > 0.0 {
totalPanelHeight = panelHeight + insets.bottom
} else {
totalPanelHeight = panelHeight + insets.bottom
panelHeight += insets.bottom
}
let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - totalPanelHeight), size: CGSize(width: layout.size.width, height: panelHeight))
var buttonTransition: ComponentTransition = .easeInOut(duration: 0.2)
if !hadLayout {
buttonTransition = .immediate
}
let buttonSize = self.button.update(
transition: buttonTransition,
component: AnyComponent(
ButtonComponent(
background: ButtonComponent.Background(
color: self.item.theme.list.itemCheckColors.fillColor,
foreground: self.item.theme.list.itemCheckColors.foregroundColor,
pressedColor: self.item.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
),
content: AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(ButtonTextContentComponent(
text: self.item.title,
badge: Int(self.item.badgeCount),
textColor: self.item.theme.list.itemCheckColors.foregroundColor,
badgeBackground: self.item.theme.list.itemCheckColors.foregroundColor,
badgeForeground: self.item.theme.list.itemCheckColors.fillColor,
badgeStyle: .roundedRectangle,
badgeIconName: "Premium/BoostButtonIcon",
combinedAlignment: true
))
),
isEnabled: true,
displaysProgress: self.item.isLoading,
action: { [weak self] in
guard let self else {
return
}
self.item.action()
}
)
),
environment: {},
containerSize: CGSize(width: buttonWidth, height: 50.0)
)
if let view = self.button.view {
if view.superview == nil {
self.view.addSubview(view)
}
transition.updateFrame(view: view, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + buttonInset, y: panelFrame.minY + inset), size: buttonSize))
}
transition.updateFrame(node: self.backgroundNode, frame: panelFrame)
self.backgroundNode.update(size: panelFrame.size, transition: transition)
transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: panelFrame.origin, size: CGSize(width: panelFrame.width, height: UIScreenPixel)))
return totalPanelHeight
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
if self.backgroundNode.frame.contains(point) {
return true
} else {
return false
}
}
}
@@ -0,0 +1,260 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import Markdown
import ComponentFlow
import PremiumStarComponent
final class CreateGiveawayHeaderItem: ItemListControllerHeaderItem {
let theme: PresentationTheme
let strings: PresentationStrings
let title: String
let text: String
let isStars: Bool
let cancel: () -> Void
init(theme: PresentationTheme, strings: PresentationStrings, title: String, text: String, isStars: Bool, cancel: @escaping () -> Void) {
self.theme = theme
self.strings = strings
self.title = title
self.text = text
self.isStars = isStars
self.cancel = cancel
}
func isEqual(to: ItemListControllerHeaderItem) -> Bool {
if let item = to as? CreateGiveawayHeaderItem {
return self.theme === item.theme && self.title == item.title && self.text == item.text && self.isStars == item.isStars
} else {
return false
}
}
func node(current: ItemListControllerHeaderItemNode?) -> ItemListControllerHeaderItemNode {
if let current = current as? CreateGiveawayHeaderItemNode {
current.item = self
return current
} else {
return CreateGiveawayHeaderItemNode(item: self)
}
}
}
private let titleFont = Font.semibold(20.0)
private let textFont = Font.regular(15.0)
class CreateGiveawayHeaderItemNode: ItemListControllerHeaderItemNode {
private let backgroundNode: NavigationBackgroundNode
private let separatorNode: ASDisplayNode
private let titleNode: ImmediateTextNode
private let textNode: ImmediateTextNode
private let cancelNode: HighlightableButtonNode
private var hostView: ComponentHostView<Empty>?
private var component: AnyComponent<Empty>?
private var validLayout: ContainerViewLayout?
fileprivate var item: CreateGiveawayHeaderItem {
didSet {
self.updateItem()
if let layout = self.validLayout {
let _ = self.updateLayout(layout: layout, transition: .immediate)
}
}
}
init(item: CreateGiveawayHeaderItem) {
self.item = item
self.backgroundNode = NavigationBackgroundNode(color: item.theme.rootController.navigationBar.blurredBackgroundColor)
self.separatorNode = ASDisplayNode()
self.titleNode = ImmediateTextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
self.textNode = ImmediateTextNode()
self.textNode.isUserInteractionEnabled = false
self.textNode.contentMode = .left
self.textNode.contentsScale = UIScreen.main.scale
self.textNode.maximumNumberOfLines = 0
self.cancelNode = HighlightableButtonNode()
super.init()
self.addSubnode(self.textNode)
self.addSubnode(self.backgroundNode)
self.addSubnode(self.separatorNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.cancelNode)
self.cancelNode.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside)
self.updateItem()
}
@objc private func cancelPressed() {
self.item.cancel()
}
override func didLoad() {
super.didLoad()
let hostView = ComponentHostView<Empty>()
self.hostView = hostView
self.view.insertSubview(hostView, at: 0)
if let layout = self.validLayout, let component = self.component {
let containerSize = CGSize(width: min(414.0, layout.size.width), height: 220.0)
let size = hostView.update(
transition: .immediate,
component: component,
environment: {},
containerSize: containerSize
)
hostView.bounds = CGRect(origin: .zero, size: size)
}
}
func updateItem() {
self.backgroundNode.updateColor(color: self.item.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate)
self.separatorNode.backgroundColor = self.item.theme.rootController.navigationBar.separatorColor
let attributedTitle = NSAttributedString(string: self.item.title, font: titleFont, textColor: self.item.theme.list.itemPrimaryTextColor, paragraphAlignment: .center)
let attributedText = NSAttributedString(string: self.item.text, font: textFont, textColor: self.item.theme.list.freeTextColor, paragraphAlignment: .center)
self.titleNode.attributedText = attributedTitle
self.textNode.attributedText = attributedText
self.cancelNode.setAttributedTitle(NSAttributedString(string: self.item.strings.Common_Cancel, font: Font.regular(17.0), textColor: self.item.theme.rootController.navigationBar.accentTextColor), for: .normal)
}
override func updateContentOffset(_ contentOffset: CGFloat, transition: ContainedViewLayoutTransition) {
guard let layout = self.validLayout else {
return
}
let navigationHeight: CGFloat = 56.0
let statusBarHeight = layout.statusBarHeight ?? 0.0
let topInset : CGFloat = 0.0
let titleOffsetDelta = (topInset + 160.0) - (statusBarHeight + (navigationHeight - statusBarHeight) / 2.0)
let topContentOffset = contentOffset + max(0.0, min(1.0, contentOffset / titleOffsetDelta)) * 10.0
let titleOffset = topContentOffset
let fraction = max(0.0, min(1.0, titleOffset / titleOffsetDelta))
let titleScale = 1.0 - fraction * 0.18
let topPanelAlpha = min(20.0, max(0.0, contentOffset - 95.0)) / 20.0
transition.updateAlpha(node: self.backgroundNode, alpha: topPanelAlpha)
transition.updateAlpha(node: self.separatorNode, alpha: topPanelAlpha)
let starPosition = CGPoint(
x: layout.size.width / 2.0,
y: -contentOffset + 80.0
)
if let view = self.hostView {
transition.updatePosition(layer: view.layer, position: starPosition)
}
let titlePosition = CGPoint(
x: layout.size.width / 2.0,
y: max(topInset + 170.0 - titleOffset, statusBarHeight + (navigationHeight - statusBarHeight) / 2.0)
)
transition.updatePosition(node: self.titleNode, position: titlePosition)
transition.updateTransformScale(node: self.titleNode, scale: titleScale)
let textPosition = CGPoint(
x: layout.size.width / 2.0,
y: -contentOffset + 212.0
)
transition.updatePosition(node: self.textNode, position: textPosition)
}
override func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) -> CGFloat {
let leftInset: CGFloat = 24.0
let navigationBarHeight: CGFloat = 56.0
let constrainedSize = CGSize(width: layout.size.width - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude)
let titleSize = self.titleNode.updateLayout(constrainedSize)
let textSize = self.textNode.updateLayout(constrainedSize)
let cancelSize = self.cancelNode.measure(constrainedSize)
transition.updateFrame(node: self.cancelNode, frame: CGRect(origin: CGPoint(x: 16.0 + layout.safeInsets.left, y: floorToScreenPixels((navigationBarHeight - cancelSize.height) / 2.0)), size: cancelSize))
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: .zero, size: CGSize(width: layout.size.width, height: navigationBarHeight)))
transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: UIScreenPixel)))
self.backgroundNode.update(size: CGSize(width: layout.size.width, height: navigationBarHeight), transition: transition)
let colors: [UIColor]
let particleColor: UIColor?
if self.item.isStars {
colors = [
UIColor(rgb: 0xe57d02),
UIColor(rgb: 0xf09903),
UIColor(rgb: 0xf9b004),
UIColor(rgb: 0xfdd219)
]
particleColor = UIColor(rgb: 0xf9b004)
} else {
colors = [
UIColor(rgb: 0x6a94ff),
UIColor(rgb: 0x9472fd),
UIColor(rgb: 0xe26bd3)
]
particleColor = nil
}
let component = AnyComponent(PremiumStarComponent(
theme: self.item.theme,
isIntro: true,
isVisible: true,
hasIdleAnimations: true,
colors: colors,
particleColor: particleColor,
backgroundColor: self.item.theme.list.blocksBackgroundColor
))
let containerSize = CGSize(width: min(414.0, layout.size.width), height: 220.0)
if let hostView = self.hostView {
let size = hostView.update(
transition: .immediate,
component: component,
environment: {},
containerSize: containerSize
)
hostView.bounds = CGRect(origin: .zero, size: size)
}
self.titleNode.bounds = CGRect(origin: .zero, size: titleSize)
self.textNode.bounds = CGRect(origin: .zero, size: textSize)
let contentHeight = titleSize.height + textSize.height + 128.0
self.component = component
self.validLayout = layout
return contentHeight
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
if let hostView = self.hostView, hostView.frame.contains(point) {
return true
} else {
return super.point(inside: point, with: event)
}
}
}
@@ -0,0 +1,179 @@
import Foundation
import Metal
import MetalKit
import Display
@available(iOS 10.0, *)
public final class MatrixView: MTKView, MTKViewDelegate, PhoneDemoDecorationView {
public func draw(in view: MTKView) {
}
private let commandQueue: MTLCommandQueue
private let drawPassthroughPipelineState: MTLRenderPipelineState
private let symbolTexture: MTLTexture
private let randomTexture: MTLTexture
private var viewportDimensions = CGSize(width: 1, height: 1)
private var displayLink: SharedDisplayLinkDriver.Link?
private var startTimestamp = CACurrentMediaTime()
public init?(test: Bool) {
let mainBundle = Bundle(for: MatrixView.self)
guard let path = mainBundle.path(forResource: "PremiumUIBundle", ofType: "bundle") else {
return nil
}
guard let bundle = Bundle(path: path) else {
return nil
}
guard let device = MTLCreateSystemDefaultDevice() else {
return nil
}
guard let defaultLibrary = try? device.makeDefaultLibrary(bundle: bundle) else {
return nil
}
guard let commandQueue = device.makeCommandQueue() else {
return nil
}
self.commandQueue = commandQueue
guard let loadedVertexProgram = defaultLibrary.makeFunction(name: "matrixVertex") else {
return nil
}
guard let loadedFragmentProgram = defaultLibrary.makeFunction(name: "matrixFragment") else {
return nil
}
let textureLoader = MTKTextureLoader(device: device)
let pipelineStateDescriptor = MTLRenderPipelineDescriptor()
pipelineStateDescriptor.vertexFunction = loadedVertexProgram
pipelineStateDescriptor.fragmentFunction = loadedFragmentProgram
pipelineStateDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
pipelineStateDescriptor.colorAttachments[0].isBlendingEnabled = true
pipelineStateDescriptor.colorAttachments[0].rgbBlendOperation = .add
pipelineStateDescriptor.colorAttachments[0].alphaBlendOperation = .add
pipelineStateDescriptor.colorAttachments[0].sourceRGBBlendFactor = .sourceAlpha
pipelineStateDescriptor.colorAttachments[0].sourceAlphaBlendFactor = .sourceAlpha
pipelineStateDescriptor.colorAttachments[0].destinationRGBBlendFactor = .oneMinusSourceAlpha
pipelineStateDescriptor.colorAttachments[0].destinationAlphaBlendFactor = .oneMinusSourceAlpha
self.drawPassthroughPipelineState = try! device.makeRenderPipelineState(descriptor: pipelineStateDescriptor)
guard let url = bundle.url(forResource: "chars", withExtension: "png"), let texture = try? textureLoader.newTexture(URL: url, options: nil) else {
return nil
}
self.symbolTexture = texture
guard let url = bundle.url(forResource: "random", withExtension: "jpg"), let texture = try? textureLoader.newTexture(URL: url, options: nil) else {
return nil
}
self.randomTexture = texture
super.init(frame: CGRect(), device: device)
self.delegate = self
self.isOpaque = false
self.backgroundColor = .clear
self.framebufferOnly = true
self.displayLink = SharedDisplayLinkDriver.shared.add { [weak self] _ in
self?.tick()
}
self.displayLink?.isPaused = true
self.isPaused = true
}
public func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
self.viewportDimensions = size
}
required public init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.displayLink?.invalidate()
}
func setVisible(_ visible: Bool) {
if visible {
self.displayLink?.isPaused = false
}
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear)
transition.updateAlpha(layer: self.layer, alpha: visible ? 0.4 : 0.0, completion: { [weak self] finished in
if let strongSelf = self, finished && !visible {
strongSelf.displayLink?.isPaused = false
}
})
}
func resetAnimation() {
}
private func tick() {
self.draw()
}
override public func draw(_ rect: CGRect) {
self.redraw(drawable: self.currentDrawable!)
}
private func redraw(drawable: MTLDrawable) {
guard let commandBuffer = self.commandQueue.makeCommandBuffer() else {
return
}
let renderPassDescriptor = self.currentRenderPassDescriptor!
renderPassDescriptor.colorAttachments[0].loadAction = .clear
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 0.0)
guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else {
return
}
let viewportDimensions = self.viewportDimensions
renderEncoder.setViewport(MTLViewport(originX: 0.0, originY: 0.0, width: viewportDimensions.width, height: viewportDimensions.height, znear: -1.0, zfar: 1.0))
renderEncoder.setRenderPipelineState(self.drawPassthroughPipelineState)
var vertices: [Float] = [
1, -1,
-1, -1,
-1, 1,
1, -1,
-1, 1,
1, 1
]
renderEncoder.setVertexBytes(&vertices, length: 4 * vertices.count, index: 0)
renderEncoder.setFragmentTexture(self.symbolTexture, index: 0)
renderEncoder.setFragmentTexture(self.randomTexture, index: 1)
var resolution = simd_uint2(UInt32(viewportDimensions.width), UInt32(viewportDimensions.height))
renderEncoder.setFragmentBytes(&resolution, length: MemoryLayout<simd_uint2>.size * 2, index: 0)
var time = Float(CACurrentMediaTime() - self.startTimestamp) * 0.75
renderEncoder.setFragmentBytes(&time, length: 4, index: 1)
renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6, instanceCount: 1)
renderEncoder.endEncoding()
commandBuffer.present(drawable)
commandBuffer.commit()
}
}
@@ -0,0 +1,225 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import SwiftSignalKit
import GZip
import AppBundle
import LegacyComponents
import AvatarNode
import AccountContext
import TelegramCore
import AnimationCache
import MultiAnimationRenderer
import EmojiStatusComponent
class EmojiHeaderComponent: Component {
let context: AccountContext
let animationCache: AnimationCache
let animationRenderer: MultiAnimationRenderer
let placeholderColor: UIColor
let accentColor: UIColor
let fileId: Int64
let file: TelegramMediaFile?
let isVisible: Bool
let hasIdleAnimations: Bool
init(
context: AccountContext,
animationCache: AnimationCache,
animationRenderer: MultiAnimationRenderer,
placeholderColor: UIColor,
accentColor: UIColor,
fileId: Int64,
file: TelegramMediaFile? = nil,
isVisible: Bool,
hasIdleAnimations: Bool
) {
self.context = context
self.animationCache = animationCache
self.animationRenderer = animationRenderer
self.placeholderColor = placeholderColor
self.accentColor = accentColor
self.fileId = fileId
self.file = file
self.isVisible = isVisible
self.hasIdleAnimations = hasIdleAnimations
}
static func ==(lhs: EmojiHeaderComponent, rhs: EmojiHeaderComponent) -> Bool {
return lhs.placeholderColor == rhs.placeholderColor && lhs.accentColor == rhs.accentColor && lhs.fileId == rhs.fileId && lhs.isVisible == rhs.isVisible && lhs.hasIdleAnimations == rhs.hasIdleAnimations
}
final class View: UIView, ComponentTaggedView {
final class Tag {
}
func matches(tag: Any) -> Bool {
if let _ = tag as? Tag {
return true
}
return false
}
private var _ready = Promise<Bool>(true)
var ready: Signal<Bool, NoError> {
return self._ready.get()
}
weak var animateFrom: UIView?
var sourceRect: CGRect?
weak var containerView: UIView?
let statusView: ComponentHostView<Empty>
private var hasIdleAnimations = false
override init(frame: CGRect) {
self.statusView = ComponentHostView<Empty>()
super.init(frame: frame)
self.statusView.isHidden = true
self.addSubview(self.statusView)
self.disablesInteractiveModalDismiss = true
self.disablesInteractiveTransitionGestureRecognizer = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var scheduledAnimateIn = false
override func didMoveToWindow() {
super.didMoveToWindow()
if self.scheduledAnimateIn {
self.animateIn()
self.scheduledAnimateIn = false
}
}
func animateIn() {
guard let animateFrom = self.animateFrom, var containerView = self.containerView else {
return
}
guard let _ = self.window else {
self.scheduledAnimateIn = true
return
}
self.statusView.isHidden = false
if containerView.subviews.count > 1 && containerView.subviews[1].subviews.count > 1 {
let candidateView = containerView.subviews[1].subviews[1]
if !(candidateView is UIVisualEffectView) {
containerView = candidateView
}
}
let initialPosition = self.statusView.center
let targetPosition = self.statusView.superview!.convert(self.statusView.center, to: containerView)
var sourceOffset: CGPoint = .zero
if let sourceRect = self.sourceRect {
sourceOffset = CGPoint(x: sourceRect.center.x - animateFrom.frame.width / 2.0, y: 0.0)
}
let sourcePosition = animateFrom.superview!.convert(animateFrom.center, to: containerView).offsetBy(dx: sourceOffset.x, dy: sourceOffset.y)
containerView.addSubview(self.statusView)
self.statusView.center = targetPosition
animateFrom.alpha = 0.0
self.statusView.layer.animateScale(from: 0.24, to: 1.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.linear.rawValue)
self.statusView.layer.animatePosition(from: sourcePosition, to: targetPosition, duration: 0.55, timingFunction: kCAMediaTimingFunctionSpring)
Queue.mainQueue().after(0.55, {
self.statusView.layer.removeAllAnimations()
self.addSubview(self.statusView)
self.statusView.center = initialPosition
})
Queue.mainQueue().after(0.4, {
animateFrom.alpha = 1.0
})
self.animateFrom = nil
self.containerView = nil
}
func update(component: EmojiHeaderComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
self.hasIdleAnimations = component.hasIdleAnimations
let size = self.statusView.update(
transition: .immediate,
component: AnyComponent(EmojiStatusComponent(
context: component.context,
animationCache: component.animationCache,
animationRenderer: component.animationRenderer,
content: .animation(
content: component.file.flatMap { .file(file: $0) } ?? .customEmoji(fileId: component.fileId),
size: CGSize(width: 100.0, height: 100.0),
placeholderColor: component.placeholderColor,
themeColor: component.accentColor,
loopMode: .forever
),
isVisibleForAnimations: true,
action: nil
)),
environment: {},
containerSize: CGSize(width: 96.0, height: 96.0)
)
self.statusView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - size.width) / 2.0), y: 63.0), size: size)
if let _ = component.file {
self.statusView.isHidden = false
}
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
private func generateParabollicMotionKeyframes(from sourcePoint: CGPoint, to targetPosition: CGPoint, elevation: CGFloat) -> [CGPoint] {
let midPoint = CGPoint(x: (sourcePoint.x + targetPosition.x) / 2.0, y: sourcePoint.y - elevation)
let x1 = sourcePoint.x
let y1 = sourcePoint.y
let x2 = midPoint.x
let y2 = midPoint.y
let x3 = targetPosition.x
let y3 = targetPosition.y
var keyframes: [CGPoint] = []
if abs(y1 - y3) < 5.0 && abs(x1 - x3) < 5.0 {
for i in 0 ..< 10 {
let k = CGFloat(i) / CGFloat(10 - 1)
let x = sourcePoint.x * (1.0 - k) + targetPosition.x * k
let y = sourcePoint.y * (1.0 - k) + targetPosition.y * k
keyframes.append(CGPoint(x: x, y: y))
}
} else {
let a = (x3 * (y2 - y1) + x2 * (y1 - y3) + x1 * (y3 - y2)) / ((x1 - x2) * (x1 - x3) * (x2 - x3))
let b = (x1 * x1 * (y2 - y3) + x3 * x3 * (y1 - y2) + x2 * x2 * (y3 - y1)) / ((x1 - x2) * (x1 - x3) * (x2 - x3))
let c = (x2 * x2 * (x3 * y1 - x1 * y3) + x2 * (x1 * x1 * y3 - x3 * x3 * y1) + x1 * x3 * (x3 - x1) * y2) / ((x1 - x2) * (x1 - x3) * (x2 - x3))
for i in 0 ..< 10 {
let k = CGFloat(i) / CGFloat(10 - 1)
let x = sourcePoint.x * (1.0 - k) + targetPosition.x * k
let y = a * x * x + b * x + c
keyframes.append(CGPoint(x: x, y: y))
}
}
return keyframes
}
@@ -0,0 +1,139 @@
import Foundation
import UIKit
import SceneKit
import Display
import AppBundle
import LegacyComponents
import PremiumStarComponent
private let sceneVersion: Int = 1
final class FasterStarsView: UIView, PhoneDemoDecorationView {
private let sceneView: SCNView
private var particles: SCNNode?
override init(frame: CGRect) {
self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size))
self.sceneView.backgroundColor = .clear
if let scene = loadCompressedScene(name: "lightspeed", version: sceneVersion) {
self.sceneView.scene = scene
}
self.sceneView.isUserInteractionEnabled = false
self.sceneView.preferredFramesPerSecond = 60
super.init(frame: frame)
self.alpha = 0.0
self.addSubview(self.sceneView)
self.particles = self.sceneView.scene?.rootNode.childNode(withName: "particles", recursively: false)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.particles = nil
}
func setVisible(_ visible: Bool) {
if visible, let particles = self.particles, particles.parent == nil {
self.sceneView.scene?.rootNode.addChildNode(particles)
}
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear)
transition.updateAlpha(layer: self.layer, alpha: visible ? 0.4 : 0.0, completion: { [weak self] finished in
if let strongSelf = self, finished && !visible && strongSelf.particles?.parent != nil {
strongSelf.particles?.removeFromParentNode()
}
})
}
private var playing = false
func startAnimation() {
guard !self.playing, let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "particles", recursively: false), let particles = node.particleSystems?.first else {
return
}
self.playing = true
let speedAnimation = POPBasicAnimation()
speedAnimation.property = (POPAnimatableProperty.property(withName: "speedFactor", initializer: { property in
property?.readBlock = { particleSystem, values in
values?.pointee = (particleSystem as! SCNParticleSystem).speedFactor
}
property?.writeBlock = { particleSystem, values in
(particleSystem as! SCNParticleSystem).speedFactor = values!.pointee
}
property?.threshold = 0.01
}) as! POPAnimatableProperty)
speedAnimation.fromValue = 1.0 as NSNumber
speedAnimation.toValue = 3.0 as NSNumber
speedAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
speedAnimation.duration = 0.8
particles.pop_add(speedAnimation, forKey: "speedFactor")
let stretchAnimation = POPBasicAnimation()
stretchAnimation.property = (POPAnimatableProperty.property(withName: "stretchFactor", initializer: { property in
property?.readBlock = { particleSystem, values in
values?.pointee = (particleSystem as! SCNParticleSystem).stretchFactor
}
property?.writeBlock = { particleSystem, values in
(particleSystem as! SCNParticleSystem).stretchFactor = values!.pointee
}
property?.threshold = 0.01
}) as! POPAnimatableProperty)
stretchAnimation.fromValue = 0.05 as NSNumber
stretchAnimation.toValue = 0.3 as NSNumber
stretchAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
stretchAnimation.duration = 0.8
particles.pop_add(stretchAnimation, forKey: "stretchFactor")
}
func resetAnimation() {
guard self.playing, let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "particles", recursively: false), let particles = node.particleSystems?.first else {
return
}
self.playing = false
let speedAnimation = POPBasicAnimation()
speedAnimation.property = (POPAnimatableProperty.property(withName: "speedFactor", initializer: { property in
property?.readBlock = { particleSystem, values in
values?.pointee = (particleSystem as! SCNParticleSystem).speedFactor
}
property?.writeBlock = { particleSystem, values in
(particleSystem as! SCNParticleSystem).speedFactor = values!.pointee
}
property?.threshold = 0.01
}) as! POPAnimatableProperty)
speedAnimation.fromValue = 3.0 as NSNumber
speedAnimation.toValue = 1.0 as NSNumber
speedAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
speedAnimation.duration = 0.35
particles.pop_add(speedAnimation, forKey: "speedFactor")
let stretchAnimation = POPBasicAnimation()
stretchAnimation.property = (POPAnimatableProperty.property(withName: "stretchFactor", initializer: { property in
property?.readBlock = { particleSystem, values in
values?.pointee = (particleSystem as! SCNParticleSystem).stretchFactor
}
property?.writeBlock = { particleSystem, values in
(particleSystem as! SCNParticleSystem).stretchFactor = values!.pointee
}
property?.threshold = 0.01
}) as! POPAnimatableProperty)
stretchAnimation.fromValue = 0.3 as NSNumber
stretchAnimation.toValue = 0.05 as NSNumber
stretchAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
stretchAnimation.duration = 0.35
particles.pop_add(stretchAnimation, forKey: "stretchFactor")
}
override func layoutSubviews() {
super.layoutSubviews()
self.sceneView.frame = CGRect(origin: .zero, size: frame.size)
}
}
@@ -0,0 +1,792 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AccountContext
import AvatarNode
public final class GiftOptionItem: ListViewItem, ItemListItem {
public enum Icon: Equatable {
public enum Color {
case blue
case green
case red
case violet
case premium
case stars
}
case peer(EnginePeer)
case image(color: Color, name: String)
}
public enum Font {
case regular
case bold
}
public enum SubtitleFont {
case regular
case small
}
public enum Label {
case generic(String)
case semitransparent(String)
case boosts(Int32)
var string: String {
switch self {
case let .generic(value), let .semitransparent(value):
return value
case let .boosts(value):
return "\(value)"
}
}
}
let presentationData: ItemListPresentationData
let context: AccountContext
let icon: Icon?
let title: String
let titleFont: Font
let titleBadge: String?
let subtitle: String?
let subtitleFont: SubtitleFont
let subtitleActive: Bool
let label: Label?
let badge: String?
let isSelected: Bool?
let stars: Int64?
public let sectionId: ItemListSectionId
let action: (() -> Void)?
public init(presentationData: ItemListPresentationData, context: AccountContext, icon: Icon? = nil, title: String, titleFont: Font = .regular, titleBadge: String? = nil, subtitle: String?, subtitleFont: SubtitleFont = .regular, subtitleActive: Bool = false, label: Label? = nil, badge: String? = nil, isSelected: Bool? = nil, stars: Int64? = nil, sectionId: ItemListSectionId, action: (() -> Void)?) {
self.presentationData = presentationData
self.icon = icon
self.context = context
self.title = title
self.titleFont = titleFont
self.titleBadge = titleBadge
self.subtitle = subtitle
self.subtitleFont = subtitleFont
self.subtitleActive = subtitleActive
self.label = label
self.badge = badge
self.isSelected = isSelected
self.stars = stars
self.sectionId = sectionId
self.action = action
}
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = GiftOptionItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply(false) })
})
}
}
}
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? GiftOptionItemNode {
let makeLayout = nodeValue.asyncLayout()
var animated = true
if case .None = animation {
animated = false
}
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply(animated)
})
}
}
}
}
}
public var selectable: Bool {
return self.action != nil
}
public func selected(listView: ListView){
listView.clearHighlightAnimated(true)
self.action?()
}
}
class GiftOptionItemNode: ItemListRevealOptionsItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private let maskNode: ASImageNode
private let containerNode: ASDisplayNode
override var controlsContainer: ASDisplayNode {
return self.containerNode
}
fileprivate var iconNode: ASImageNode?
fileprivate var avatarNode: AvatarNode?
private let titleNode: TextNode
private let titleBadge = ComponentView<Empty>()
private let statusNode: TextNode
private var statusArrowNode: ASImageNode?
private var starsIconNode: ASImageNode?
private var labelBackgroundNode: ASImageNode?
private let labelNode: TextNode
private var labelIconNode: ASImageNode?
private let badgeTextNode: TextNode
private var badgeBackgroundNode: ASImageNode?
private var layoutParams: (GiftOptionItem, ListViewItemLayoutParams, ItemListNeighbors)?
private var selectableControlNode: ItemListSelectableControlNode?
private let activateArea: AccessibilityAreaNode
private let fetchDisposable = MetaDisposable()
override var canBeSelected: Bool {
return true
}
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.containerNode = ASDisplayNode()
self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
self.statusNode = TextNode()
self.statusNode.isUserInteractionEnabled = false
self.statusNode.contentMode = .left
self.statusNode.contentsScale = UIScreen.main.scale
self.labelNode = TextNode()
self.labelNode.isUserInteractionEnabled = false
self.labelNode.contentMode = .left
self.labelNode.contentsScale = UIScreen.main.scale
self.badgeTextNode = TextNode()
self.badgeTextNode.isUserInteractionEnabled = false
self.badgeTextNode.contentMode = .left
self.badgeTextNode.contentsScale = UIScreen.main.scale
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
self.activateArea = AccessibilityAreaNode()
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
self.addSubnode(self.containerNode)
self.containerNode.addSubnode(self.titleNode)
self.containerNode.addSubnode(self.statusNode)
self.containerNode.addSubnode(self.labelNode)
self.addSubnode(self.activateArea)
}
override func tapped() {
guard let item = self.layoutParams?.0 else {
return
}
item.action?()
}
func asyncLayout() -> (_ item: GiftOptionItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeStatusLayout = TextNode.asyncLayout(self.statusNode)
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
let makeBadgeLayout = TextNode.asyncLayout(self.badgeTextNode)
let selectableControlLayout = ItemListSelectableControlNode.asyncLayout(self.selectableControlNode)
let currentItem = self.layoutParams?.0
return { item, params, neighbors in
let titleFont: UIFont
switch item.titleFont {
case .regular:
titleFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 17.0 / 17.0))
case .bold:
titleFont = Font.semibold(floor(item.presentationData.fontSize.itemListBaseFontSize * 17.0 / 17.0))
}
let statusFont: UIFont
switch item.subtitleFont {
case .regular:
statusFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0))
case .small:
statusFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 13.0 / 17.0))
}
var updatedTheme: PresentationTheme?
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
}
let rightInset: CGFloat = params.rightInset
let titleAttributedString = NSAttributedString(string: item.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor)
let statusAttributedString = NSAttributedString(string: item.subtitle ?? "", font: statusFont, textColor: item.subtitleActive ? item.presentationData.theme.list.itemAccentColor : item.presentationData.theme.list.itemSecondaryTextColor)
let badgeAttributedString = NSAttributedString(string: item.badge ?? "", font: Font.with(size: 13.0, design: .round, weight: .semibold), textColor: item.presentationData.theme.list.itemCheckColors.foregroundColor)
let labelColor: UIColor
let labelFont: UIFont
if let label = item.label, case .boosts = label {
labelColor = item.presentationData.theme.list.itemAccentColor
labelFont = Font.semibold(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0))
} else if let label = item.label, case .semitransparent = label {
labelColor = item.presentationData.theme.list.itemAccentColor
labelFont = Font.semibold(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0))
} else {
labelColor = item.presentationData.theme.list.itemSecondaryTextColor
labelFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 17.0 / 17.0))
}
let labelAttributedString = NSAttributedString(string: item.label?.string ?? "", font: labelFont, textColor: labelColor)
let leftInset: CGFloat = 14.0 + params.leftInset
var avatarInset: CGFloat = 0.0
if let _ = item.icon {
avatarInset += 48.0
}
let verticalInset: CGFloat = 10.0
var titleSpacing: CGFloat = 2.0
if case .bold = item.titleFont {
titleSpacing = 0.0
}
let insets = itemListNeighborsGroupedInsets(neighbors, params)
let separatorHeight = UIScreenPixel
var selectableControlSizeAndApply: (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode)?
var editingOffset: CGFloat = 0.0
if let isSelected = item.isSelected {
let sizeAndApply = selectableControlLayout(item.presentationData.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.list.itemCheckColors.fillColor, item.presentationData.theme.list.itemCheckColors.foregroundColor, isSelected, .regular)
selectableControlSizeAndApply = sizeAndApply
editingOffset = sizeAndApply.0
}
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: labelAttributedString, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: .greatestFiniteMagnitude)))
var textConstrainedWidth = params.width - leftInset - 8.0 - editingOffset - rightInset - labelLayout.size.width - avatarInset
var subtitleConstrainedWidth = textConstrainedWidth
if let label = item.label, case .semitransparent = label {
textConstrainedWidth -= 54.0
subtitleConstrainedWidth -= 30.0
}
if let _ = item.titleBadge {
textConstrainedWidth -= 32.0
subtitleConstrainedWidth -= 32.0
}
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: textConstrainedWidth, height: .greatestFiniteMagnitude)))
let (statusLayout, statusApply) = makeStatusLayout(TextNodeLayoutArguments(attributedString: statusAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: subtitleConstrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (badgeLayout, badgeApply) = makeBadgeLayout(TextNodeLayoutArguments(attributedString: badgeAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: textConstrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let contentSize = CGSize(width: params.width, height: verticalInset * 2.0 + titleLayout.size.height + titleSpacing + statusLayout.size.height)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
return (layout, { [weak self] animated in
if let strongSelf = self {
strongSelf.layoutParams = (item, params, neighbors)
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
strongSelf.activateArea.accessibilityLabel = titleAttributedString.string
strongSelf.activateArea.accessibilityValue = statusAttributedString.string
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
}
let transition: ContainedViewLayoutTransition
if animated {
transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
} else {
transition = .immediate
}
let iconUpdated = currentItem?.icon != item.icon
let iconSize = CGSize(width: 40.0, height: 40.0)
if let icon = item.icon {
let iconFrame = CGRect(origin: CGPoint(x: leftInset - 3.0 + editingOffset, y: floorToScreenPixels((layout.contentSize.height - iconSize.height) / 2.0)), size: iconSize)
switch icon {
case let .peer(peer):
if let iconNode = strongSelf.iconNode {
strongSelf.iconNode = nil
iconNode.removeFromSupernode()
}
let avatarNode: AvatarNode
if let current = strongSelf.avatarNode {
avatarNode = current
} else {
avatarNode = AvatarNode(font: avatarPlaceholderFont(size: floor(40.0 * 16.0 / 37.0)))
strongSelf.addSubnode(avatarNode)
strongSelf.avatarNode = avatarNode
}
avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: peer)
avatarNode.frame = iconFrame
case let .image(color, name):
if let avatarNode = strongSelf.avatarNode {
strongSelf.avatarNode = nil
avatarNode.removeFromSupernode()
}
let iconNode: ASImageNode
if let current = strongSelf.iconNode {
iconNode = current
} else {
iconNode = ASImageNode()
iconNode.displaysAsynchronously = false
strongSelf.addSubnode(iconNode)
strongSelf.iconNode = iconNode
}
let colors: [UIColor]
var diagonal = false
switch color {
case .blue:
colors = [UIColor(rgb: 0x2a9ef1), UIColor(rgb: 0x71d4fc)]
case .green:
colors = [UIColor(rgb: 0x54cb68), UIColor(rgb: 0xa0de7e)]
case .red:
colors = [UIColor(rgb: 0xff516a), UIColor(rgb: 0xff885e)]
case .violet:
colors = [UIColor(rgb: 0xd569ec), UIColor(rgb: 0xe0a2f3)]
case .premium:
colors = [
UIColor(rgb: 0x6b93ff),
UIColor(rgb: 0x6b93ff),
UIColor(rgb: 0x8d77ff),
UIColor(rgb: 0xb56eec),
UIColor(rgb: 0xb56eec)
]
diagonal = true
case .stars:
colors = [UIColor(rgb: 0xdd6f12), UIColor(rgb: 0xfec80f)]
diagonal = true
}
if iconNode.image == nil || iconUpdated {
iconNode.image = generateAvatarImage(size: iconSize, icon: generateTintedImage(image: UIImage(bundleImageName: name), color: .white), iconScale: 1.0, cornerRadius: 20.0, color: .blue, customColors: colors, diagonal: diagonal)
}
iconNode.frame = iconFrame
}
} else {
if let avatarNode = strongSelf.avatarNode {
strongSelf.avatarNode = nil
avatarNode.removeFromSupernode()
}
if let iconNode = strongSelf.iconNode {
strongSelf.iconNode = nil
iconNode.removeFromSupernode()
}
}
if let selectableControlSizeAndApply = selectableControlSizeAndApply {
let selectableControlSize = CGSize(width: selectableControlSizeAndApply.0, height: layout.contentSize.height)
let selectableControlFrame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: selectableControlSize)
if strongSelf.selectableControlNode == nil {
let selectableControlNode = selectableControlSizeAndApply.1(selectableControlSize, false)
strongSelf.selectableControlNode = selectableControlNode
strongSelf.addSubnode(selectableControlNode)
selectableControlNode.frame = selectableControlFrame
transition.animatePosition(node: selectableControlNode, from: CGPoint(x: -selectableControlFrame.size.width / 2.0, y: selectableControlFrame.midY))
selectableControlNode.alpha = 0.0
transition.updateAlpha(node: selectableControlNode, alpha: 1.0)
} else if let selectableControlNode = strongSelf.selectableControlNode {
transition.updateFrame(node: selectableControlNode, frame: selectableControlFrame)
let _ = selectableControlSizeAndApply.1(selectableControlSize, true)
}
} else if let selectableControlNode = strongSelf.selectableControlNode {
var selectableControlFrame = selectableControlNode.frame
selectableControlFrame.origin.x = -selectableControlFrame.size.width
strongSelf.selectableControlNode = nil
transition.updateAlpha(node: selectableControlNode, alpha: 0.0)
transition.updateFrame(node: selectableControlNode, frame: selectableControlFrame, completion: { [weak selectableControlNode] _ in
selectableControlNode?.removeFromSupernode()
})
}
var titleOffset: CGFloat = 0.0
if let stars = item.stars {
let starsIconNode: ASImageNode
if let current = strongSelf.starsIconNode {
starsIconNode = current
} else {
starsIconNode = ASImageNode()
starsIconNode.displaysAsynchronously = false
strongSelf.addSubnode(starsIconNode)
strongSelf.starsIconNode = starsIconNode
starsIconNode.image = generateStarsIcon(amount: stars)
}
if let icon = starsIconNode.image {
starsIconNode.frame = CGRect(origin: CGPoint(x: leftInset + editingOffset + avatarInset, y: 10.0), size: icon.size)
titleOffset += icon.size.width + 3.0
}
}
let _ = titleApply()
let _ = statusApply()
let _ = labelApply()
let _ = badgeApply()
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.addSubnode(strongSelf.maskNode)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset + editingOffset + avatarInset
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: strongSelf.backgroundNode.frame.size)
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
transition.updateFrame(node: strongSelf.topStripeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)))
transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)))
let titleVerticalOriginY: CGFloat
if statusLayout.size.height > 0.0 {
titleVerticalOriginY = verticalInset
} else {
titleVerticalOriginY = floorToScreenPixels((contentSize.height - titleLayout.size.height) / 2.0)
}
let titleFrame = CGRect(origin: CGPoint(x: leftInset + editingOffset + avatarInset + titleOffset, y: titleVerticalOriginY), size: titleLayout.size)
transition.updateFrame(node: strongSelf.titleNode, frame: titleFrame)
var badgeOffset: CGFloat = 0.0
if badgeLayout.size.width > 0.0 {
let badgeFrame = CGRect(origin: CGPoint(x: leftInset + editingOffset + avatarInset + 2.0, y: strongSelf.titleNode.frame.maxY + titleSpacing), size: badgeLayout.size)
let badgeBackgroundFrame = badgeFrame.insetBy(dx: -3.0, dy: -2.0)
let badgeBackgroundNode: ASImageNode
if let current = strongSelf.badgeBackgroundNode {
badgeBackgroundNode = current
} else {
badgeBackgroundNode = ASImageNode()
badgeBackgroundNode.displaysAsynchronously = false
badgeBackgroundNode.image = generateStretchableFilledCircleImage(radius: 5.0, color: item.presentationData.theme.list.itemCheckColors.fillColor)
strongSelf.badgeBackgroundNode = badgeBackgroundNode
strongSelf.containerNode.addSubnode(badgeBackgroundNode)
strongSelf.containerNode.addSubnode(strongSelf.badgeTextNode)
}
transition.updateFrame(node: badgeBackgroundNode, frame: badgeBackgroundFrame)
transition.updateFrame(node: strongSelf.badgeTextNode, frame: badgeFrame)
badgeOffset = badgeLayout.size.width + 10.0
}
transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: leftInset + editingOffset + avatarInset + badgeOffset, y: strongSelf.titleNode.frame.maxY + titleSpacing), size: statusLayout.size))
if let label = item.label, case .boosts = label {
let backgroundNode: ASImageNode
let iconNode: ASImageNode
if let currentBackground = strongSelf.labelBackgroundNode, let currentIcon = strongSelf.labelIconNode {
backgroundNode = currentBackground
iconNode = currentIcon
} else {
backgroundNode = ASImageNode()
backgroundNode.displaysAsynchronously = false
backgroundNode.image = generateStretchableFilledCircleImage(radius: 13.0, color: item.presentationData.theme.list.itemAccentColor.withAlphaComponent(0.1))
strongSelf.containerNode.insertSubnode(backgroundNode, at: 1)
iconNode = ASImageNode()
iconNode.displaysAsynchronously = false
iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Premium/BoostChannel"), color: item.presentationData.theme.list.itemAccentColor)
strongSelf.containerNode.addSubnode(iconNode)
strongSelf.labelBackgroundNode = backgroundNode
strongSelf.labelIconNode = iconNode
}
if let icon = iconNode.image {
let labelFrame = CGRect(origin: CGPoint(x: layoutSize.width - rightInset - labelLayout.size.width - 21.0, y: floorToScreenPixels((layout.contentSize.height - labelLayout.size.height) / 2.0)), size: labelLayout.size)
let iconFrame = CGRect(origin: CGPoint(x: labelFrame.minX - icon.size.width - 2.0, y: labelFrame.minY - 1.0), size: icon.size)
let totalFrame = CGRect(x: iconFrame.minX - 7.0, y: labelFrame.minY - 4.0, width: iconFrame.width + labelFrame.width + 18.0, height: 26.0)
transition.updateFrame(node: backgroundNode, frame: totalFrame)
transition.updateFrame(node: strongSelf.labelNode, frame: labelFrame)
transition.updateFrame(node: iconNode, frame: iconFrame)
}
} else if let label = item.label, case .semitransparent = label {
let backgroundNode: ASImageNode
if let currentBackground = strongSelf.labelBackgroundNode {
backgroundNode = currentBackground
} else {
backgroundNode = ASImageNode()
backgroundNode.displaysAsynchronously = false
backgroundNode.image = generateStretchableFilledCircleImage(radius: 13.0, color: item.presentationData.theme.list.itemAccentColor.withAlphaComponent(0.1))
strongSelf.containerNode.insertSubnode(backgroundNode, at: 1)
strongSelf.labelBackgroundNode = backgroundNode
}
let labelFrame = CGRect(origin: CGPoint(x: layoutSize.width - rightInset - labelLayout.size.width - 19.0, y: floorToScreenPixels((layout.contentSize.height - labelLayout.size.height) / 2.0)), size: labelLayout.size)
let totalFrame = CGRect(x: labelFrame.minX - 7.0, y: labelFrame.minY - 5.0, width: labelFrame.width + 14.0, height: 26.0)
transition.updateFrame(node: backgroundNode, frame: totalFrame)
transition.updateFrame(node: strongSelf.labelNode, frame: labelFrame)
} else {
transition.updateFrame(node: strongSelf.labelNode, frame: CGRect(origin: CGPoint(x: layoutSize.width - rightInset - labelLayout.size.width - 18.0, y: floorToScreenPixels((layout.contentSize.height - labelLayout.size.height) / 2.0)), size: labelLayout.size))
if let labelIconNode = strongSelf.labelIconNode {
strongSelf.labelIconNode = nil
labelIconNode.removeFromSupernode()
}
if let labelBackgroundNode = strongSelf.labelBackgroundNode {
strongSelf.labelBackgroundNode = nil
labelBackgroundNode.removeFromSupernode()
}
}
if item.subtitleActive {
let statusArrowNode: ASImageNode
if let current = strongSelf.statusArrowNode {
statusArrowNode = current
} else {
statusArrowNode = ASImageNode()
statusArrowNode.displaysAsynchronously = false
statusArrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Contact List/SubtitleArrow"), color: item.presentationData.theme.list.itemAccentColor)
strongSelf.statusArrowNode = statusArrowNode
strongSelf.containerNode.addSubnode(statusArrowNode)
}
if let arrowSize = statusArrowNode.image?.size {
transition.updateFrame(node: statusArrowNode, frame: CGRect(origin: CGPoint(x: leftInset + editingOffset + avatarInset + statusLayout.size.width + 4.0, y: strongSelf.titleNode.frame.maxY + titleSpacing + 4.0), size: arrowSize))
}
} else if let statusArrowNode = strongSelf.statusArrowNode {
strongSelf.statusArrowNode = nil
statusArrowNode.removeFromSupernode()
}
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: strongSelf.backgroundNode.frame.height + UIScreenPixel + UIScreenPixel))
strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
if let badge = item.titleBadge {
let badgeSize = strongSelf.titleBadge.update(
transition: .immediate,
component: AnyComponent(
BoostIconComponent(hasIcon: true, text: badge)
),
environment: {},
containerSize: CGSize(width: params.width, height: 100.0)
)
if let view = strongSelf.titleBadge.view {
if view.superview == nil {
strongSelf.view.addSubview(view)
}
let badgeFrame = CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: floorToScreenPixels(titleFrame.midY - badgeSize.height / 2.0) - 1.0), size: badgeSize)
view.frame = badgeFrame
}
} else {
if let view = strongSelf.titleBadge.view {
view.removeFromSuperview()
}
}
}
})
}
}
override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
super.setHighlighted(highlighted, at: point, animated: animated)
if highlighted {
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
var anchorNode: ASDisplayNode?
if self.bottomStripeNode.supernode != nil {
anchorNode = self.bottomStripeNode
} else if self.topStripeNode.supernode != nil {
anchorNode = self.topStripeNode
} else if self.backgroundNode.supernode != nil {
anchorNode = self.backgroundNode
}
if let anchorNode = anchorNode {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
} else {
self.addSubnode(self.highlightedBackgroundNode)
}
}
} else {
if self.highlightedBackgroundNode.supernode != nil {
if animated {
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
if let strongSelf = self {
if completed {
strongSelf.highlightedBackgroundNode.removeFromSupernode()
}
}
})
self.highlightedBackgroundNode.alpha = 0.0
} else {
self.highlightedBackgroundNode.removeFromSupernode()
}
}
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}
private func generateStarsIcon(amount: Int64) -> UIImage {
let stars: [Int64: Int] = [
15: 1,
75: 2,
250: 3,
500: 4,
1000: 5,
2500: 6,
25: 1,
50: 1,
100: 2,
150: 2,
350: 3,
750: 4,
1500: 5,
5000: 6,
10000: 6,
25000: 7,
35000: 7
]
let count = stars[amount] ?? 1
let image = generateGradientTintedImage(
image: UIImage(bundleImageName: "Peer Info/PremiumIcon"),
colors: [
UIColor(rgb: 0xfed219),
UIColor(rgb: 0xf3a103),
UIColor(rgb: 0xe78104)
],
direction: .diagonal
)!
let imageSize = CGSize(width: 20.0, height: 20.0)
let partImage = generateImage(imageSize, contextGenerator: { size, context in
context.clear(CGRect(origin: .zero, size: size))
if let cgImage = image.cgImage {
context.draw(cgImage, in: CGRect(origin: .zero, size: size), byTiling: false)
context.saveGState()
context.clip(to: CGRect(origin: .zero, size: size).insetBy(dx: -1.0, dy: -1.0).offsetBy(dx: -2.0, dy: 0.0), mask: cgImage)
context.setBlendMode(.clear)
context.setFillColor(UIColor.clear.cgColor)
context.fill(CGRect(origin: .zero, size: size))
context.restoreGState()
context.setBlendMode(.clear)
context.setFillColor(UIColor.clear.cgColor)
context.fill(CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width / 2.0, height: size.height - 4.0)))
}
})!
let spacing: CGFloat = (3.0 - UIScreenPixel)
let totalWidth = 20.0 + spacing * CGFloat(count - 1)
return generateImage(CGSize(width: ceil(totalWidth), height: 20.0), contextGenerator: { size, context in
context.clear(CGRect(origin: .zero, size: size))
var originX = floorToScreenPixels((size.width - totalWidth) / 2.0)
let mainImage = UIImage(bundleImageName: "Premium/Stars/StarLarge")
if let cgImage = mainImage?.cgImage, let partCGImage = partImage.cgImage {
context.draw(cgImage, in: CGRect(origin: CGPoint(x: originX, y: 0.0), size: imageSize).insetBy(dx: -1.5, dy: -1.5), byTiling: false)
originX += spacing + UIScreenPixel
for _ in 0 ..< count - 1 {
context.draw(partCGImage, in: CGRect(origin: CGPoint(x: originX, y: -UIScreenPixel), size: imageSize).insetBy(dx: -1.0 + UIScreenPixel, dy: -1.0 + UIScreenPixel), byTiling: false)
originX += spacing
}
}
})!
}
@@ -0,0 +1,599 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import TelegramCore
import AccountContext
import TelegramStringFormatting
import TelegramPresentationData
import Markdown
import AlertUI
public func presentGiveawayInfoController(
context: AccountContext,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil,
messageId: EngineMessage.Id,
giveawayInfo: PremiumGiveawayInfo,
present: @escaping (ViewController) -> Void,
openLink: @escaping (String) -> Void
) {
var peerIds: [EnginePeer.Id] = [context.account.peerId]
if case let .ongoing(_, status) = giveawayInfo, case let .notAllowed(reason) = status, case let .channelAdmin(adminId) = reason {
peerIds.append(adminId)
}
let _ = (context.engine.data.get(
TelegramEngine.EngineData.Item.Messages.Message(id: messageId),
EngineDataMap(peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init))
)
|> deliverOnMainQueue).startStandalone(next: { message, peerMap in
guard let message else {
return
}
let giveaway = message.media.first(where: { $0 is TelegramMediaGiveaway }) as? TelegramMediaGiveaway
let giveawayResults = message.media.first(where: { $0 is TelegramMediaGiveawayResults }) as? TelegramMediaGiveawayResults
var quantity: Int32 = 0
if let giveaway {
quantity = giveaway.quantity
} else if let giveawayResults {
quantity = giveawayResults.winnersCount + giveawayResults.unclaimedCount
}
var months: Int32 = 0
var stars: Int64 = 0
if let giveaway {
switch giveaway.prize {
case let .premium(monthsValue):
months = monthsValue
case let .stars(amount):
stars = amount
}
} else if let giveawayResults {
switch giveawayResults.prize {
case let .premium(monthsValue):
months = monthsValue
case let .stars(amount):
stars = amount
}
}
var prizeDescription: String?
if let giveaway {
prizeDescription = giveaway.prizeDescription
} else if let giveawayResults {
prizeDescription = giveawayResults.prizeDescription
}
var untilDateValue: Int32 = 0
if let giveaway {
untilDateValue = giveaway.untilDate
} else if let giveawayResults {
untilDateValue = giveawayResults.untilDate
}
var onlyNewSubscribers = false
if let giveaway, giveaway.flags.contains(.onlyNewSubscribers) {
onlyNewSubscribers = true
} else if let giveawayResults, giveawayResults.flags.contains(.onlyNewSubscribers) {
onlyNewSubscribers = true
}
var author = message.forwardInfo?.author ?? message.author?._asPeer()
if author is TelegramChannel {
} else {
if let peer = message.forwardInfo?.source ?? message.peers[message.id.peerId] {
author = peer
}
}
var isGroup = false
if let channel = author as? TelegramChannel, case .group = channel.info {
isGroup = true
}
var peerName = ""
if let author {
peerName = EnginePeer(author).compactDisplayTitle
}
var groupsAndChannels = false
var channelsCount: Int32 = 1
if let giveaway {
channelsCount = Int32(giveaway.channelPeerIds.count)
var channelCount = 0
var groupCount = 0
for peerId in giveaway.channelPeerIds {
if let peer = message.peers[peerId] as? TelegramChannel {
switch peer.info {
case .broadcast:
channelCount += 1
case .group:
groupCount += 1
}
}
}
if groupCount > 0 && channelCount > 0 {
groupsAndChannels = true
}
} else if let giveawayResults {
channelsCount = 1 + giveawayResults.additionalChannelsCount
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let timeZone = TimeZone.current
let untilDate = stringForDate(timestamp: untilDateValue, timeZone: timeZone, strings: presentationData.strings)
let title: String
let text: String
var warning: String?
var dismissImpl: (() -> Void)?
var actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {
dismissImpl?()
})]
var additionalPrizes = ""
if let prizeDescription, !prizeDescription.isEmpty {
additionalPrizes = "\n\n" + presentationData.strings.Chat_Giveaway_Info_AdditionalPrizes(peerName, "\(quantity) \(prizeDescription)").string
}
switch giveawayInfo {
case let .ongoing(start, status):
let startDate = presentationData.strings.Chat_Giveaway_Info_FullDate(
stringForMessageTimestamp(timestamp: start, dateTimeFormat: presentationData.dateTimeFormat),
stringForDate(timestamp: start, timeZone: timeZone, strings: presentationData.strings)
).string.trimmingCharacters(in: CharacterSet(charactersIn: "*"))
title = presentationData.strings.Chat_Giveaway_Info_Title
let intro: String
if stars > 0 {
let starsString = presentationData.strings.Chat_Giveaway_Info_Stars_Stars(Int32(clamping: stars))
if case .almostOver = status {
if isGroup {
intro = presentationData.strings.Chat_Giveaway_Info_Stars_Group_EndedIntro(peerName, starsString).string
} else {
intro = presentationData.strings.Chat_Giveaway_Info_Stars_EndedIntro(peerName, starsString).string
}
} else {
if isGroup {
intro = presentationData.strings.Chat_Giveaway_Info_Stars_Group_OngoingIntro(peerName, starsString).string
} else {
intro = presentationData.strings.Chat_Giveaway_Info_Stars_OngoingIntro(peerName, starsString).string
}
}
} else {
let subscriptionsString = presentationData.strings.Chat_Giveaway_Info_Subscriptions(quantity)
let monthsString = presentationData.strings.Chat_Giveaway_Info_Months(months)
if case .almostOver = status {
if isGroup {
intro = presentationData.strings.Chat_Giveaway_Info_Group_EndedIntro(peerName, subscriptionsString, monthsString).string
} else {
intro = presentationData.strings.Chat_Giveaway_Info_EndedIntro(peerName, subscriptionsString, monthsString).string
}
} else {
if isGroup {
intro = presentationData.strings.Chat_Giveaway_Info_Group_OngoingIntro(peerName, subscriptionsString, monthsString).string
} else {
intro = presentationData.strings.Chat_Giveaway_Info_OngoingIntro(peerName, subscriptionsString, monthsString).string
}
}
}
var otherText: String = ""
if channelsCount > 1 {
if isGroup {
if groupsAndChannels {
if channelsCount == 2 {
otherText = presentationData.strings.Chat_Giveaway_Info_OtherChannels(Int32(channelsCount - 1))
} else {
otherText = presentationData.strings.Chat_Giveaway_Info_OtherGroupsAndChannels(Int32(channelsCount - 1))
}
} else {
otherText = presentationData.strings.Chat_Giveaway_Info_OtherGroups(Int32(channelsCount - 1))
}
} else {
if groupsAndChannels {
if channelsCount == 2 {
otherText = presentationData.strings.Chat_Giveaway_Info_OtherGroups(Int32(channelsCount - 1))
} else {
otherText = presentationData.strings.Chat_Giveaway_Info_OtherChannelsAndGroups(Int32(channelsCount - 1))
}
} else {
otherText = presentationData.strings.Chat_Giveaway_Info_OtherChannels(Int32(channelsCount - 1))
}
}
}
let ending: String
if onlyNewSubscribers {
let randomUsers = presentationData.strings.Chat_Giveaway_Info_RandomUsers(quantity)
if channelsCount > 1 {
ending = presentationData.strings.Chat_Giveaway_Info_OngoingNewMany(untilDate, randomUsers, peerName, otherText, startDate).string
} else {
ending = presentationData.strings.Chat_Giveaway_Info_OngoingNew(untilDate, randomUsers, peerName, startDate).string
}
} else {
let randomSubscribers = isGroup ? presentationData.strings.Chat_Giveaway_Info_Group_RandomMembers(quantity) : presentationData.strings.Chat_Giveaway_Info_RandomSubscribers(quantity)
if channelsCount > 1 {
ending = presentationData.strings.Chat_Giveaway_Info_OngoingMany(untilDate, randomSubscribers, peerName, otherText).string
} else {
ending = presentationData.strings.Chat_Giveaway_Info_Ongoing(untilDate, randomSubscribers, peerName).string
}
}
var participation: String
switch status {
case .notQualified:
if channelsCount > 1 {
participation = presentationData.strings.Chat_Giveaway_Info_NotQualifiedMany(peerName, otherText, untilDate).string
} else {
participation = presentationData.strings.Chat_Giveaway_Info_NotQualified(peerName, untilDate).string
}
case let .notAllowed(reason):
switch reason {
case let .joinedTooEarly(joinedOn):
let joinDate = stringForDate(timestamp: joinedOn, strings: presentationData.strings)
participation = presentationData.strings.Chat_Giveaway_Info_NotAllowedJoinedEarly(joinDate).string
case let .channelAdmin(adminId):
var channelName = peerName
var isGroup = false
if let maybePeer = peerMap[adminId], let peer = maybePeer {
channelName = peer.compactDisplayTitle
if case let .channel(channel) = peer, case .group = channel.info {
isGroup = true
}
}
participation = isGroup ? presentationData.strings.Chat_Giveaway_Info_NotAllowedAdminGroup(channelName).string : presentationData.strings.Chat_Giveaway_Info_NotAllowedAdmin(channelName).string
case .disallowedCountry:
participation = presentationData.strings.Chat_Giveaway_Info_NotAllowedCountry
}
case .participating:
if channelsCount > 1 {
participation = presentationData.strings.Chat_Giveaway_Info_ParticipatingMany(peerName, otherText).string
} else {
participation = presentationData.strings.Chat_Giveaway_Info_Participating(peerName).string
}
case .almostOver:
participation = presentationData.strings.Chat_Giveaway_Info_AlmostOver
}
if !participation.isEmpty {
participation = "\n\n\(participation)"
}
text = "\(intro)\(additionalPrizes)\n\n\(ending)\(participation)"
case let .finished(status, start, finish, _, activatedCount):
let startDate = presentationData.strings.Chat_Giveaway_Info_FullDate(
stringForMessageTimestamp(timestamp: start, dateTimeFormat: presentationData.dateTimeFormat),
stringForDate(timestamp: start, timeZone: timeZone, strings: presentationData.strings)
).string.trimmingCharacters(in: CharacterSet(charactersIn: "*"))
let finishDate = stringForDate(timestamp: finish, timeZone: timeZone, strings: presentationData.strings)
title = presentationData.strings.Chat_Giveaway_Info_EndedTitle
let intro: String
if stars > 0 {
let starsString = presentationData.strings.Chat_Giveaway_Info_Stars_Stars(Int32(clamping: stars))
if isGroup {
intro = presentationData.strings.Chat_Giveaway_Info_Stars_Group_EndedIntro(peerName, starsString).string
} else {
intro = presentationData.strings.Chat_Giveaway_Info_Stars_EndedIntro(peerName, starsString).string
}
} else {
let subscriptionsString = presentationData.strings.Chat_Giveaway_Info_Subscriptions(quantity)
let monthsString = presentationData.strings.Chat_Giveaway_Info_Months(months)
if isGroup {
intro = presentationData.strings.Chat_Giveaway_Info_Group_EndedIntro(peerName, subscriptionsString, monthsString).string
} else {
intro = presentationData.strings.Chat_Giveaway_Info_EndedIntro(peerName, subscriptionsString, monthsString).string
}
}
var ending: String
if onlyNewSubscribers {
let randomUsers = presentationData.strings.Chat_Giveaway_Info_RandomUsers(quantity)
if channelsCount > 1 {
ending = presentationData.strings.Chat_Giveaway_Info_EndedNewMany(finishDate, randomUsers, peerName, startDate).string
} else {
ending = presentationData.strings.Chat_Giveaway_Info_EndedNew(finishDate, randomUsers, peerName, startDate).string
}
} else {
let randomSubscribers = isGroup ? presentationData.strings.Chat_Giveaway_Info_Group_RandomMembers(quantity) : presentationData.strings.Chat_Giveaway_Info_RandomSubscribers(quantity)
if channelsCount > 1 {
ending = presentationData.strings.Chat_Giveaway_Info_EndedMany(finishDate, randomSubscribers, peerName).string
} else {
ending = presentationData.strings.Chat_Giveaway_Info_Ended(finishDate, randomSubscribers, peerName).string
}
}
if let activatedCount, activatedCount > 0 {
ending += " " + presentationData.strings.Chat_Giveaway_Info_ActivatedLinks(activatedCount)
}
var result: String
switch status {
case .refunded:
result = ""
warning = presentationData.strings.Chat_Giveaway_Info_Refunded
actions = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Close, action: {
dismissImpl?()
})]
case .notWon:
result = "**\(presentationData.strings.Chat_Giveaway_Info_DidntWin)**\n\n"
case let .wonPremium(slug):
result = "**\(presentationData.strings.Chat_Giveaway_Info_Won("").string)**\n\n"
actions = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Chat_Giveaway_Info_ViewPrize, action: {
dismissImpl?()
openLink(slug)
}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
dismissImpl?()
})]
case let .wonStars(stars):
let _ = stars
result = "**\(presentationData.strings.Chat_Giveaway_Info_Won("").string)**\n\n"
actions = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Chat_Giveaway_Info_ViewPrize, action: {
dismissImpl?()
openLink("")
}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
dismissImpl?()
})]
}
text = "\(result)\(intro)\(additionalPrizes)\n\n\(ending)"
}
let alertController = giveawayInfoAlertController(
context: context,
updatedPresentationData: updatedPresentationData,
title: title,
text: text,
warning: warning,
actions: actions
)
dismissImpl = { [weak alertController] in
alertController?.dismissAnimated()
}
present(alertController)
})
}
private final class GiveawayInfoAlertContentNode: AlertContentNode {
private let title: String
private let text: String
private let warning: String?
private let titleNode: ASTextNode
private let textNode: ASTextNode
fileprivate let warningBackgroundNode: ASImageNode
fileprivate let warningTextNode: ImmediateTextNode
private let actionNodesSeparator: ASDisplayNode
private let actionNodes: [TextAlertContentActionNode]
private let actionVerticalSeparators: [ASDisplayNode]
private var validLayout: CGSize?
public var theme: PresentationTheme
public override var dismissOnOutsideTap: Bool {
return self.isUserInteractionEnabled
}
public init(theme: AlertControllerTheme, ptheme: PresentationTheme, title: String, text: String, warning: String?, actions: [TextAlertAction]) {
self.theme = ptheme
self.title = title
self.text = text
self.warning = warning
self.titleNode = ASTextNode()
self.titleNode.maximumNumberOfLines = 0
self.textNode = ASTextNode()
self.textNode.maximumNumberOfLines = 0
self.warningBackgroundNode = ASImageNode()
self.warningBackgroundNode.displaysAsynchronously = false
self.warningTextNode = ImmediateTextNode()
self.warningTextNode.maximumNumberOfLines = 0
self.warningTextNode.lineSpacing = 0.1
self.actionNodesSeparator = ASDisplayNode()
self.actionNodesSeparator.isLayerBacked = true
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
return TextAlertContentActionNode(theme: theme, action: action)
}
var actionVerticalSeparators: [ASDisplayNode] = []
if actions.count > 1 {
for _ in 0 ..< actions.count - 1 {
let separatorNode = ASDisplayNode()
separatorNode.isLayerBacked = true
actionVerticalSeparators.append(separatorNode)
}
}
self.actionVerticalSeparators = actionVerticalSeparators
super.init()
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.warningBackgroundNode)
self.addSubnode(self.warningTextNode)
self.addSubnode(self.actionNodesSeparator)
for actionNode in self.actionNodes {
self.addSubnode(actionNode)
}
for separatorNode in self.actionVerticalSeparators {
self.addSubnode(separatorNode)
}
self.updateTheme(theme)
}
public override func updateTheme(_ theme: AlertControllerTheme) {
self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center)
let body = MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor)
let bold = MarkdownAttributeSet(font: Font.semibold(13.0), textColor: theme.primaryColor)
let attributedText = parseMarkdownIntoAttributedString(self.text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .center)
self.textNode.attributedText = attributedText
self.warningTextNode.attributedText = NSAttributedString(string: self.warning ?? "", font: Font.semibold(13.0), textColor: theme.destructiveColor, paragraphAlignment: .center)
self.warningBackgroundNode.image = generateStretchableFilledCircleImage(radius: 5.0, color: theme.destructiveColor.withAlphaComponent(0.1))
self.actionNodesSeparator.backgroundColor = theme.separatorColor
for actionNode in self.actionNodes {
actionNode.updateTheme(theme)
}
for separatorNode in self.actionVerticalSeparators {
separatorNode.backgroundColor = theme.separatorColor
}
if let size = self.validLayout {
_ = self.updateLayout(size: size, transition: .immediate)
}
}
public override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
var size = size
size.width = min(size.width, 270.0)
let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude)
self.validLayout = size
var origin: CGPoint = CGPoint(x: 0.0, y: 20.0)
let titleSize = self.titleNode.measure(measureSize)
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize))
origin.y += titleSize.height + 4.0
let textSize = self.textNode.measure(measureSize)
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize))
origin.y += textSize.height + 6.0
let actionButtonHeight: CGFloat = 44.0
var minActionsWidth: CGFloat = 0.0
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
let actionTitleInsets: CGFloat = 8.0
var effectiveActionLayout = TextAlertContentActionLayout.horizontal
for actionNode in self.actionNodes {
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
effectiveActionLayout = .vertical
}
switch effectiveActionLayout {
case .horizontal:
minActionsWidth += actionTitleSize.width + actionTitleInsets
case .vertical:
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
}
}
if "".isEmpty {
effectiveActionLayout = .vertical
}
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0)
var contentWidth = max(titleSize.width, minActionsWidth)
contentWidth = max(contentWidth, 234.0)
var actionsHeight: CGFloat = 0.0
switch effectiveActionLayout {
case .horizontal:
actionsHeight = actionButtonHeight
case .vertical:
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
}
let resultWidth = contentWidth + insets.left + insets.right
var warningHeight: CGFloat = 0.0
if let _ = self.warning {
let warningSize = self.warningTextNode.updateLayout(measureSize)
let warningFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - warningSize.width) / 2.0), y: origin.y + 20.0), size: warningSize)
transition.updateFrame(node: self.warningTextNode, frame: warningFrame)
transition.updateFrame(node: self.warningBackgroundNode, frame: warningFrame.insetBy(dx: -8.0, dy: -8.0))
warningHeight += warningSize.height + 26.0
}
let resultSize = CGSize(width: resultWidth, height: titleSize.height + textSize.height + 8.0 + actionsHeight + warningHeight + insets.top + insets.bottom)
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
var actionOffset: CGFloat = 0.0
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
var separatorIndex = -1
var nodeIndex = 0
for actionNode in self.actionNodes {
if separatorIndex >= 0 {
let separatorNode = self.actionVerticalSeparators[separatorIndex]
switch effectiveActionLayout {
case .horizontal:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
case .vertical:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
}
}
separatorIndex += 1
let currentActionWidth: CGFloat
switch effectiveActionLayout {
case .horizontal:
if nodeIndex == self.actionNodes.count - 1 {
currentActionWidth = resultSize.width - actionOffset
} else {
currentActionWidth = actionWidth
}
case .vertical:
currentActionWidth = resultSize.width
}
let actionNodeFrame: CGRect
switch effectiveActionLayout {
case .horizontal:
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += currentActionWidth
case .vertical:
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += actionButtonHeight
}
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
nodeIndex += 1
}
return resultSize
}
}
private func giveawayInfoAlertController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, title: String, text: String, warning: String?, actions: [TextAlertAction]) -> AlertController {
let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
let contentNode = GiveawayInfoAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, title: title, text: text, warning: warning, actions: actions)
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode)
let presentationDataDisposable = (updatedPresentationData?.signal ?? context.sharedContext.presentationData).start(next: { [weak controller] presentationData in
controller?.theme = AlertControllerTheme(presentationData: presentationData)
})
controller.dismissed = { _ in
presentationDataDisposable.dispose()
}
return controller
}
@@ -0,0 +1,185 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
private let phrases = [
"Вітаю",
"你好",
"Hello",
"سلام",
"Bonjour",
"Guten tag",
"שלום",
"नमस्ते",
"Ciao",
"こんにちは",
"Hei",
"Olá",
"Привет",
"Zdravo",
"Hola",
"Привіт",
"Salom",
"Halo"
]
private var simultaneousDisplayCount = 13
private let referenceWidth: CGFloat = 1180
private let positions: [CGPoint] = [
CGPoint(x: 315.0, y: 83.0),
CGPoint(x: 676.0, y: 18.0),
CGPoint(x: 880.0, y: 130.0),
CGPoint(x: 90.0, y: 214.0),
CGPoint(x: 550.0, y: 150.0),
CGPoint(x: 1130.0, y: 220.0),
CGPoint(x: 220.0, y: 440.0),
CGPoint(x: 1080.0, y: 350.0),
CGPoint(x: 85.0, y: 630.0),
CGPoint(x: 1180.0, y: 550.0),
CGPoint(x: 150.0, y: 810.0),
CGPoint(x: 1010.0, y: 770.0),
CGPoint(x: 40.0, y: 1000.0),
CGPoint(x: 1130.0, y: 1000.0)
]
final class HelloView: UIView, PhoneDemoDecorationView {
private var activePhrases = Set<Int>()
private var activePositions = Set<Int>()
private var containerView: UIView
override init(frame: CGRect) {
self.containerView = UIView()
super.init(frame: frame)
self.addSubview(self.containerView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private var didSetup = false
func setupAnimations() {
guard self.activePhrases.isEmpty, self.visible else {
return
}
self.didSetup = true
var ids: [Int] = []
for i in 0 ..< phrases.count {
ids.append(i)
}
ids.shuffle()
let phraseIds = Array(self.availablePhraseIds()).shuffled()
let positionIds = Array(self.availablePositionIds()).shuffled()
for i in 0 ..< simultaneousDisplayCount {
let delay: Double = Double.random(in: 0.0 ..< 0.8)
Queue.mainQueue().after(delay) {
self.spawnPhrase(phraseIds[i], positionIndex: positionIds[i])
}
}
}
func availablePhraseIds() -> Set<Int> {
var ids = Set<Int>()
for i in 0 ..< phrases.count {
ids.insert(i)
}
for id in self.activePhrases {
ids.remove(id)
}
return ids
}
func availablePositionIds() -> Set<Int> {
var ids = Set<Int>()
for i in 0 ..< positions.count {
ids.insert(i)
}
for id in self.activePositions {
ids.remove(id)
}
return ids
}
func spawnNextPhrase() {
let phraseIds = Array(self.availablePhraseIds()).shuffled()
let positionIds = Array(self.availablePositionIds()).shuffled()
if let phrase = phraseIds.first, let position = positionIds.first {
self.spawnPhrase(phrase, positionIndex: position)
}
}
func spawnPhrase(_ index: Int, positionIndex: Int) {
let view = UILabel()
view.alpha = 0.0
view.text = phrases[index]
view.font = Font.with(size: 24.0, design: .round, weight: .semibold, traits: [])
view.textColor = UIColor(rgb: 0xffffff, alpha: CGFloat.random(in: 0.4 ... 0.6))
view.layer.compositingFilter = "softLightBlendMode"
view.sizeToFit()
view.center = self.positionForIndex(positionIndex)
self.activePhrases.insert(index)
self.activePositions.insert(positionIndex)
let duration: Double = Double.random(in: 1.75...2.25)
view.layer.animateKeyframes(values: [0.0, 1.0, 0.0] as [NSNumber], duration: duration, keyPath: "opacity", removeOnCompletion: false, completion: { [weak view, weak self] _ in
if let self {
self.activePhrases.remove(index)
self.activePositions.remove(positionIndex)
view?.removeFromSuperview()
self.spawnNextPhrase()
}
})
view.layer.animateScale(from: CGFloat.random(in: 0.4 ..< 0.6), to: CGFloat.random(in: 0.9 ..< 1.2), duration: duration, removeOnCompletion: false)
self.containerView.addSubview(view)
}
func positionForIndex(_ index: Int) -> CGPoint {
var position = positions[index]
let spread: CGPoint = CGPoint(x: 30.0, y: 5.0)
position.x = (self.frame.width - self.frame.height) / 2.0 + position.x / referenceWidth * self.frame.height + CGFloat.random(in: -spread.x ... spread.x)
position.y = position.y / referenceWidth * self.frame.height + CGFloat.random(in: -spread.y ... spread.y)
return position
}
private var visible = false
func setVisible(_ visible: Bool) {
guard self.visible != visible else {
return
}
self.visible = visible
if visible {
self.setupAnimations()
} else {
self.didSetup = false
}
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear)
transition.updateAlpha(layer: self.containerView.layer, alpha: visible ? 1.0 : 0.0, completion: { [weak self] finished in
if let strongSelf = self, finished && !visible && !strongSelf.visible {
for view in strongSelf.containerView.subviews {
view.removeFromSuperview()
}
}
})
}
func resetAnimation() {
}
override func layoutSubviews() {
super.layoutSubviews()
self.containerView.frame = CGRect(origin: .zero, size: self.frame.size)
}
}
@@ -0,0 +1,151 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import SolidRoundedButtonNode
import AppBundle
public final class IncreaseLimitFooterItem: ItemListControllerFooterItem {
let theme: PresentationTheme
let title: String
let colorful: Bool
let action: () -> Void
public init(theme: PresentationTheme, title: String, colorful: Bool, action: @escaping () -> Void) {
self.theme = theme
self.title = title
self.colorful = colorful
self.action = action
}
public func isEqual(to: ItemListControllerFooterItem) -> Bool {
if let item = to as? IncreaseLimitFooterItem {
return self.theme === item.theme && self.title == item.title && self.colorful == item.colorful
} else {
return false
}
}
public func node(current: ItemListControllerFooterItemNode?) -> ItemListControllerFooterItemNode {
if let current = current as? IncreaseLimitFooterItemNode {
current.item = self
return current
} else {
return IncreaseLimitFooterItemNode(item: self)
}
}
}
final class IncreaseLimitFooterItemNode: ItemListControllerFooterItemNode {
private let backgroundNode: NavigationBackgroundNode
private let separatorNode: ASDisplayNode
private let buttonNode: SolidRoundedButtonNode
private var validLayout: ContainerViewLayout?
var item: IncreaseLimitFooterItem {
didSet {
self.updateItem()
if let layout = self.validLayout {
let _ = self.updateLayout(layout: layout, transition: .immediate)
}
}
}
init(item: IncreaseLimitFooterItem) {
self.item = item
self.backgroundNode = NavigationBackgroundNode(color: item.theme.rootController.tabBar.backgroundColor)
self.separatorNode = ASDisplayNode()
self.buttonNode = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(backgroundColor: .black, foregroundColor: .white), height: 50.0, cornerRadius: 11.0)
self.buttonNode.iconPosition = .right
self.buttonNode.icon = UIImage(bundleImageName: "Premium/X2")
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.separatorNode)
self.addSubnode(self.buttonNode)
self.updateItem()
}
private func updateItem() {
self.backgroundNode.updateColor(color: self.item.theme.rootController.tabBar.backgroundColor, transition: .immediate)
self.separatorNode.backgroundColor = self.item.theme.rootController.tabBar.separatorColor
let textColor: UIColor
let backgroundColor = self.item.theme.list.itemCheckColors.fillColor
let backgroundColors: [UIColor]
let icon: UIImage?
if self.item.colorful {
textColor = .white
backgroundColors = [
UIColor(rgb: 0x0077ff),
UIColor(rgb: 0x6b93ff),
UIColor(rgb: 0x8878ff),
UIColor(rgb: 0xe46ace)
]
icon = UIImage(bundleImageName: "Premium/X2")
} else {
textColor = self.item.theme.list.itemCheckColors.foregroundColor
backgroundColors = []
icon = nil
}
self.buttonNode.updateTheme(SolidRoundedButtonTheme(backgroundColor: backgroundColor, backgroundColors: backgroundColors, foregroundColor: textColor), animated: true)
self.buttonNode.title = self.item.title
self.buttonNode.icon = icon
self.buttonNode.pressed = { [weak self] in
self?.item.action()
}
}
override func updateBackgroundAlpha(_ alpha: CGFloat, transition: ContainedViewLayoutTransition) {
transition.updateAlpha(node: self.backgroundNode, alpha: alpha)
transition.updateAlpha(node: self.separatorNode, alpha: alpha)
}
override func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) -> CGFloat {
self.validLayout = layout
let buttonInset: CGFloat = 16.0
let buttonWidth = layout.size.width - layout.safeInsets.left - layout.safeInsets.right - buttonInset * 2.0
let buttonHeight = self.buttonNode.updateLayout(width: buttonWidth, transition: transition)
let inset: CGFloat = 9.0
let insets = layout.insets(options: [.input])
var panelHeight: CGFloat = buttonHeight + inset * 2.0
let totalPanelHeight: CGFloat
if let inputHeight = layout.inputHeight, inputHeight > 0.0 {
totalPanelHeight = panelHeight + insets.bottom
} else {
panelHeight += insets.bottom
totalPanelHeight = panelHeight
}
let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - totalPanelHeight), size: CGSize(width: layout.size.width, height: panelHeight))
transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + buttonInset, y: panelFrame.minY + inset), size: CGSize(width: buttonWidth, height: buttonHeight)))
transition.updateFrame(node: self.backgroundNode, frame: panelFrame)
self.backgroundNode.update(size: panelFrame.size, transition: transition)
transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: panelFrame.origin, size: CGSize(width: panelFrame.width, height: UIScreenPixel)))
return panelHeight
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
if self.backgroundNode.frame.contains(point) {
return true
} else {
return false
}
}
}
@@ -0,0 +1,233 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import Markdown
import ComponentFlow
public class IncreaseLimitHeaderItem: ListViewItem, ItemListItem {
public enum Icon {
case group
case link
}
let theme: PresentationTheme
let strings: PresentationStrings
let icon: Icon
let count: Int32
let limit: Int32
let premiumCount: Int32
let text: String
let isPremiumDisabled: Bool
public let sectionId: ItemListSectionId
public init(theme: PresentationTheme, strings: PresentationStrings, icon: Icon, count: Int32, limit: Int32, premiumCount: Int32, text: String, isPremiumDisabled: Bool, sectionId: ItemListSectionId) {
self.theme = theme
self.strings = strings
self.icon = icon
self.count = count
self.limit = limit
self.premiumCount = premiumCount
self.text = text
self.isPremiumDisabled = isPremiumDisabled
self.sectionId = sectionId
}
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = IncreaseLimitHeaderItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
guard let nodeValue = node() as? IncreaseLimitHeaderItemNode else {
assertionFailure()
return
}
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
private let titleFont = Font.semibold(17.0)
private let textFont = Font.regular(15.0)
private let boldTextFont = Font.semibold(15.0)
class IncreaseLimitHeaderItemNode: ListViewItemNode {
private var hostView: ComponentHostView<Empty>?
private var params: (AnyComponent<Empty>, CGSize, ListViewItemNodeLayout, CGSize)?
private let titleNode: TextNode
private let textNode: TextNode
private var item: IncreaseLimitHeaderItem?
init() {
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
self.textNode = TextNode()
self.textNode.isUserInteractionEnabled = false
self.textNode.contentMode = .left
self.textNode.contentsScale = UIScreen.main.scale
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
}
override func didLoad() {
super.didLoad()
let hostView = ComponentHostView<Empty>()
self.hostView = hostView
self.view.addSubview(hostView)
if let (component, containerSize, layout, textSize) = self.params {
var size = hostView.update(
transition: .immediate,
component: component,
environment: {},
containerSize: containerSize
)
hostView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - size.width) / 2.0), y: -30.0), size: size)
if let item = self.item, item.isPremiumDisabled {
size.height -= 54.0
}
let textSpacing: CGFloat = -6.0
self.textNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - textSize.width) / 2.0), y: size.height + textSpacing), size: textSize)
}
}
func asyncLayout() -> (_ item: IncreaseLimitHeaderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTextLayout = TextNode.asyncLayout(self.textNode)
return { item, params, neighbors in
let topInset: CGFloat = 2.0
let badgeHeight: CGFloat = 200.0
let textSpacing: CGFloat = -6.0
let bottomInset: CGFloat = -86.0
let textColor = item.theme.list.freeTextColor
let attributedText = parseMarkdownIntoAttributedString(item.text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: titleFont, textColor: textColor), linkAttribute: { _ in
return nil
}))
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.1, cutout: nil, insets: UIEdgeInsets()))
var contentSize = CGSize(width: params.width, height: topInset + badgeHeight + textSpacing + textLayout.size.height + bottomInset)
if item.isPremiumDisabled {
contentSize.height -= 54.0
}
let insets = itemListNeighborsGroupedInsets(neighbors, params)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.accessibilityLabel = attributedText.string
let badgeIconName: String
switch item.icon {
case .group:
badgeIconName = "Premium/Group"
case .link:
badgeIconName = "Premium/Link"
}
let gradientColors: [UIColor]
if item.isPremiumDisabled {
gradientColors = [
UIColor(rgb: 0x007afe),
UIColor(rgb: 0x5494ff)
]
} else {
gradientColors = [
UIColor(rgb: 0x0077ff),
UIColor(rgb: 0x6b93ff),
UIColor(rgb: 0x8878ff),
UIColor(rgb: 0xe46ace)
]
}
let component = AnyComponent(PremiumLimitDisplayComponent(
inactiveColor: item.theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.5),
activeColors: gradientColors,
inactiveTitle: item.strings.Premium_Free,
inactiveValue: item.count > item.limit ? "\(item.limit)" : "",
inactiveTitleColor: item.theme.list.itemPrimaryTextColor,
activeTitle: item.strings.Premium_Premium,
activeValue: item.count >= item.premiumCount ? "" : "\(item.premiumCount)",
activeTitleColor: .white,
badgeIconName: badgeIconName,
badgeText: "\(item.count)",
badgePosition: CGFloat(item.count) / CGFloat(item.premiumCount),
badgeGraphPosition: CGFloat(item.limit) / CGFloat(item.premiumCount),
isPremiumDisabled: item.isPremiumDisabled
))
let containerSize = CGSize(width: layout.size.width - params.leftInset - params.rightInset, height: 200.0)
let _ = textApply()
if let hostView = strongSelf.hostView {
var size = hostView.update(
transition: .immediate,
component: component,
environment: {},
containerSize: containerSize
)
if item.isPremiumDisabled {
size.height -= 54.0
}
hostView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - size.width) / 2.0), y: -30.0), size: size)
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - textLayout.size.width) / 2.0), y: size.height + textSpacing), size: textLayout.size)
}
strongSelf.params = (component, containerSize, layout, textLayout.size)
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}
@@ -0,0 +1,621 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import SwiftSignalKit
import TelegramCore
import AccountContext
import MultilineTextComponent
import BlurredBackgroundComponent
import Markdown
import TelegramPresentationData
import ScrollComponent
private final class LimitComponent: CombinedComponent {
let title: String
let titleColor: UIColor
let text: String
let textColor: UIColor
let accentColor: UIColor
let inactiveColor: UIColor
let inactiveTextColor: UIColor
let inactiveTitle: String
let inactiveValue: String
let activeColor: UIColor
let activeTextColor: UIColor
let activeTitle: String
let activeValue: String
public init(
title: String,
titleColor: UIColor,
text: String,
textColor: UIColor,
accentColor: UIColor,
inactiveColor: UIColor,
inactiveTextColor: UIColor,
inactiveTitle: String,
inactiveValue: String,
activeColor: UIColor,
activeTextColor: UIColor,
activeTitle: String,
activeValue: String
) {
self.title = title
self.titleColor = titleColor
self.text = text
self.textColor = textColor
self.accentColor = accentColor
self.inactiveColor = inactiveColor
self.inactiveTextColor = inactiveTextColor
self.inactiveTitle = inactiveTitle
self.inactiveValue = inactiveValue
self.activeColor = activeColor
self.activeTextColor = activeTextColor
self.activeTitle = activeTitle
self.activeValue = activeValue
}
static func ==(lhs: LimitComponent, rhs: LimitComponent) -> Bool {
if lhs.title != rhs.title {
return false
}
if lhs.titleColor != rhs.titleColor {
return false
}
if lhs.text != rhs.text {
return false
}
if lhs.textColor != rhs.textColor {
return false
}
if lhs.accentColor != rhs.accentColor {
return false
}
if lhs.inactiveColor != rhs.inactiveColor {
return false
}
if lhs.inactiveTextColor != rhs.inactiveTextColor {
return false
}
if lhs.inactiveTitle != rhs.inactiveTitle {
return false
}
if lhs.inactiveValue != rhs.inactiveValue {
return false
}
if lhs.activeColor != rhs.activeColor {
return false
}
if lhs.activeTextColor != rhs.activeTextColor {
return false
}
if lhs.activeTitle != rhs.activeTitle {
return false
}
if lhs.activeValue != rhs.activeValue {
return false
}
return true
}
static var body: Body {
let title = Child(MultilineTextComponent.self)
let text = Child(MultilineTextComponent.self)
let limit = Child(PremiumLimitDisplayComponent.self)
return { context in
let component = context.component
let sideInset: CGFloat = 16.0
let textSideInset: CGFloat = sideInset + 8.0
let spacing: CGFloat = 4.0
let textTopInset: CGFloat = 9.0
let title = title.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(
string: component.title,
font: Font.regular(17.0),
textColor: component.titleColor,
paragraphAlignment: .natural
)),
horizontalAlignment: .center,
maximumNumberOfLines: 1
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
let textFont = Font.regular(13.0)
let boldTextFont = Font.semibold(13.0)
let textColor = component.textColor
let markdownAttributes = MarkdownAttributes(
body: MarkdownAttributeSet(font: textFont, textColor: textColor),
bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor),
link: MarkdownAttributeSet(font: textFont, textColor: component.accentColor),
linkAttribute: { _ in
return nil
}
)
let text = text.update(
component: MultilineTextComponent(
text: .markdown(text: component.text, attributes: markdownAttributes),
horizontalAlignment: .natural,
maximumNumberOfLines: 0,
lineSpacing: 0.0
),
availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height),
transition: .immediate
)
let limit = limit.update(
component: PremiumLimitDisplayComponent(
inactiveColor: component.inactiveColor,
activeColors: [component.activeColor],
inactiveTitle: component.inactiveTitle,
inactiveValue: component.inactiveValue,
inactiveTitleColor: component.inactiveTextColor,
activeTitle: component.activeTitle,
activeValue: component.activeValue,
activeTitleColor: component.activeTextColor,
badgeIconName: "",
badgeText: nil,
badgePosition: 0.0,
badgeGraphPosition: 0.5,
isPremiumDisabled: false
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height),
transition: .immediate
)
context.add(title
.position(CGPoint(x: textSideInset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0))
)
context.add(text
.position(CGPoint(x: textSideInset + text.size.width / 2.0, y: textTopInset + title.size.height + spacing + text.size.height / 2.0))
)
context.add(limit
.position(CGPoint(x: context.availableSize.width / 2.0, y: textTopInset + title.size.height + spacing + text.size.height - 20.0))
)
return CGSize(width: context.availableSize.width, height: textTopInset + title.size.height + text.size.height + 56.0)
}
}
}
private enum Limit: CaseIterable {
case groups
case pins
case publicLinks
case savedGifs
case favedStickers
case about
case captions
case folders
case chatsPerFolder
case account
case recommendedChannels
func title(strings: PresentationStrings) -> String {
switch self {
case .groups:
return strings.Premium_Limits_GroupsAndChannels
case .pins:
return strings.Premium_Limits_PinnedChats
case .publicLinks:
return strings.Premium_Limits_PublicLinks
case .savedGifs:
return strings.Premium_Limits_SavedGifs
case .favedStickers:
return strings.Premium_Limits_FavedStickers
case .about:
return strings.Premium_Limits_Bio
case .captions:
return strings.Premium_Limits_Captions
case .folders:
return strings.Premium_Limits_Folders
case .chatsPerFolder:
return strings.Premium_Limits_ChatsPerFolder
case .account:
return strings.Premium_Limits_Accounts
case .recommendedChannels:
return strings.Premium_Limits_RecommendedChannels
}
}
func text(strings: PresentationStrings) -> String {
switch self {
case .groups:
return strings.Premium_Limits_GroupsAndChannelsInfo
case .pins:
return strings.Premium_Limits_PinnedChatsInfo
case .publicLinks:
return strings.Premium_Limits_PublicLinksInfo
case .savedGifs:
return strings.Premium_Limits_SavedGifsInfo
case .favedStickers:
return strings.Premium_Limits_FavedStickersInfo
case .about:
return strings.Premium_Limits_BioInfo
case .captions:
return strings.Premium_Limits_CaptionsInfo
case .folders:
return strings.Premium_Limits_FoldersInfo
case .chatsPerFolder:
return strings.Premium_Limits_ChatsPerFolderInfo
case .account:
return strings.Premium_Limits_AccountsInfo
case .recommendedChannels:
return strings.Premium_Limits_RecommendedChannelsInfo
}
}
func limit(_ configuration: EngineConfiguration.UserLimits, isPremium: Bool) -> String {
let value: Int32
switch self {
case .groups:
value = configuration.maxChannelsCount
case .pins:
value = configuration.maxPinnedChatCount
case .publicLinks:
value = configuration.maxPublicLinksCount
case .savedGifs:
value = configuration.maxSavedGifCount
case .favedStickers:
value = configuration.maxFavedStickerCount
case .about:
value = configuration.maxAboutLength
case .captions:
value = configuration.maxCaptionLength
case .folders:
value = configuration.maxFoldersCount
case .chatsPerFolder:
value = configuration.maxFolderChatsCount
case .account:
value = isPremium ? 4 : 3
case .recommendedChannels:
value = configuration.maxChannelRecommendationsCount
}
return "\(value)"
}
}
private final class LimitsListComponent: CombinedComponent {
typealias EnvironmentType = (Empty, ScrollChildEnvironment)
let context: AccountContext
let theme: PresentationTheme
let topInset: CGFloat
let bottomInset: CGFloat
init(context: AccountContext, theme: PresentationTheme, topInset: CGFloat, bottomInset: CGFloat) {
self.context = context
self.theme = theme
self.topInset = topInset
self.bottomInset = bottomInset
}
static func ==(lhs: LimitsListComponent, rhs: LimitsListComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.topInset != rhs.topInset {
return false
}
if lhs.bottomInset != rhs.bottomInset {
return false
}
return true
}
final class State: ComponentState {
private let context: AccountContext
private var disposable: Disposable?
var limits: EngineConfiguration.UserLimits = .defaultValue
var premiumLimits: EngineConfiguration.UserLimits = .defaultValue
init(context: AccountContext) {
self.context = context
super.init()
self.disposable = (context.engine.data.get(
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false),
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true)
)
|> deliverOnMainQueue).start(next: { [weak self] limits, premiumLimits in
if let strongSelf = self {
strongSelf.limits = limits
strongSelf.premiumLimits = premiumLimits
strongSelf.updated(transition: .immediate)
}
})
}
deinit {
self.disposable?.dispose()
}
}
func makeState() -> State {
return State(context: self.context)
}
static var body: Body {
let list = Child(List<Empty>.self)
return { context in
let state = context.state
let theme = context.component.context.sharedContext.currentPresentationData.with { $0 }.theme
let strings = context.component.context.sharedContext.currentPresentationData.with { $0 }.strings
let colors = [
UIColor(rgb: 0xdf7138),
UIColor(rgb: 0xd6593f),
UIColor(rgb: 0xca4550),
UIColor(rgb: 0xbc496d),
UIColor(rgb: 0xae4c92),
UIColor(rgb: 0x9153e5),
UIColor(rgb: 0x825af6),
UIColor(rgb: 0x676bf7),
UIColor(rgb: 0x5991f8),
UIColor(rgb: 0x5b99d0),
UIColor(rgb: 0x5ea4a4)
]
let items: [AnyComponentWithIdentity<Empty>] = Limit.allCases.enumerated().map { index, value in
AnyComponentWithIdentity(
id: value, component: AnyComponent(
LimitComponent(
title: value.title(strings: strings),
titleColor: theme.list.itemPrimaryTextColor,
text: value.text(strings: strings),
textColor: theme.list.itemSecondaryTextColor,
accentColor: theme.list.itemAccentColor,
inactiveColor: theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.5),
inactiveTextColor: theme.list.itemPrimaryTextColor,
inactiveTitle: strings.Premium_Free,
inactiveValue: value.limit(state.limits, isPremium: false),
activeColor: colors[index],
activeTextColor: .white,
activeTitle: strings.Premium_Premium,
activeValue: value.limit(state.premiumLimits, isPremium: true)
)
)
)
}
let list = list.update(
component: List(items),
availableSize: CGSize(width: context.availableSize.width, height: 10000.0),
transition: context.transition
)
let contentHeight = context.component.topInset + list.size.height + context.component.bottomInset
context.add(list
.position(CGPoint(x: list.size.width / 2.0, y: context.component.topInset + list.size.height / 2.0))
)
return CGSize(width: context.availableSize.width, height: contentHeight)
}
}
}
final class LimitsPageComponent: CombinedComponent {
typealias EnvironmentType = DemoPageEnvironment
let context: AccountContext
let theme: PresentationTheme
let neighbors: PageNeighbors
let bottomInset: CGFloat
let updatedBottomAlpha: (CGFloat) -> Void
let updatedDismissOffset: (CGFloat) -> Void
let updatedIsDisplaying: (Bool) -> Void
init(context: AccountContext, theme: PresentationTheme, neighbors: PageNeighbors, bottomInset: CGFloat, updatedBottomAlpha: @escaping (CGFloat) -> Void, updatedDismissOffset: @escaping (CGFloat) -> Void, updatedIsDisplaying: @escaping (Bool) -> Void) {
self.context = context
self.theme = theme
self.neighbors = neighbors
self.bottomInset = bottomInset
self.updatedBottomAlpha = updatedBottomAlpha
self.updatedDismissOffset = updatedDismissOffset
self.updatedIsDisplaying = updatedIsDisplaying
}
static func ==(lhs: LimitsPageComponent, rhs: LimitsPageComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.neighbors != rhs.neighbors {
return false
}
if lhs.bottomInset != rhs.bottomInset {
return false
}
return true
}
final class State: ComponentState {
let updateBottomAlpha: (CGFloat) -> Void
let updateDismissOffset: (CGFloat) -> Void
let updatedIsDisplaying: (Bool) -> Void
var resetScroll: ActionSlot<CGPoint?>?
var topContentOffset: CGFloat = 0.0
var bottomContentOffset: CGFloat = 100.0 {
didSet {
self.updateAlpha()
}
}
var position: CGFloat? {
didSet {
self.updateAlpha()
}
}
var isDisplaying = false {
didSet {
if oldValue != self.isDisplaying {
self.updatedIsDisplaying(self.isDisplaying)
if !self.isDisplaying {
self.resetScroll?.invoke(nil)
}
}
}
}
var neighbors = PageNeighbors(leftIsList: false, rightIsList: false)
init(updateBottomAlpha: @escaping (CGFloat) -> Void, updateDismissOffset: @escaping (CGFloat) -> Void, updateIsDisplaying: @escaping (Bool) -> Void) {
self.updateBottomAlpha = updateBottomAlpha
self.updateDismissOffset = updateDismissOffset
self.updatedIsDisplaying = updateIsDisplaying
super.init()
}
func updateAlpha() {
var dismissToLeft = false
if let position = self.position, position > 0.0 {
dismissToLeft = true
}
var dismissPosition = min(1.0, abs(self.position ?? 0.0) / 1.3333)
var position = min(1.0, abs(self.position ?? 0.0))
if position > 0.001, (dismissToLeft && self.neighbors.leftIsList) || (!dismissToLeft && self.neighbors.rightIsList) {
dismissPosition = 0.0
position = 0.0
}
self.updateDismissOffset(dismissPosition)
let verticalPosition = 1.0 - min(30.0, self.bottomContentOffset) / 30.0
let backgroundAlpha: CGFloat = max(position, verticalPosition)
self.updateBottomAlpha(backgroundAlpha)
}
}
func makeState() -> State {
return State(updateBottomAlpha: self.updatedBottomAlpha, updateDismissOffset: self.updatedDismissOffset, updateIsDisplaying: self.updatedIsDisplaying)
}
static var body: Body {
let background = Child(Rectangle.self)
let scroll = Child(ScrollComponent<Empty>.self)
let topPanel = Child(BlurredBackgroundComponent.self)
let topSeparator = Child(Rectangle.self)
let title = Child(MultilineTextComponent.self)
let resetScroll = ActionSlot<CGPoint?>()
return { context in
let state = context.state
let environment = context.environment[DemoPageEnvironment.self].value
state.neighbors = context.component.neighbors
state.resetScroll = resetScroll
state.position = environment.position
state.isDisplaying = environment.isDisplaying
let theme = context.component.theme
let strings = context.component.context.sharedContext.currentPresentationData.with { $0 }.strings
let topInset: CGFloat = 56.0
let scroll = scroll.update(
component: ScrollComponent<Empty>(
content: AnyComponent(
LimitsListComponent(
context: context.component.context,
theme: context.component.theme,
topInset: topInset,
bottomInset: context.component.bottomInset
)
),
contentInsets: UIEdgeInsets(top: topInset, left: 0.0, bottom: 0.0, right: 0.0),
contentOffsetUpdated: { [weak state] topContentOffset, bottomContentOffset in
state?.topContentOffset = topContentOffset
state?.bottomContentOffset = bottomContentOffset
Queue.mainQueue().justDispatch {
state?.updated(transition: .immediate)
}
},
contentOffsetWillCommit: { _ in },
resetScroll: resetScroll
),
availableSize: context.availableSize,
transition: context.transition
)
let background = background.update(
component: Rectangle(color: theme.overallDarkAppearance ? theme.list.blocksBackgroundColor : theme.list.plainBackgroundColor),
availableSize: scroll.size,
transition: context.transition
)
context.add(background
.position(CGPoint(x: context.availableSize.width / 2.0, y: background.size.height / 2.0))
)
context.add(scroll
.position(CGPoint(x: context.availableSize.width / 2.0, y: scroll.size.height / 2.0))
)
let topPanel = topPanel.update(
component: BlurredBackgroundComponent(
color: theme.rootController.navigationBar.blurredBackgroundColor
),
availableSize: CGSize(width: context.availableSize.width, height: topInset),
transition: context.transition
)
let topSeparator = topSeparator.update(
component: Rectangle(
color: theme.rootController.navigationBar.separatorColor
),
availableSize: CGSize(width: context.availableSize.width, height: UIScreenPixel),
transition: context.transition
)
let title = title.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(string: strings.Premium_DoubledLimits, font: Font.semibold(17.0), textColor: theme.rootController.navigationBar.primaryTextColor)),
horizontalAlignment: .center,
truncationType: .end,
maximumNumberOfLines: 1
),
availableSize: context.availableSize,
transition: context.transition
)
let topPanelAlpha: CGFloat = min(30.0, state.topContentOffset) / 30.0
context.add(topPanel
.position(CGPoint(x: context.availableSize.width / 2.0, y: topPanel.size.height / 2.0))
.opacity(topPanelAlpha)
)
context.add(topSeparator
.position(CGPoint(x: context.availableSize.width / 2.0, y: topPanel.size.height))
.opacity(topPanelAlpha)
)
context.add(title
.position(CGPoint(x: context.availableSize.width / 2.0, y: topPanel.size.height / 2.0))
)
return scroll.size
}
}
}
@@ -0,0 +1,420 @@
import Foundation
import UIKit
import Display
import ComponentFlow
public final class PageIndicatorComponent: Component {
private let pageCount: Int
private let position: CGFloat
private let inactiveColor: UIColor
private let activeColor: UIColor
public init(
pageCount: Int,
position: CGFloat,
inactiveColor: UIColor,
activeColor: UIColor
) {
self.pageCount = pageCount
self.position = position
self.inactiveColor = inactiveColor
self.activeColor = activeColor
}
public static func ==(lhs: PageIndicatorComponent, rhs: PageIndicatorComponent) -> Bool {
if lhs.pageCount != rhs.pageCount {
return false
}
if lhs.position != rhs.position {
return false
}
if !lhs.inactiveColor.isEqual(rhs.inactiveColor) {
return false
}
if !lhs.activeColor.isEqual(rhs.activeColor) {
return false
}
return true
}
public final class View: UIView {
private var component: PageIndicatorComponent?
private let indicatorView: PageIndicatorView
public override init(frame: CGRect) {
self.indicatorView = PageIndicatorView(frame: frame)
super.init(frame: frame)
self.addSubview(self.indicatorView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func update(component: PageIndicatorComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
let isFirstTime = self.component == nil
self.component = component
self.indicatorView.pageCount = component.pageCount
self.indicatorView.setProgress(progress: component.position, animated: !isFirstTime)
self.indicatorView.activeColor = component.activeColor
self.indicatorView.inactiveColor = component.inactiveColor
let size = self.indicatorView.intrinsicContentSize
self.indicatorView.frame = CGRect(origin: .zero, size: size)
return size
}
}
public func makeView() -> View {
return View()
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
private final class PageIndicatorView: UIView {
var displayCount: Int {
return min(12, self.pageCount)
}
var dotSize: CGFloat = 8.0
var dotSpace: CGFloat = 10.0
var smallDotSizeRatio: CGFloat = 0.5
var mediumDotSizeRatio: CGFloat = 0.75
public func setCurrentPage(at currentPage: Int, animated: Bool = false) {
guard (currentPage < self.pageCount && currentPage >= 0) else { return }
guard currentPage != self.currentPage else { return }
self.scrollView.layer.removeAllAnimations()
self.updateDot(at: currentPage, animated: animated)
self.currentPage = currentPage
}
public private(set) var currentPage: Int = 0
public var pageCount: Int = 0 {
didSet {
guard self.pageCount != oldValue else {
return
}
self.update(currentPage: self.currentPage)
}
}
public var inactiveColor: UIColor = .gray {
didSet {
guard !self.inactiveColor.isEqual(oldValue) else {
return
}
self.updateDotColor(currentPage: self.currentPage)
}
}
public var activeColor: UIColor = .blue {
didSet {
guard !self.activeColor.isEqual(oldValue) else {
return
}
self.updateDotColor(currentPage: self.currentPage)
}
}
public var animationDuration: Double = 0.3
public init() {
super.init(frame: .zero)
self.setup()
self.updateViewSize()
}
public override init(frame: CGRect) {
super.init(frame: frame)
self.setup()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func layoutSubviews() {
super.layoutSubviews()
self.scrollView.center = CGPoint(x: bounds.width / 2, y: bounds.height / 2)
}
public override var intrinsicContentSize: CGSize {
return CGSize(width: self.itemSize * CGFloat(self.displayCount), height: self.itemSize)
}
public func setProgress(progress: CGFloat, animated: Bool = true) {
let currentPage = Int(round(progress * CGFloat(self.pageCount - 1)))
self.setCurrentPage(at: currentPage, animated: animated)
}
public func updateViewSize() {
self.bounds.size = intrinsicContentSize
}
private let scrollView = UIScrollView()
private var itemSize: CGFloat {
return self.dotSize + self.dotSpace
}
private var items: [ItemView] = []
private func setup() {
self.backgroundColor = .clear
self.scrollView.backgroundColor = .clear
self.scrollView.isUserInteractionEnabled = false
self.scrollView.showsHorizontalScrollIndicator = false
self.addSubview(self.scrollView)
}
private func update(currentPage: Int) {
if currentPage < self.displayCount {
self.items = (-2..<(self.displayCount + 2))
.map { ItemView(itemSize: self.itemSize, dotSize: self.dotSize, smallDotSizeRatio: self.smallDotSizeRatio, mediumDotSizeRatio: self.mediumDotSizeRatio, index: $0) }
} else {
guard let firstItem = self.items.first else { return }
guard let lastItem = self.items.last else { return }
self.items = (firstItem.index...lastItem.index)
.map { ItemView(itemSize: self.itemSize, dotSize: self.dotSize, smallDotSizeRatio: self.smallDotSizeRatio, mediumDotSizeRatio: self.mediumDotSizeRatio, index: $0) }
}
self.scrollView.contentSize = .init(width: self.itemSize * CGFloat(self.pageCount), height: self.itemSize)
self.scrollView.subviews.forEach { $0.removeFromSuperview() }
self.items.forEach { self.scrollView.addSubview($0) }
let size: CGSize = .init(width: self.itemSize * CGFloat(self.displayCount), height: self.itemSize)
self.scrollView.bounds.size = size
if self.displayCount < self.pageCount {
self.scrollView.contentInset = .init(top: 0.0, left: self.itemSize * 2.0, bottom: 0, right: self.itemSize * 2.0)
} else {
self.scrollView.contentInset = .zero
}
self.updateDot(at: currentPage, animated: false)
}
private func updateDot(at currentPage: Int, animated: Bool) {
self.updateDotColor(currentPage: currentPage)
if self.pageCount > self.displayCount {
self.updateDotPosition(currentPage: currentPage, animated: animated)
self.updateDotSize(currentPage: currentPage, animated: animated)
}
}
private func updateDotColor(currentPage: Int) {
self.items.forEach {
$0.dotColor = ($0.index == currentPage) ?
self.activeColor : self.inactiveColor
}
}
private func updateDotPosition(currentPage: Int, animated: Bool) {
let duration = animated ? self.animationDuration : 0
let action: (Int, Double) -> Void = { currentPage, duration in
if currentPage == 0 {
let x = -self.scrollView.contentInset.left
self.moveScrollView(x: x, duration: duration)
} else if currentPage == self.pageCount - 1 {
let x = self.scrollView.contentSize.width - self.scrollView.bounds.width + self.scrollView.contentInset.right
self.moveScrollView(x: x, duration: duration)
} else if CGFloat(currentPage) * self.itemSize <= self.scrollView.contentOffset.x + self.itemSize {
let x = self.scrollView.contentOffset.x - self.itemSize
self.moveScrollView(x: x, duration: duration)
} else if CGFloat(currentPage) * self.itemSize + self.itemSize >= self.scrollView.contentOffset.x + self.scrollView.bounds.width - self.itemSize {
let x = self.scrollView.contentOffset.x + self.itemSize
self.moveScrollView(x: x, duration: duration)
}
}
if !animated {
if currentPage == 0 {
action(currentPage, 0)
} else {
for i in 0 ..< currentPage {
action(i, 0)
}
}
} else {
action(currentPage, duration)
}
}
private func updateDotSize(currentPage: Int, animated: Bool) {
let duration = animated ? self.animationDuration : 0
self.items.forEach { item in
item.animateDuration = duration
if item.index == currentPage {
item.state = .normal
} else if item.index < 0 {
item.state = .none
} else if item.index > self.pageCount - 1 {
item.state = .none
} else if item.frame.minX <= self.scrollView.contentOffset.x {
item.state = .small
} else if item.frame.maxX >= self.scrollView.contentOffset.x + self.scrollView.bounds.width {
item.state = .small
} else if item.frame.minX <= self.scrollView.contentOffset.x + self.itemSize {
item.state = .medium
} else if item.frame.maxX >= self.scrollView.contentOffset.x + self.scrollView.bounds.width - self.itemSize {
item.state = .medium
} else {
item.state = .normal
}
}
}
private func moveScrollView(x: CGFloat, duration: TimeInterval = 0.0) {
let direction = self.behaviorDirection(x: x)
self.reusedView(direction: direction)
if duration > 0.0 {
UIView.animate(withDuration: duration, animations: { [unowned self] in
self.scrollView.contentOffset.x = x
})
} else {
self.scrollView.contentOffset.x = x
}
}
private enum Direction {
case left
case right
case stay
}
private func behaviorDirection(x: CGFloat) -> Direction {
switch x {
case let x where x > self.scrollView.contentOffset.x:
return .right
case let x where x < self.scrollView.contentOffset.x:
return .left
default:
return .stay
}
}
private func reusedView(direction: Direction) {
guard let firstItem = self.items.first else { return }
guard let lastItem = self.items.last else { return }
switch direction {
case .left:
lastItem.index = firstItem.index - 1
lastItem.frame = CGRect(origin: CGPoint(x: CGFloat(lastItem.index) * self.itemSize, y: 0.0), size: CGSize(width: self.itemSize, height: self.itemSize))
self.items.insert(lastItem, at: 0)
self.items.removeLast()
case .right:
firstItem.index = lastItem.index + 1
firstItem.frame = CGRect(origin: CGPoint(x: CGFloat(firstItem.index) * self.itemSize, y: 0.0), size: CGSize(width: self.itemSize, height: self.itemSize))
self.items.insert(firstItem, at: self.items.count)
self.items.removeFirst()
case .stay:
break
}
}
}
private class ItemView: UIView {
enum State {
case none
case small
case medium
case normal
}
private let dotView = UIView()
var index: Int
var dotColor = UIColor.lightGray {
didSet {
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .linear)
transition.updateBackgroundColor(layer: self.dotView.layer, color: dotColor)
}
}
var state: State = .normal {
didSet {
self.updateDotSize(state: state)
}
}
private let itemSize: CGFloat
private let dotSize: CGFloat
private let smallSizeRatio: CGFloat
private let mediumSizeRatio: CGFloat
var animateDuration: Double = 0.3
init(itemSize: CGFloat, dotSize: CGFloat, smallDotSizeRatio: CGFloat, mediumDotSizeRatio: CGFloat, index: Int) {
self.itemSize = itemSize
self.dotSize = dotSize
self.mediumSizeRatio = mediumDotSizeRatio
self.smallSizeRatio = smallDotSizeRatio
self.index = index
let x = itemSize * CGFloat(index)
let frame = CGRect(x: x, y: 0, width: itemSize, height: itemSize)
super.init(frame: frame)
self.backgroundColor = UIColor.clear
self.dotView.frame.size = CGSize(width: dotSize, height: dotSize)
self.dotView.center = CGPoint(x: itemSize / 2.0, y: itemSize / 2.0)
self.dotView.backgroundColor = self.dotColor
self.dotView.layer.cornerRadius = dotSize / 2.0
self.dotView.layer.masksToBounds = true
addSubview(dotView)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func updateDotSize(state: State) {
var size: CGSize
switch state {
case .normal:
size = CGSize(width: self.dotSize, height: self.dotSize)
case .medium:
size = CGSize(width: self.dotSize * self.mediumSizeRatio, height: self.dotSize * self.mediumSizeRatio)
case .small:
size = CGSize( width: self.dotSize * self.smallSizeRatio, height: self.dotSize * self.smallSizeRatio
)
case .none:
size = CGSize.zero
}
UIView.animate(withDuration: self.animateDuration, animations: { [unowned self] in
self.dotView.layer.cornerRadius = size.height / 2.0
self.dotView.layer.bounds.size = size
})
}
}
@@ -0,0 +1,742 @@
import Foundation
import UIKit
import SceneKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import ComponentFlow
import AccountContext
import RadialStatusNode
import UniversalMediaPlayer
import TelegramUniversalVideoContent
import AppBundle
import ShimmerEffect
private let phoneSize = CGSize(width: 262.0, height: 539.0)
private var phoneBorderImage = {
return generateImage(phoneSize, rotatedContext: { size, context in
context.clear(CGRect(origin: .zero, size: size))
context.setFillColor(UIColor(rgb: 0x000000, alpha: 0.5).cgColor)
try? drawSvgPath(context, path: "M203.506,7.0 C211.281,0.0 217.844,0.0 223.221,0.439253 C228.851,0.899605 234.245,1.90219 239.377,4.51905 C247.173,8.49411 253.512,14.8369 257.484,22.6384 C260.099,27.7743 261.101,33.1718 261.561,38.8062 C262.0,44.1865 262.0,50.754 262.0,58.5351 V480.465 C262.0,488.246 262.0,494.813 261.561,500.194 C261.101,505.828 260.099,511.226 257.484,516.362 C253.512,524.163 247.173,530.506 239.377,534.481 C234.245,537.098 228.851,538.1 223.221,538.561 C217.844,539.0 211.281,539.0 203.506,539.0 H58.4942 C50.7185,539 44.1556,539.0 38.7791,538.561 C33.1486,538.1 27.7549,537.098 22.6226,534.481 C14.8265,530.506 8.48817,524.163 4.51589,516.362 C1.90086,511.226 0.898976,505.828 0.438946,500.194 C0.0,494.813 0.0,488.246 7.0,480.465 V58.5354 C0.0,50.7541 0.0,44.1866 0.438946,38.8062 C0.898976,33.1718 1.90086,27.7743 4.51589,22.6384 C8.48817,14.8369 14.8265,8.49411 22.6226,4.51905 C27.7549,1.90219 33.1486,0.899605 38.7791,0.439253 C44.1557,-0.0 50.7187,0.0 58.4945,7.0 H203.506 Z ")
context.setBlendMode(.copy)
context.fill(CGRect(origin: CGPoint(x: 43.0, y: UIScreenPixel), size: CGSize(width: 175.0, height: 8.0)))
context.fill(CGRect(origin: CGPoint(x: UIScreenPixel, y: 43.0), size: CGSize(width: 8.0, height: 452.0)))
context.setBlendMode(.clear)
try? drawSvgPath(context, path: "M15.3737,28.1746 C12.1861,34.4352 12.1861,42.6307 12.1861,59.0217 V479.978 C12.1861,496.369 12.1861,504.565 15.3737,510.825 C18.1777,516.332 22.6518,520.81 28.1549,523.615 C34.4111,526.805 42.6009,526.805 58.9805,526.805 H203.02 C219.399,526.805 227.589,526.805 233.845,523.615 C239.348,520.81 243.822,516.332 246.626,510.825 C249.814,504.565 249.814,496.369 249.814,479.978 V59.0217 C249.814,42.6307 249.814,34.4352 246.626,28.1746 C243.822,22.6677 239.348,18.1904 233.845,15.3845 C227.589,12.1946 219.399,12.1946 203.02,12.1946 H58.9805 C42.6009,12.1946 34.4111,12.1946 28.1549,15.3845 C22.6518,18.1904 18.1777,22.6677 15.3737,28.1746 Z ")
context.setBlendMode(.copy)
context.setFillColor(UIColor.black.cgColor)
try? drawSvgPath(context, path: "M222.923,4.08542 C217.697,3.65815 211.263,3.65823 203.378,3.65833 H58.6219 C50.7366,3.65823 44.3026,3.65815 39.0768,4.08542 C33.6724,4.52729 28.8133,5.46834 24.2823,7.77863 C17.1741,11.4029 11.395,17.1861 7.77325,24.2992 C5.46457,28.8334 4.52418,33.6959 4.08262,39.1041 C3.65565,44.3336 3.65573,50.7721 3.65583,58.6628 V480.337 C3.65573,488.228 3.65565,494.666 4.08262,499.896 C4.52418,505.304 5.46457,510.167 7.77325,514.701 C11.395,521.814 17.1741,527.597 24.2823,531.221 C28.8133,533.532 33.6724,534.473 39.0768,534.915 C44.3028,535.342 50.737,535.342 58.6226,535.342 H203.377 C211.263,535.342 217.697,535.342 222.923,534.915 C228.328,534.473 233.187,533.532 237.718,531.221 C244.826,527.597 250.605,521.814 254.227,514.701 C256.535,510.167 257.476,505.304 257.917,499.896 C258.344,494.667 258.344,488.228 258.344,480.338 V58.6617 C258.344,50.7714 258.344,44.3333 257.917,39.1041 C257.476,33.6959 256.535,28.8334 254.227,24.2992 C250.605,17.1861 244.826,11.4029 237.718,7.77863 C233.187,5.46834 228.328,4.52729 222.923,4.08542 Z ")
context.setBlendMode(.clear)
try? drawSvgPath(context, path: "M12.1861,59.0217 C12.1861,42.6306 12.1861,34.4351 15.3737,28.1746 C18.1777,22.6676 22.6519,18.1904 28.1549,15.3844 C34.4111,12.1945 42.6009,12.1945 58.9805,12.1945 H76.6868 L76.8652,12.1966 C78.1834,12.2201 79.0316,12.4428 79.7804,12.8418 C80.5733,13.2644 81.1963,13.8848 81.6226,14.6761 C81.9735,15.3276 82.1908,16.0553 82.2606,17.1064 C82.3128,22.5093 82.9306,24.5829 84.0474,26.6727 C85.2157,28.8587 86.9301,30.5743 89.1145,31.7434 C91.299,32.9124 93.4658,33.535 99.441,33.535 H162.561 C168.537,33.535 170.703,32.9124 172.888,31.7434 C175.072,30.5743 176.787,28.8587 177.955,26.6727 C179.072,24.5829 179.69,22.5093 179.742,17.1051 C179.812,16.0553 180.029,15.3276 180.38,14.6761 C180.806,13.8848 181.429,13.2644 182.222,12.8418 C182.971,12.4428 183.819,12.2201 185.137,12.1966 L185.316,12.1945 H203.02 C219.399,12.1945 227.589,12.1945 233.845,15.3844 C239.348,18.1904 243.822,22.6676 246.626,28.1746 C249.814,34.4351 249.814,42.6306 249.814,59.0217 V479.978 C249.814,496.369 249.814,504.565 246.626,510.825 C243.822,516.332 239.348,520.81 233.845,523.615 C227.589,526.805 219.399,526.805 203.02,526.805 H58.9805 C42.6009,526.805 34.4111,526.805 28.1549,523.615 C22.6519,520.81 18.1777,516.332 15.3737,510.825 C12.1861,504.565 12.1861,496.369 12.1861,479.978 V59.0217 Z ")
})
}()
private var phoneBorderImage14 = {
return generateImage(phoneSize, rotatedContext: { size, context in
context.clear(CGRect(origin: .zero, size: size))
context.setFillColor(UIColor(rgb: 0x000000, alpha: 0.5).cgColor)
try? drawSvgPath(context, path: "M203.506,7.0 C211.281,0.0 217.844,0.0 223.221,0.439253 C228.851,0.899605 234.245,1.90219 239.377,4.51905 C247.173,8.49411 253.512,14.8369 257.484,22.6384 C260.099,27.7743 261.101,33.1718 261.561,38.8062 C262.0,44.1865 262.0,50.754 262.0,58.5351 V480.465 C262.0,488.246 262.0,494.813 261.561,500.194 C261.101,505.828 260.099,511.226 257.484,516.362 C253.512,524.163 247.173,530.506 239.377,534.481 C234.245,537.098 228.851,538.1 223.221,538.561 C217.844,539.0 211.281,539.0 203.506,539.0 H58.4942 C50.7185,539 44.1556,539.0 38.7791,538.561 C33.1486,538.1 27.7549,537.098 22.6226,534.481 C14.8265,530.506 8.48817,524.163 4.51589,516.362 C1.90086,511.226 0.898976,505.828 0.438946,500.194 C0.0,494.813 0.0,488.246 7.0,480.465 V58.5354 C0.0,50.7541 0.0,44.1866 0.438946,38.8062 C0.898976,33.1718 1.90086,27.7743 4.51589,22.6384 C8.48817,14.8369 14.8265,8.49411 22.6226,4.51905 C27.7549,1.90219 33.1486,0.899605 38.7791,0.439253 C44.1557,-0.0 50.7187,0.0 58.4945,7.0 H203.506 Z ")
context.setBlendMode(.copy)
context.fill(CGRect(origin: CGPoint(x: 43.0, y: UIScreenPixel), size: CGSize(width: 175.0, height: 8.0)))
context.fill(CGRect(origin: CGPoint(x: UIScreenPixel, y: 43.0), size: CGSize(width: 8.0, height: 452.0)))
context.setBlendMode(.clear)
try? drawSvgPath(context, path: "M15.3737,28.1746 C12.1861,34.4352 12.1861,42.6307 12.1861,59.0217 V479.978 C12.1861,496.369 12.1861,504.565 15.3737,510.825 C18.1777,516.332 22.6518,520.81 28.1549,523.615 C34.4111,526.805 42.6009,526.805 58.9805,526.805 H203.02 C219.399,526.805 227.589,526.805 233.845,523.615 C239.348,520.81 243.822,516.332 246.626,510.825 C249.814,504.565 249.814,496.369 249.814,479.978 V59.0217 C249.814,42.6307 249.814,34.4352 246.626,28.1746 C243.822,22.6677 239.348,18.1904 233.845,15.3845 C227.589,12.1946 219.399,12.1946 203.02,12.1946 H58.9805 C42.6009,12.1946 34.4111,12.1946 28.1549,15.3845 C22.6518,18.1904 18.1777,22.6677 15.3737,28.1746 Z ")
context.setBlendMode(.copy)
context.setFillColor(UIColor.black.cgColor)
try? drawSvgPath(context, path: "M222.923,4.08542 C217.697,3.65815 211.263,3.65823 203.378,3.65833 H58.6219 C50.7366,3.65823 44.3026,3.65815 39.0768,4.08542 C33.6724,4.52729 28.8133,5.46834 24.2823,7.77863 C17.1741,11.4029 11.395,17.1861 7.77325,24.2992 C5.46457,28.8334 4.52418,33.6959 4.08262,39.1041 C3.65565,44.3336 3.65573,50.7721 3.65583,58.6628 V480.337 C3.65573,488.228 3.65565,494.666 4.08262,499.896 C4.52418,505.304 5.46457,510.167 7.77325,514.701 C11.395,521.814 17.1741,527.597 24.2823,531.221 C28.8133,533.532 33.6724,534.473 39.0768,534.915 C44.3028,535.342 50.737,535.342 58.6226,535.342 H203.377 C211.263,535.342 217.697,535.342 222.923,534.915 C228.328,534.473 233.187,533.532 237.718,531.221 C244.826,527.597 250.605,521.814 254.227,514.701 C256.535,510.167 257.476,505.304 257.917,499.896 C258.344,494.667 258.344,488.228 258.344,480.338 V58.6617 C258.344,50.7714 258.344,44.3333 257.917,39.1041 C257.476,33.6959 256.535,28.8334 254.227,24.2992 C250.605,17.1861 244.826,11.4029 237.718,7.77863 C233.187,5.46834 228.328,4.52729 222.923,4.08542 Z ")
context.setBlendMode(.clear)
try? drawSvgPath(context, path: "M12.1861,59.0217 C12.1861,42.6306 12.1861,34.4351 15.3737,28.1746 C18.1777,22.6676 22.6519,18.1904 28.1549,15.3844 C34.4111,12.1945 42.6009,12.1945 58.9805,12.1945 H76.6868 H162.561 L185.316,12.1945 H203.02 C219.399,12.1945 227.589,12.1945 233.845,15.3844 C239.348,18.1904 243.822,22.6676 246.626,28.1746 C249.814,34.4351 249.814,42.6306 249.814,59.0217 V479.978 C249.814,496.369 249.814,504.565 246.626,510.825 C243.822,516.332 239.348,520.81 233.845,523.615 C227.589,526.805 219.399,526.805 203.02,526.805 H58.9805 C42.6009,526.805 34.4111,526.805 28.1549,523.615 C22.6519,520.81 18.1777,516.332 15.3737,510.825 C12.1861,504.565 12.1861,496.369 12.1861,479.978 V59.0217 Z ")
context.setBlendMode(.copy)
let islandSize = CGSize(width: 76.0, height: 23.0)
let island = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: floor((size.width - islandSize.width) / 2.0), y: 17.0), size: islandSize), cornerRadius: islandSize.height / 2.0)
context.addPath(island.cgPath)
context.fillPath()
})
}()
private var phoneBorderMaskImage = {
generateImage(phoneSize, rotatedContext: { size, context in
context.clear(CGRect(origin: .zero, size: size))
context.setStrokeColor(UIColor.white.cgColor)
context.setLineWidth(2.0)
context.translateBy(x: 12.0, y: 12.0 - UIScreenPixel)
try? drawSvgPath(context, path: "M1.17188,47.3156 C1.17188,39.1084 1.17265,33.013 1.56706,28.1857 C1.96052,23.3701 2.74071,19.9044 4.25094,16.9404 C6.95936,11.6248 11.2811,7.30311 16.5966,4.59469 C19.5606,3.08446 23.0263,2.30427 27.842,1.91081 C32.6693,1.5164 38.7646,1.51562 46.9719,1.51562 H64.6745 H64.6803 L64.8409,1.51754 C64.8419,1.51756 64.8429,1.51758 64.8439,1.5176 C66.0418,1.53925 66.7261,1.73731 67.3042,2.04519 L67.7736,1.16377 L67.3042,2.04519 C67.9232,2.37486 68.4036,2.8529 68.7364,3.47024 C69.0069,3.97209 69.1915,4.54972 69.2551,5.46352 C69.3102,10.9333 69.9419,13.1793 71.16,15.457 C72.4216,17.816 74.2789,19.6733 76.6379,20.9349 C79.0269,22.2126 81.3803,22.8438 87.4372,22.8438 H150.565 C156.622,22.8438 158.976,22.2126 161.364,20.9349 C163.723,19.6733 165.581,17.816 166.842,15.457 C168.061,13.1793 168.692,10.9334 168.747,5.46231 C168.811,4.54985 168.995,3.97217 169.266,3.47025 C169.599,2.8529 170.079,2.37486 170.698,2.04519 C171.276,1.7373 171.961,1.53925 173.159,1.5176 C173.16,1.51758 173.161,1.51756 173.162,1.51754 L173.322,1.51562 H173.328 H191.028 C199.235,1.51562 205.331,1.5164 210.158,1.91081 C214.974,2.30427 218.439,3.08446 221.403,4.59469 C226.719,7.30311 231.041,11.6248 233.749,16.9404 C235.259,19.9044 236.039,23.3701 236.433,28.1857 C236.827,33.013 236.828,39.1084 236.828,47.3156 V468.028 C236.828,476.235 236.827,482.331 236.433,487.158 C236.039,491.974 235.259,495.439 233.749,498.403 C231.041,503.719 226.719,508.041 221.403,510.749 C218.439,512.259 214.974,513.039 210.158,513.433 C205.331,513.827 199.235,513.828 191.028,513.828 H46.9719 C38.7646,513.828 32.6693,513.827 27.842,513.433 C23.0263,513.039 19.5606,512.259 16.5966,510.749 C11.2811,508.041 6.95936,503.719 4.25094,498.403 C2.74071,495.439 1.96052,491.974 1.56706,487.158 C1.17265,482.331 1.17188,476.235 1.17188,468.028 V47.3156 S ")
})
}()
private var phoneBorderMaskImage14 = {
generateImage(phoneSize, rotatedContext: { size, context in
context.clear(CGRect(origin: .zero, size: size))
context.setStrokeColor(UIColor.white.cgColor)
context.setLineWidth(2.0)
context.translateBy(x: 12.0, y: 12.0 - UIScreenPixel)
try? drawSvgPath(context, path: " M1.17188,47.3156 C1.17188,39.1084 1.17265,33.013 1.56706,28.1857 C1.96052,23.3701 2.74071,19.9044 4.25094,16.9404 C6.95936,11.6248 11.2811,7.30311 16.5966,4.59469 C19.5606,3.08446 23.0263,2.30427 27.842,1.91081 C32.6693,1.5164 38.7646,1.51562 46.9719,1.51562 H64.6745 H64.6803 L64.8409,1.51754 L173.322,1.51562 H173.328 H191.028 C199.235,1.51562 205.331,1.5164 210.158,1.91081 C214.974,2.30427 218.439,3.08446 221.403,4.59469 C226.719,7.30311 231.041,11.6248 233.749,16.9404 C235.259,19.9044 236.039,23.3701 236.433,28.1857 C236.827,33.013 236.828,39.1084 236.828,47.3156 V468.028 C236.828,476.235 236.827,482.331 236.433,487.158 C236.039,491.974 235.259,495.439 233.749,498.403 C231.041,503.719 226.719,508.041 221.403,510.749 C218.439,512.259 214.974,513.039 210.158,513.433 C205.331,513.827 199.235,513.828 191.028,513.828 H46.9719 C38.7646,513.828 32.6693,513.827 27.842,513.433 C23.0263,513.039 19.5606,512.259 16.5966,510.749 C11.2811,508.041 6.95936,503.719 4.25094,498.403 C2.74071,495.439 1.96052,491.974 1.56706,487.158 C1.17265,482.331 1.17188,476.235 1.17188,468.028 V47.3156 S ")
})
}()
private var starMaskImage = {
return generateImage(CGSize(width: 88.0, height: 84.0), rotatedContext: { size, context in
context.clear(CGRect(origin: .zero, size: size))
context.setFillColor(UIColor.white.cgColor)
try? drawSvgPath(context, path: "M41.7419,71.1897 L22.1639,83.1833 C20.1282,84.4304 17.4669,83.7911 16.2198,81.7553 C15.6107,80.7611 15.4291,79.5629 15.7162,78.4328 L18.7469,66.504 C19.8409,62.1979 22.7876,58.5983 26.7928,56.6754 L48.1514,46.4207 C49.1472,45.9426 49.5668,44.7479 49.0887,43.7521 C48.7016,42.9457 47.826,42.4945 46.9446,42.6471 L23.1697,46.7631 C18.3368,47.5998 13.3807,46.2653 9.62146,43.1149 L2.11077,36.8207 C0.28097,35.2873 0.0407101,32.5609 1.57413,30.7311 C2.31994,29.8411 3.39241,29.2886 4.55001,29.198 L27.4974,27.4022 C29.1186,27.2753 30.5314,26.2494 31.1537,24.747 L40.0064,3.37722 C40.9201,1.17161 43.4488,0.124313 45.6544,1.03801 C46.7135,1.47673 47.5549,2.31816 47.9936,3.37722 L56.8463,24.747 C57.4686,26.2494 58.8815,27.2753 60.5026,27.4022 L83.5761,29.2079 C85.9562,29.3942 87.7347,31.4746 87.5484,33.8547 C87.4588,34.9997 86.9172,36.0619 86.0433,36.807 L68.4461,51.809 C67.2073,52.8651 66.6669,54.5275 67.0478,56.1102 L72.4577,78.5841 C73.0165,80.9052 71.5878,83.2397 69.2667,83.7985 C68.1515,84.0669 66.9752,83.8811 65.997,83.2818 L46.2581,71.1897 C44.8724,70.3408 43.1277,70.3408 41.7419,71.1897 Z ")
})
}()
private final class PhoneView: UIView {
let contentContainerView: UIView
let overlayView: UIView
let borderView: UIImageView
let backShimmerView: UIView
let backShimmerEffectView: ShimmerEffectForegroundView
let backShimmerFadeView: UIView
let frontShimmerView: UIView
let shimmerEffectView: ShimmerEffectForegroundView
let shimmerMaskView: UIView
let shimmerBorderView: UIImageView
let shimmerStarView: UIImageView
fileprivate var videoNode: UniversalVideoNode?
var playbackStatus: Signal<MediaPlayerStatus?, NoError> {
return self.playbackStatusPromise.get()
}
private var playbackStatusPromise = ValuePromise<MediaPlayerStatus?>(nil)
private var playbackStatusValue: MediaPlayerStatus?
private var statusDisposable = MetaDisposable()
var screenRotation: CGFloat = 0.0 {
didSet {
if self.screenRotation > 0.0 {
self.overlayView.backgroundColor = .white
} else {
self.overlayView.backgroundColor = .black
}
self.overlayView.alpha = self.screenRotation > 0.0 ? self.screenRotation * 0.5 : self.screenRotation * -1.0
}
}
var model: PhoneDemoComponent.Model = .notch {
didSet {
if self.model != oldValue {
switch self.model {
case .notch:
self.borderView.image = phoneBorderImage
self.shimmerBorderView.image = phoneBorderMaskImage
case .island:
self.borderView.image = phoneBorderImage14
self.shimmerBorderView.image = phoneBorderMaskImage14
}
}
}
}
override init(frame: CGRect) {
self.contentContainerView = UIView()
self.contentContainerView.clipsToBounds = true
self.contentContainerView.backgroundColor = .darkGray
self.contentContainerView.layer.cornerRadius = 10.0
self.contentContainerView.layer.allowsGroupOpacity = true
self.overlayView = UIView()
self.overlayView.backgroundColor = .black
self.borderView = UIImageView(image: phoneBorderImage)
self.shimmerMaskView = UIView()
self.shimmerBorderView = UIImageView(image: phoneBorderMaskImage)
self.shimmerStarView = UIImageView(image: starMaskImage)
self.shimmerStarView.alpha = 0.7
self.backShimmerView = UIView()
self.backShimmerView.alpha = 0.0
self.backShimmerEffectView = ShimmerEffectForegroundView()
self.backShimmerFadeView = UIView()
self.backShimmerFadeView.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.2)
self.frontShimmerView = UIView()
self.frontShimmerView.alpha = 0.0
self.shimmerEffectView = ShimmerEffectForegroundView()
super.init(frame: frame)
self.addSubview(self.contentContainerView)
self.contentContainerView.addSubview(self.overlayView)
self.contentContainerView.addSubview(self.backShimmerView)
self.addSubview(self.borderView)
self.addSubview(self.frontShimmerView)
self.backShimmerView.addSubview(self.backShimmerFadeView)
self.backShimmerView.addSubview(self.backShimmerEffectView)
self.shimmerMaskView.addSubview(self.shimmerBorderView)
self.shimmerMaskView.addSubview(self.shimmerStarView)
self.frontShimmerView.mask = self.shimmerMaskView
self.frontShimmerView.addSubview(self.shimmerEffectView)
self.backShimmerEffectView.update(backgroundColor: .clear, foregroundColor: UIColor.white.withAlphaComponent(0.45), gradientSize: 32.0, globalTimeOffset: true, duration: 4.0, horizontal: true)
self.backShimmerEffectView.layer.compositingFilter = "overlayBlendMode"
self.shimmerEffectView.update(backgroundColor: .clear, foregroundColor: UIColor.white.withAlphaComponent(0.5), gradientSize: 16.0, globalTimeOffset: true, duration: 4.0, horizontal: true)
self.shimmerEffectView.layer.compositingFilter = "overlayBlendMode"
}
deinit {
self.statusDisposable.dispose()
}
private var position: PhoneDemoComponent.Position = .top
func setup(context: AccountContext, videoFile: TelegramMediaFile?, position: PhoneDemoComponent.Position) {
self.position = position
guard self.videoNode == nil, let file = videoFile else {
return
}
self.contentContainerView.backgroundColor = .clear
let videoContent = NativeVideoContent(
id: .message(1, EngineMedia.Id(namespace: 0, id: Int64.random(in: 0..<Int64.max))),
userLocation: .other,
fileReference: .standalone(media: file),
streamVideo: .conservative,
loopVideo: true,
enableSound: false,
fetchAutomatically: true,
onlyFullSizeThumbnail: false,
continuePlayingWithoutSoundOnLostAudioSession: false,
placeholderColor: .darkGray,
hintDimensions: CGSize(width: 1170, height: 1754),
storeAfterDownload: nil
)
let videoNode = UniversalVideoNode(context: context, postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: VideoDecoration(), content: videoContent, priority: .embedded)
videoNode.canAttachContent = true
self.videoNode = videoNode
let status = videoNode.status
|> mapToSignal { status -> Signal<MediaPlayerStatus?, NoError> in
var isLoading = false
if let status = status {
if case .buffering = status.status {
isLoading = true
} else if status.duration.isZero {
isLoading = true
}
}
if isLoading {
return .single(status) |> delay(0.6, queue: Queue.mainQueue())
} else {
return .single(status)
}
}
self.statusDisposable.set((status |> deliverOnMainQueue).start(next: { [weak self] status in
if let strongSelf = self {
strongSelf.playbackStatusValue = status
strongSelf.playbackStatusPromise.set(status)
strongSelf.updatePlaybackStatus()
}
}))
self.contentContainerView.insertSubview(videoNode.view, at: 0)
videoNode.pause()
self.setNeedsLayout()
}
private func updatePlaybackStatus() {
var isDisplayingProgress = false
if let playbackStatus = self.playbackStatusValue {
if case .buffering = playbackStatus.status {
isDisplayingProgress = true
} else if playbackStatus.status == .playing {
isDisplayingProgress = playbackStatus.duration.isZero
}
} else {
isDisplayingProgress = true
}
let targetAlpha = isDisplayingProgress ? 1.0 : 0.0
if self.frontShimmerView.alpha != targetAlpha {
let sourceAlpha = self.frontShimmerView.alpha
self.frontShimmerView.alpha = targetAlpha
self.frontShimmerView.layer.animateAlpha(from: sourceAlpha, to: targetAlpha, duration: 0.2)
self.backShimmerView.alpha = targetAlpha
self.backShimmerView.layer.animateAlpha(from: sourceAlpha, to: targetAlpha, duration: 0.2)
}
}
private var isPlaying = false
func play() {
if let videoNode = self.videoNode, !self.isPlaying {
self.isPlaying = true
videoNode.play()
}
}
func reset() {
if let videoNode = self.videoNode {
self.isPlaying = false
videoNode.pause()
videoNode.seek(0.0)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
if let phoneImage = self.borderView.image {
let phoneBounds = CGRect(origin: .zero, size: phoneImage.size)
self.borderView.frame = phoneBounds
self.contentContainerView.frame = CGRect(origin: CGPoint(x: 12.0, y: 12.0), size: CGSize(width: phoneImage.size.width - 24.0, height: phoneImage.size.height - 24.0))
self.overlayView.frame = self.contentContainerView.bounds
let videoSize = CGSize(width: self.contentContainerView.frame.width, height: 354.0)
if let videoNode = self.videoNode {
videoNode.view.frame = CGRect(origin: CGPoint(x: 0.0, y: self.position == .top ? 0.0 : self.contentContainerView.frame.height - videoSize.height), size: videoSize)
videoNode.updateLayout(size: videoSize, transition: .immediate)
}
self.backShimmerView.frame = phoneBounds.insetBy(dx: -12.0, dy: -12.0)
self.backShimmerEffectView.frame = phoneBounds
self.backShimmerFadeView.frame = phoneBounds
self.frontShimmerView.frame = phoneBounds
self.shimmerEffectView.frame = phoneBounds
self.shimmerMaskView.frame = phoneBounds
self.shimmerBorderView.frame = phoneBounds
self.backShimmerEffectView.updateAbsoluteRect(CGRect(origin: CGPoint(x: phoneBounds.width * 8.0, y: 0.0), size: phoneBounds.size), within: CGSize(width: phoneBounds.width * 17.0, height: phoneBounds.height))
self.shimmerEffectView.updateAbsoluteRect(CGRect(origin: CGPoint(x: phoneBounds.width * 8.0, y: 0.0), size: phoneBounds.size), within: CGSize(width: phoneBounds.width * 17.0, height: phoneBounds.height))
let notchHeight: CGFloat = 20.0
if let starImage = self.shimmerStarView.image {
let starSize = starImage.size
self.shimmerStarView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((phoneImage.size.width - starSize.width) / 2.0), y: self.position == .top ? notchHeight + floor((videoSize.height - notchHeight - starSize.height) / 2.0) : self.contentContainerView.frame.height - videoSize.height + floor((videoSize.height - starSize.height) / 2.0)), size: starSize)
}
}
}
}
protocol PhoneDemoDecorationView: UIView {
func setVisible(_ visible: Bool)
func resetAnimation()
}
public final class PhoneDemoComponent: Component {
public typealias EnvironmentType = DemoPageEnvironment
public enum Position {
case top
case bottom
}
public enum BackgroundDecoration {
case none
case dataRain
case swirlStars
case fasterStars
case badgeStars
case emoji
case hello
case tag
case business
case todo
}
public enum Model {
case notch
case island
}
let context: AccountContext
let position: Position
let model: Model
let videoFile: TelegramMediaFile?
let decoration: BackgroundDecoration
public init(
context: AccountContext,
position: PhoneDemoComponent.Position,
model: Model = .notch,
videoFile: TelegramMediaFile?,
decoration: BackgroundDecoration = .none
) {
self.context = context
self.position = position
self.model = model
self.videoFile = videoFile
self.decoration = decoration
}
public static func ==(lhs: PhoneDemoComponent, rhs: PhoneDemoComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.position != rhs.position {
return false
}
if lhs.model != rhs.model {
return false
}
if lhs.videoFile != rhs.videoFile {
return false
}
if lhs.decoration != rhs.decoration {
return false
}
return true
}
public final class View: UIView, ComponentTaggedView {
public final class Tag {
}
public func matches(tag: Any) -> Bool {
if let _ = tag as? Tag, self.isCentral {
return true
}
return false
}
private var isCentral = false
private var component: PhoneDemoComponent?
private let decorationContainerView: UIView
private var decorationView: PhoneDemoDecorationView?
private let containerView: UIView
private let phoneView: PhoneView
private var playbackStatusDisposable: Disposable?
public var ready: Signal<Bool, NoError> {
if let videoNode = self.phoneView.videoNode {
return videoNode.ready
|> map { _ in
return true
}
} else {
return .single(true)
}
}
public override init(frame: CGRect) {
self.decorationContainerView = UIView(frame: frame)
self.decorationContainerView.clipsToBounds = true
self.containerView = UIView(frame: frame)
self.containerView.clipsToBounds = true
self.phoneView = PhoneView(frame: CGRect(origin: .zero, size: phoneSize))
self.phoneView.isUserInteractionEnabled = false
super.init(frame: frame)
self.addSubview(self.decorationContainerView)
self.addSubview(self.containerView)
self.containerView.addSubview(self.phoneView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.playbackStatusDisposable?.dispose()
}
public func update(component: PhoneDemoComponent, availableSize: CGSize, environment: Environment<DemoPageEnvironment>, transition: ComponentTransition) -> CGSize {
self.component = component
self.containerView.frame = CGRect(origin: .zero, size: availableSize)
self.decorationContainerView.frame = CGRect(origin: CGPoint(x: -availableSize.width * 0.5, y: 0.0), size: CGSize(width: availableSize.width * 2.0, height: availableSize.height))
self.phoneView.bounds = CGRect(origin: .zero, size: phoneSize)
self.phoneView.model = component.model
switch component.decoration {
case .none:
break
case .dataRain:
if #available(iOS 10.0, *) {
if let _ = self.decorationView as? MatrixView {
} else if let decorationView = MatrixView(test: true) {
decorationView.frame = self.decorationContainerView.bounds.insetBy(dx: availableSize.width * 0.5, dy: 0.0)
self.decorationView = decorationView
self.decorationContainerView.addSubview(decorationView)
}
}
case .swirlStars:
if let _ = self.decorationView as? SwirlStarsView {
} else {
let decorationView = SwirlStarsView(frame: self.decorationContainerView.bounds)
self.decorationView = decorationView
self.decorationContainerView.addSubview(decorationView)
}
case .fasterStars:
if let _ = self.decorationView as? FasterStarsView {
} else {
let decorationView = FasterStarsView(frame: self.decorationContainerView.bounds)
self.decorationView = decorationView
self.decorationContainerView.addSubview(decorationView)
self.playbackStatusDisposable = (self.phoneView.playbackStatus
|> deliverOnMainQueue).start(next: { [weak decorationView] status in
if let starsView = decorationView, let status = status {
if status.timestamp > 8.0 {
starsView.resetAnimation()
} else if status.timestamp > 0.85 {
starsView.startAnimation()
}
}
})
}
case .badgeStars:
if let _ = self.decorationView as? BadgeStarsView {
} else {
let decorationView = BadgeStarsView(frame: self.decorationContainerView.bounds)
self.decorationView = decorationView
self.decorationContainerView.addSubview(decorationView)
}
case .emoji:
if let _ = self.decorationView as? EmojiStarsView {
} else {
let decorationView = EmojiStarsView(frame: self.decorationContainerView.bounds)
self.decorationView = decorationView
self.decorationContainerView.addSubview(decorationView)
}
case .hello:
if let _ = self.decorationView as? HelloView {
} else {
let decorationView = HelloView(frame: self.decorationContainerView.bounds)
self.decorationView = decorationView
self.decorationContainerView.addSubview(decorationView)
}
case .tag:
if let _ = self.decorationView as? TagStarsView {
} else {
let decorationView = TagStarsView(frame: self.decorationContainerView.bounds)
self.decorationView = decorationView
self.decorationContainerView.addSubview(decorationView)
}
case .business:
if let _ = self.decorationView as? BadgeBusinessView {
} else {
let decorationView = BadgeBusinessView(frame: self.decorationContainerView.bounds)
self.decorationView = decorationView
self.decorationContainerView.addSubview(decorationView)
}
case .todo:
if let _ = self.decorationView as? TodoChecksView {
} else {
let decorationView = TodoChecksView(frame: self.decorationContainerView.bounds)
self.decorationView = decorationView
self.decorationContainerView.addSubview(decorationView)
}
}
self.phoneView.setup(context: component.context, videoFile: component.videoFile, position: component.position)
var mappedPosition = environment[DemoPageEnvironment.self].position
mappedPosition *= abs(mappedPosition)
let scale: CGFloat = availableSize.width / 390.0
let phoneX = mappedPosition * 50.0 * scale
let phoneY: CGFloat
switch component.position {
case .top:
phoneY = availableSize.height + (-phoneSize.height / 2.0 + 24.0 + 149.0 + abs(mappedPosition) * 24.0) * scale
case .bottom:
phoneY = (-149.0 + phoneSize.height / 2.0 - 24.0 - abs(mappedPosition) * 24.0) * scale
}
let isVisible = environment[DemoPageEnvironment.self].isDisplaying
let isCentral = environment[DemoPageEnvironment.self].isCentral
self.isCentral = isCentral
if let decorationView = self.decorationView {
decorationView.setVisible(isVisible && abs(mappedPosition) < 0.4)
}
self.phoneView.center = CGPoint(x: availableSize.width / 2.0 + phoneX, y: phoneY)
self.phoneView.screenRotation = mappedPosition * -0.7
var perspective = CATransform3DMakeScale(scale, scale, 1.0)
perspective.m34 = mappedPosition / 50.0
self.phoneView.layer.transform = CATransform3DRotate(perspective, 0.1, 0, 1, 0)
if abs(mappedPosition) < .ulpOfOne {
self.phoneView.play()
} else if !isVisible {
self.phoneView.reset()
self.decorationView?.resetAnimation()
}
if let _ = transition.userData(DemoAnimateInTransition.self), abs(mappedPosition) < .ulpOfOne {
let from: CGFloat
switch component.position {
case .top:
from = -200.0
case .bottom:
from = 200.0
}
self.containerView.layer.animateBoundsOriginYAdditive(from: from, to: 0.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
}
return availableSize
}
}
public func makeView() -> View {
return View()
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<DemoPageEnvironment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition)
}
}
private final class VideoDecoration: UniversalVideoDecoration {
public let backgroundNode: ASDisplayNode? = nil
public let contentContainerNode: ASDisplayNode
public let foregroundNode: ASDisplayNode? = nil
private var contentNode: (ASDisplayNode & UniversalVideoContentNode)?
private var validLayout: (size: CGSize, actualSize: CGSize)?
public init() {
self.contentContainerNode = ASDisplayNode()
}
public func updateContentNode(_ contentNode: (UniversalVideoContentNode & ASDisplayNode)?) {
if self.contentNode !== contentNode {
let previous = self.contentNode
self.contentNode = contentNode
if let previous = previous {
if previous.supernode === self.contentContainerNode {
previous.removeFromSupernode()
}
}
if let contentNode = contentNode {
if contentNode.supernode !== self.contentContainerNode {
self.contentContainerNode.addSubnode(contentNode)
if let validLayout = self.validLayout {
contentNode.frame = CGRect(origin: CGPoint(), size: validLayout.size)
contentNode.updateLayout(size: validLayout.size, actualSize: validLayout.actualSize, transition: .immediate)
}
}
}
}
}
public func updateCorners(_ corners: ImageCorners) {
self.contentContainerNode.clipsToBounds = true
if isRoundEqualCorners(corners) {
self.contentContainerNode.cornerRadius = corners.topLeft.radius
} else {
let boundingSize: CGSize = CGSize(width: max(corners.topLeft.radius, corners.bottomLeft.radius) + max(corners.topRight.radius, corners.bottomRight.radius), height: max(corners.topLeft.radius, corners.topRight.radius) + max(corners.bottomLeft.radius, corners.bottomRight.radius))
let size: CGSize = CGSize(width: boundingSize.width + corners.extendedEdges.left + corners.extendedEdges.right, height: boundingSize.height + corners.extendedEdges.top + corners.extendedEdges.bottom)
let arguments = TransformImageArguments(corners: corners, imageSize: size, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets())
guard let context = DrawingContext(size: size, clear: true) else {
return
}
context.withContext { ctx in
ctx.setFillColor(UIColor.black.cgColor)
ctx.fill(arguments.drawingRect)
}
addCorners(context, arguments: arguments)
if let maskImage = context.generateImage() {
let mask = CALayer()
mask.contents = maskImage.cgImage
mask.contentsScale = maskImage.scale
mask.contentsCenter = CGRect(x: max(corners.topLeft.radius, corners.bottomLeft.radius) / maskImage.size.width, y: max(corners.topLeft.radius, corners.topRight.radius) / maskImage.size.height, width: (maskImage.size.width - max(corners.topLeft.radius, corners.bottomLeft.radius) - max(corners.topRight.radius, corners.bottomRight.radius)) / maskImage.size.width, height: (maskImage.size.height - max(corners.topLeft.radius, corners.topRight.radius) - max(corners.bottomLeft.radius, corners.bottomRight.radius)) / maskImage.size.height)
self.contentContainerNode.layer.mask = mask
self.contentContainerNode.layer.mask?.frame = self.contentContainerNode.bounds
}
}
}
public func updateClippingFrame(_ frame: CGRect, completion: (() -> Void)?) {
self.contentContainerNode.layer.animate(from: NSValue(cgRect: self.contentContainerNode.bounds), to: NSValue(cgRect: frame), keyPath: "bounds", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in
})
if let maskLayer = self.contentContainerNode.layer.mask {
maskLayer.animate(from: NSValue(cgRect: self.contentContainerNode.bounds), to: NSValue(cgRect: frame), keyPath: "bounds", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in
})
maskLayer.animate(from: NSValue(cgPoint: maskLayer.position), to: NSValue(cgPoint: CGPoint(x: frame.midX, y: frame.midY)), keyPath: "position", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in
})
}
if let contentNode = self.contentNode {
contentNode.layer.animate(from: NSValue(cgPoint: contentNode.layer.position), to: NSValue(cgPoint: CGPoint(x: frame.midX, y: frame.midY)), keyPath: "position", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in
completion?()
})
}
}
public func updateContentNodeSnapshot(_ snapshot: UIView?) {
}
public func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) {
self.validLayout = (size, actualSize)
let bounds = CGRect(origin: CGPoint(), size: size)
if let backgroundNode = self.backgroundNode {
transition.updateFrame(node: backgroundNode, frame: bounds)
}
if let foregroundNode = self.foregroundNode {
transition.updateFrame(node: foregroundNode, frame: bounds)
}
transition.updateFrame(node: self.contentContainerNode, frame: bounds)
if let maskLayer = self.contentContainerNode.layer.mask {
transition.updateFrame(layer: maskLayer, frame: bounds)
}
if let contentNode = self.contentNode {
transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(), size: size))
contentNode.updateLayout(size: size, actualSize: actualSize, transition: transition)
}
}
public func setStatus(_ status: Signal<MediaPlayerStatus?, NoError>) {
}
public func tap() {
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,280 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import TelegramCore
import AccountContext
import TelegramPresentationData
import UndoUI
import PresentationDataUtils
private struct BoostState {
let level: Int32
let currentLevelBoosts: Int32
let nextLevelBoosts: Int32?
let boosts: Int32
func displayData(peer: EnginePeer, isCurrent: Bool, canBoostAgain: Bool, myBoostCount: Int32, currentMyBoostCount: Int32, replacedBoosts: Int32? = nil) -> (subject: PremiumLimitScreen.Subject, count: Int32) {
var currentLevel = self.level
var nextLevelBoosts = self.nextLevelBoosts
var currentLevelBoosts = self.currentLevelBoosts
var boosts = self.boosts
if let replacedBoosts {
boosts = max(currentLevelBoosts, boosts - replacedBoosts)
}
if currentMyBoostCount > 0 && self.boosts == currentLevelBoosts {
currentLevel = max(0, currentLevel - 1)
nextLevelBoosts = currentLevelBoosts
currentLevelBoosts = max(0, currentLevelBoosts - 1)
}
return (
.storiesChannelBoost(
peer: peer,
boostSubject: .stories,
isCurrent: isCurrent,
level: currentLevel,
currentLevelBoosts: currentLevelBoosts,
nextLevelBoosts: nextLevelBoosts,
link: nil,
myBoostCount: myBoostCount,
canBoostAgain: canBoostAgain
),
boosts
)
}
}
public func PremiumBoostScreen(
context: AccountContext,
contentContext: Any?,
peerId: EnginePeer.Id,
isCurrent: Bool,
status: ChannelBoostStatus?,
myBoostStatus: MyBoostStatus?,
replacedBoosts: (Int32, Int32)? = nil,
forceDark: Bool,
openPeer: @escaping (EnginePeer) -> Void,
presentController: @escaping (ViewController) -> Void,
pushController: @escaping (ViewController) -> Void,
dismissed: @escaping () -> Void
) {
let _ = (context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId),
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)
)
|> deliverOnMainQueue).startStandalone(next: { peer, accountPeer in
guard let peer, let accountPeer, let status else {
return
}
let isPremium = accountPeer.isPremium
var myBoostCount: Int32 = 0
var currentMyBoostCount: Int32 = 0
var availableBoosts: [MyBoostStatus.Boost] = []
var occupiedBoosts: [MyBoostStatus.Boost] = []
if let myBoostStatus {
for boost in myBoostStatus.boosts {
if let boostPeer = boost.peer {
if boostPeer.id == peer.id {
myBoostCount += 1
} else {
occupiedBoosts.append(boost)
}
} else {
availableBoosts.append(boost)
}
}
}
let boosts = max(Int32(status.boosts), myBoostCount)
let initialState = BoostState(level: Int32(status.level), currentLevelBoosts: Int32(status.currentLevelBoosts), nextLevelBoosts: status.nextLevelBoosts.flatMap(Int32.init), boosts: boosts)
let updatedState = Promise<BoostState?>()
updatedState.set(.single(BoostState(level: Int32(status.level), currentLevelBoosts: Int32(status.currentLevelBoosts), nextLevelBoosts: status.nextLevelBoosts.flatMap(Int32.init), boosts: boosts + 1)))
var updateImpl: (() -> Void)?
var dismissImpl: (() -> Void)?
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with({ $0 }))
let canBoostAgain = premiumConfiguration.boostsPerGiftCount > 0
let (initialSubject, initialCount) = initialState.displayData(peer: peer, isCurrent: isCurrent, canBoostAgain: canBoostAgain, myBoostCount: myBoostCount, currentMyBoostCount: 0, replacedBoosts: replacedBoosts?.0)
let controller = PremiumLimitScreen(context: context, subject: initialSubject, count: initialCount, forceDark: forceDark, action: {
let dismiss = false
updateImpl?()
return dismiss
},
openPeer: { peer in
openPeer(peer)
})
pushController(controller)
if let (replacedBoosts, inChannels) = replacedBoosts {
currentMyBoostCount += 1
let (subject, count) = initialState.displayData(peer: peer, isCurrent: isCurrent, canBoostAgain: canBoostAgain, myBoostCount: myBoostCount, currentMyBoostCount: 1, replacedBoosts: nil)
controller.updateSubject(subject, count: count)
Queue.mainQueue().after(0.3) {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let undoController = UndoOverlayController(presentationData: presentationData, content: .image(image: generateTintedImage(image: UIImage(bundleImageName: "Premium/BoostReplaceIcon"), color: .white)!, title: nil, text: presentationData.strings.ReassignBoost_Success(presentationData.strings.ReassignBoost_Boosts(replacedBoosts), presentationData.strings.ReassignBoost_OtherChannels(inChannels)).string, round: false, undoText: nil), elevatedLayout: false, position: .bottom, action: { _ in return true })
controller.present(undoController, in: .current)
}
}
controller.disposed = {
dismissed()
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
updateImpl = { [weak controller] in
if let _ = status.nextLevelBoosts {
if let availableBoost = availableBoosts.first {
currentMyBoostCount += 1
myBoostCount += 1
let _ = (context.engine.peers.applyChannelBoost(peerId: peerId, slots: [availableBoost.slot])
|> deliverOnMainQueue).startStandalone(completed: {
updatedState.set(context.engine.peers.getChannelBoostStatus(peerId: peerId)
|> map { status in
if let status {
return BoostState(level: Int32(status.level), currentLevelBoosts: Int32(status.currentLevelBoosts), nextLevelBoosts: status.nextLevelBoosts.flatMap(Int32.init), boosts: Int32(status.boosts + 1))
} else {
return nil
}
})
})
let _ = (updatedState.get()
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { state in
guard let state else {
return
}
let (subject, count) = state.displayData(peer: peer, isCurrent: isCurrent, canBoostAgain: canBoostAgain, myBoostCount: myBoostCount, currentMyBoostCount: currentMyBoostCount)
controller?.updateSubject(subject, count: count)
})
availableBoosts.removeFirst()
} else if !occupiedBoosts.isEmpty, let myBoostStatus {
if canBoostAgain {
var dismissReplaceImpl: (() -> Void)?
let replaceController = ReplaceBoostScreen(context: context, peerId: peerId, myBoostStatus: myBoostStatus, replaceBoosts: { slots in
var channelIds = Set<EnginePeer.Id>()
for boost in myBoostStatus.boosts {
if slots.contains(boost.slot) {
if let peer = boost.peer {
channelIds.insert(peer.id)
}
}
}
let _ = context.engine.peers.applyChannelBoost(peerId: peerId, slots: slots).startStandalone(completed: {
let _ = combineLatest(
queue: Queue.mainQueue(),
context.engine.peers.getChannelBoostStatus(peerId: peerId),
context.engine.peers.getMyBoostStatus()
).startStandalone(next: { boostStatus, myBoostStatus in
dismissReplaceImpl?()
PremiumBoostScreen(context: context, contentContext: contentContext, peerId: peerId, isCurrent: isCurrent, status: boostStatus, myBoostStatus: myBoostStatus, replacedBoosts: (Int32(slots.count), Int32(channelIds.count)), forceDark: forceDark, openPeer: openPeer, presentController: presentController, pushController: pushController, dismissed: dismissed)
})
})
})
dismissImpl?()
pushController(replaceController)
dismissReplaceImpl = { [weak replaceController] in
replaceController?.dismiss(animated: true)
}
} else if let boost = occupiedBoosts.first, let occupiedPeer = boost.peer {
if let cooldown = boost.cooldownUntil {
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
let timeout = cooldown - currentTime
let valueText = timeIntervalString(strings: presentationData.strings, value: timeout, usage: .afterTime, preferLowerValue: false)
let controller = textAlertController(
sharedContext: context.sharedContext,
updatedPresentationData: nil,
title: presentationData.strings.ChannelBoost_Error_BoostTooOftenTitle,
text: presentationData.strings.ChannelBoost_Error_BoostTooOftenText(valueText).string,
actions: [
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})
],
parseMarkdown: true
)
presentController(controller)
} else {
let replaceController = replaceBoostConfirmationController(context: context, fromPeers: [occupiedPeer], toPeer: peer, commit: {
currentMyBoostCount += 1
myBoostCount += 1
let _ = (context.engine.peers.applyChannelBoost(peerId: peerId, slots: [boost.slot])
|> deliverOnMainQueue).startStandalone(completed: { [weak controller] in
let _ = (updatedState.get()
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { [weak controller] state in
guard let state else {
return
}
let (subject, count) = state.displayData(peer: peer, isCurrent: isCurrent, canBoostAgain: canBoostAgain, myBoostCount: myBoostCount, currentMyBoostCount: currentMyBoostCount)
controller?.updateSubject(subject, count: count)
})
})
})
presentController(replaceController)
}
} else {
dismissImpl?()
}
} else {
if isPremium {
if !canBoostAgain {
dismissImpl?()
} else {
let controller = textAlertController(
sharedContext: context.sharedContext,
updatedPresentationData: nil,
title: presentationData.strings.ChannelBoost_MoreBoosts_Title,
text: presentationData.strings.ChannelBoost_MoreBoosts_Text(peer.compactDisplayTitle, "\(premiumConfiguration.boostsPerGiftCount)").string,
actions: [
TextAlertAction(type: .defaultAction, title: presentationData.strings.ChannelBoost_MoreBoosts_Gift, action: {
dismissImpl?()
Queue.mainQueue().after(0.4) {
let controller = context.sharedContext.makePremiumGiftController(context: context, source: .channelBoost, completion: nil)
pushController(controller)
}
}),
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Close, action: {})
],
actionLayout: .vertical,
parseMarkdown: true
)
presentController(controller)
}
} else {
let controller = textAlertController(
sharedContext: context.sharedContext,
updatedPresentationData: nil,
title: presentationData.strings.ChannelBoost_Error_PremiumNeededTitle,
text: presentationData.strings.ChannelBoost_Error_PremiumNeededText,
actions: [
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}),
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Yes, action: {
dismissImpl?()
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .channelBoost(peerId), forceDark: forceDark, dismissed: nil)
pushController(controller)
})
],
parseMarkdown: true
)
presentController(controller)
}
}
} else {
dismissImpl?()
}
}
dismissImpl = { [weak controller] in
controller?.dismissAnimated()
}
})
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,339 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import MultilineTextComponent
import CheckNode
import Markdown
final class PremiumOptionComponent: CombinedComponent {
let title: String
let subtitle: String
let labelPrice: String
let discount: String
let multiple: Bool
let selected: Bool
let primaryTextColor: UIColor
let secondaryTextColor: UIColor
let accentColor: UIColor
let checkForegroundColor: UIColor
let checkBorderColor: UIColor
init(
title: String,
subtitle: String,
labelPrice: String,
discount: String,
multiple: Bool = false,
selected: Bool,
primaryTextColor: UIColor,
secondaryTextColor: UIColor,
accentColor: UIColor,
checkForegroundColor: UIColor,
checkBorderColor: UIColor
) {
self.title = title
self.subtitle = subtitle
self.labelPrice = labelPrice
self.discount = discount
self.multiple = multiple
self.selected = selected
self.primaryTextColor = primaryTextColor
self.secondaryTextColor = secondaryTextColor
self.accentColor = accentColor
self.checkForegroundColor = checkForegroundColor
self.checkBorderColor = checkBorderColor
}
static func ==(lhs: PremiumOptionComponent, rhs: PremiumOptionComponent) -> Bool {
if lhs.title != rhs.title {
return false
}
if lhs.subtitle != rhs.subtitle {
return false
}
if lhs.labelPrice != rhs.labelPrice {
return false
}
if lhs.discount != rhs.discount {
return false
}
if lhs.multiple != rhs.multiple {
return false
}
if lhs.selected != rhs.selected {
return false
}
if lhs.primaryTextColor != rhs.primaryTextColor {
return false
}
if lhs.secondaryTextColor != rhs.secondaryTextColor {
return false
}
if lhs.accentColor != rhs.accentColor {
return false
}
if lhs.checkForegroundColor != rhs.checkForegroundColor {
return false
}
if lhs.checkBorderColor != rhs.checkBorderColor {
return false
}
return true
}
static var body: Body {
let check = Child(CheckComponent.self)
let title = Child(MultilineTextComponent.self)
let subtitle = Child(MultilineTextComponent.self)
let discountBackground = Child(RoundedRectangle.self)
let discount = Child(MultilineTextComponent.self)
let label = Child(MultilineTextComponent.self)
return { context in
let component = context.component
var insets = UIEdgeInsets(top: 15.0, left: 46.0, bottom: 17.0, right: 16.0)
let label = label.update(
component: MultilineTextComponent(
text: .plain(
NSAttributedString(
string: component.labelPrice,
font: Font.regular(17),
textColor: component.secondaryTextColor
)
),
maximumNumberOfLines: 1
),
availableSize: context.availableSize,
transition: context.transition
)
let title = title.update(
component: MultilineTextComponent(
text: .plain(
NSAttributedString(
string: component.title,
font: Font.regular(17),
textColor: component.primaryTextColor
)
),
maximumNumberOfLines: 1
),
availableSize: CGSize(width: context.availableSize.width - insets.left - insets.right - label.size.width, height: context.availableSize.height),
transition: context.transition
)
let discountSize: CGSize
if !component.discount.isEmpty {
let discount = discount.update(
component: MultilineTextComponent(
text: .plain(
NSAttributedString(
string: component.discount,
font: Font.with(size: 14.0, design: .round, weight: .semibold, traits: []),
textColor: .white
)
),
maximumNumberOfLines: 1
),
availableSize: context.availableSize,
transition: context.transition
)
discountSize = CGSize(width: discount.size.width + 6.0, height: 18.0)
let discountBackground = discountBackground.update(
component: RoundedRectangle(
color: component.accentColor,
cornerRadius: 5.0
),
availableSize: discountSize,
transition: context.transition
)
let discountPosition = CGPoint(x: insets.left + title.size.width + 6.0 + discountSize.width / 2.0, y: insets.top + title.size.height / 2.0 - 2.0)
context.add(discountBackground
.position(discountPosition)
)
context.add(discount
.position(discountPosition)
)
} else {
discountSize = CGSize(width: 0.0, height: 18.0)
}
var spacing: CGFloat = 0.0
var subtitleSize = CGSize()
if !component.subtitle.isEmpty {
spacing = 2.0
let subtitleFont = Font.regular(13)
let subtitleColor = component.secondaryTextColor
let subtitleString = parseMarkdownIntoAttributedString(
component.subtitle,
attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: subtitleFont, textColor: subtitleColor),
bold: MarkdownAttributeSet(font: subtitleFont, textColor: subtitleColor, additionalAttributes: [NSAttributedString.Key.strikethroughStyle.rawValue: NSUnderlineStyle.single.rawValue as NSNumber]),
link: MarkdownAttributeSet(font: subtitleFont, textColor: subtitleColor),
linkAttribute: { _ in return nil }
)
)
let subtitle = subtitle.update(
component: MultilineTextComponent(
text: .plain(subtitleString),
maximumNumberOfLines: 1
),
availableSize: CGSize(width: context.availableSize.width - insets.left - insets.right, height: context.availableSize.height),
transition: context.transition
)
context.add(subtitle
.position(CGPoint(x: insets.left + subtitle.size.width / 2.0, y: insets.top + title.size.height + spacing + subtitle.size.height / 2.0))
)
subtitleSize = subtitle.size
insets.top -= 2.0
insets.bottom -= 2.0
}
let check = check.update(
component: CheckComponent(
theme: CheckComponent.Theme(
backgroundColor: component.accentColor,
strokeColor: component.checkForegroundColor,
borderColor: component.checkBorderColor,
overlayBorder: false,
hasInset: false,
hasShadow: false
),
selected: component.selected
),
availableSize: context.availableSize,
transition: context.transition
)
context.add(title
.position(CGPoint(x: insets.left + title.size.width / 2.0, y: insets.top + title.size.height / 2.0))
)
let size = CGSize(width: context.availableSize.width, height: insets.top + title.size.height + spacing + subtitleSize.height + insets.bottom)
let distance = context.availableSize.width - insets.left - insets.right - label.size.width - subtitleSize.width
let labelY: CGFloat
if distance > 8.0 {
labelY = size.height / 2.0
} else {
labelY = insets.top + title.size.height / 2.0
}
context.add(label
.position(CGPoint(x: context.availableSize.width - insets.right - label.size.width / 2.0, y: labelY))
)
context.add(check
.position(CGPoint(x: 4.0 + check.size.width / 2.0, y: size.height / 2.0))
)
return size
}
}
}
private final class CheckComponent: Component {
struct Theme: Equatable {
public let backgroundColor: UIColor
public let strokeColor: UIColor
public let borderColor: UIColor
public let overlayBorder: Bool
public let hasInset: Bool
public let hasShadow: Bool
public let filledBorder: Bool
public let borderWidth: CGFloat?
public init(backgroundColor: UIColor, strokeColor: UIColor, borderColor: UIColor, overlayBorder: Bool, hasInset: Bool, hasShadow: Bool, filledBorder: Bool = false, borderWidth: CGFloat? = nil) {
self.backgroundColor = backgroundColor
self.strokeColor = strokeColor
self.borderColor = borderColor
self.overlayBorder = overlayBorder
self.hasInset = hasInset
self.hasShadow = hasShadow
self.filledBorder = filledBorder
self.borderWidth = borderWidth
}
var checkNodeTheme: CheckNodeTheme {
return CheckNodeTheme(
backgroundColor: self.backgroundColor,
strokeColor: self.strokeColor,
borderColor: self.borderColor,
overlayBorder: self.overlayBorder,
hasInset: self.hasInset,
hasShadow: self.hasShadow,
filledBorder: self.filledBorder,
borderWidth: self.borderWidth
)
}
}
let theme: Theme
let selected: Bool
init(
theme: Theme,
selected: Bool
) {
self.theme = theme
self.selected = selected
}
static func ==(lhs: CheckComponent, rhs: CheckComponent) -> Bool {
if lhs.theme != rhs.theme {
return false
}
if lhs.selected != rhs.selected {
return false
}
return true
}
final class View: UIView {
private var currentValue: CGFloat?
private var animator: DisplayLinkAnimator?
private var checkLayer: CheckLayer {
return self.layer as! CheckLayer
}
override class var layerClass: AnyClass {
return CheckLayer.self
}
init() {
super.init(frame: CGRect())
}
required init?(coder aDecoder: NSCoder) {
preconditionFailure()
}
func update(component: CheckComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
self.checkLayer.setSelected(component.selected, animated: true)
self.checkLayer.theme = component.theme.checkNodeTheme
return CGSize(width: 22.0, height: 22.0)
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
@@ -0,0 +1,566 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import SwiftSignalKit
import TelegramCore
import Markdown
import TextFormat
import TelegramPresentationData
import ViewControllerComponent
import SheetComponent
import BundleIconComponent
import BalancedTextComponent
import MultilineTextComponent
import SolidRoundedButtonComponent
import LottieComponent
import AccountContext
import GlassBarButtonComponent
private final class SheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let peerId: EnginePeer.Id
let subject: PremiumPrivacyScreen.Subject
let action: () -> Void
let openPremiumIntro: () -> Void
let dismiss: () -> Void
init(
context: AccountContext,
peerId: EnginePeer.Id,
subject: PremiumPrivacyScreen.Subject,
action: @escaping () -> Void,
openPremiumIntro: @escaping () -> Void,
dismiss: @escaping () -> Void
) {
self.context = context
self.peerId = peerId
self.subject = subject
self.action = action
self.openPremiumIntro = openPremiumIntro
self.dismiss = dismiss
}
static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.subject != rhs.subject {
return false
}
return true
}
final class State: ComponentState {
var cachedIconImage: UIImage?
let playOnce = ActionSlot<Void>()
private var didPlayAnimation = false
private var disposable: Disposable?
private(set) var peer: EnginePeer?
init(context: AccountContext, peerId: EnginePeer.Id) {
super.init()
self.disposable = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let self, let peer else {
return
}
self.peer = peer
self.updated()
})
}
deinit {
self.disposable?.dispose()
}
func playAnimationIfNeeded() {
guard !self.didPlayAnimation else {
return
}
self.didPlayAnimation = true
self.playOnce.invoke(Void())
}
}
func makeState() -> State {
return State(context: self.context, peerId: self.peerId)
}
static var body: Body {
let closeButton = Child(GlassBarButtonComponent.self)
let iconBackground = Child(Image.self)
let icon = Child(LottieComponent.self)
let title = Child(BalancedTextComponent.self)
let text = Child(BalancedTextComponent.self)
let actionButton = Child(SolidRoundedButtonComponent.self)
let orLeftLine = Child(Rectangle.self)
let orRightLine = Child(Rectangle.self)
let orText = Child(MultilineTextComponent.self)
let premiumTitle = Child(BalancedTextComponent.self)
let premiumText = Child(BalancedTextComponent.self)
let premiumButton = Child(SolidRoundedButtonComponent.self)
return { context in
let environment = context.environment[EnvironmentType.self]
let component = context.component
let state = context.state
let theme = environment.theme
let strings = environment.strings
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
let textSideInset: CGFloat = 32.0 + environment.safeInsets.left
let titleFont = Font.semibold(20.0)
let textFont = Font.regular(15.0)
let boldTextFont = Font.semibold(15.0)
let textColor = theme.actionSheet.primaryTextColor
let secondaryTextColor = theme.actionSheet.secondaryTextColor
let linkColor = theme.actionSheet.controlAccentColor
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
})
let iconName: String
let titleString: String
let textString: String
let buttonTitle: String
let premiumString: String
let premiumTitleString = strings.PrivacyInfo_UpgradeToPremium_Title
let premiumButtonTitle = strings.PrivacyInfo_UpgradeToPremium_ButtonTitle
let peerName = state.peer?.compactDisplayTitle ?? ""
switch component.subject {
case .presence:
iconName = "PremiumPrivacyPresence"
titleString = strings.PrivacyInfo_ShowLastSeen_Title
textString = strings.PrivacyInfo_ShowLastSeen_Text(peerName).string
buttonTitle = strings.PrivacyInfo_ShowLastSeen_ButtonTitle
premiumString = strings.PrivacyInfo_ShowLastSeen_PremiumInfo(peerName).string
case .readTime:
iconName = "PremiumPrivacyRead"
titleString = strings.PrivacyInfo_ShowReadTime_Title
textString = strings.PrivacyInfo_ShowReadTime_Text(peerName).string
buttonTitle = strings.PrivacyInfo_ShowReadTime_ButtonTitle
premiumString = strings.PrivacyInfo_ShowReadTime_PremiumInfo(peerName).string
}
let spacing: CGFloat = 8.0
var contentSize = CGSize(width: context.availableSize.width, height: 32.0)
let closeButton = closeButton.update(
component: GlassBarButtonComponent(
size: CGSize(width: 40.0, height: 40.0),
backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor,
isDark: theme.overallDarkAppearance,
state: .generic,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: theme.rootController.navigationBar.glassBarButtonForegroundColor
)
)),
action: { _ in
component.dismiss()
}
),
availableSize: CGSize(width: 40.0, height: 40.0),
transition: .immediate
)
context.add(closeButton
.position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0))
)
let iconSize = CGSize(width: 90.0, height: 90.0)
let gradientImage: UIImage
if let current = state.cachedIconImage {
gradientImage = current
} else {
gradientImage = generateFilledCircleImage(diameter: iconSize.width, color: theme.actionSheet.controlAccentColor)!
context.state.cachedIconImage = gradientImage
}
let iconBackground = iconBackground.update(
component: Image(image: gradientImage),
availableSize: iconSize,
transition: .immediate
)
context.add(iconBackground
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + iconBackground.size.height / 2.0))
)
let icon = icon.update(
component: LottieComponent(
content: LottieComponent.AppBundleContent(name: iconName),
playOnce: state.playOnce
),
availableSize: CGSize(width: 70, height: 70),
transition: .immediate
)
context.add(icon
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + iconBackground.size.height / 2.0))
)
contentSize.height += iconSize.height
contentSize.height += spacing + 5.0
let title = title.update(
component: BalancedTextComponent(
text: .plain(NSAttributedString(string: titleString, font: titleFont, textColor: textColor)),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.1
),
availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height),
transition: .immediate
)
context.add(title
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height / 2.0))
)
contentSize.height += title.size.height
contentSize.height += spacing
let text = text.update(
component: BalancedTextComponent(
text: .markdown(text: textString, attributes: markdownAttributes),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.2
),
availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height),
transition: .immediate
)
context.add(text
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + text.size.height / 2.0))
)
contentSize.height += text.size.height
contentSize.height += spacing + 5.0
let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0)
let actionButton = actionButton.update(
component: SolidRoundedButtonComponent(
title: buttonTitle,
theme: SolidRoundedButtonComponent.Theme(
backgroundColor: theme.list.itemCheckColors.fillColor,
backgroundColors: [],
foregroundColor: theme.list.itemCheckColors.foregroundColor
),
font: .bold,
fontSize: 17.0,
height: 52.0,
cornerRadius: 26.0,
gloss: false,
glass: true,
iconName: nil,
animationName: nil,
iconPosition: .left,
action: {
component.action()
component.dismiss()
}
),
availableSize: CGSize(width: context.availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0),
transition: context.transition
)
context.add(actionButton
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + actionButton.size.height / 2.0))
)
contentSize.height += actionButton.size.height
contentSize.height += 22.0
let orText = orText.update(
component: MultilineTextComponent(text: .plain(NSAttributedString(string: strings.ChannelBoost_Or, font: Font.regular(15.0), textColor: secondaryTextColor, paragraphAlignment: .center))),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height),
transition: .immediate
)
context.add(orText
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + orText.size.height / 2.0))
)
let orLeftLine = orLeftLine.update(
component: Rectangle(color: theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.3)),
availableSize: CGSize(width: 90.0, height: 1.0 - UIScreenPixel),
transition: .immediate
)
context.add(orLeftLine
.position(CGPoint(x: context.availableSize.width / 2.0 - orText.size.width / 2.0 - 11.0 - 45.0, y: contentSize.height + orText.size.height / 2.0))
)
let orRightLine = orRightLine.update(
component: Rectangle(color: theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.3)),
availableSize: CGSize(width: 90.0, height: 1.0 - UIScreenPixel),
transition: .immediate
)
context.add(orRightLine
.position(CGPoint(x: context.availableSize.width / 2.0 + orText.size.width / 2.0 + 11.0 + 45.0, y: contentSize.height + orText.size.height / 2.0))
)
contentSize.height += orText.size.height
contentSize.height += 18.0
let premiumTitle = premiumTitle.update(
component: BalancedTextComponent(
text: .plain(NSAttributedString(string: premiumTitleString, font: titleFont, textColor: textColor)),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.1
),
availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height),
transition: .immediate
)
context.add(premiumTitle
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + premiumTitle.size.height / 2.0))
)
contentSize.height += premiumTitle.size.height
contentSize.height += spacing
let premiumText = premiumText.update(
component: BalancedTextComponent(
text: .markdown(text: premiumString, attributes: markdownAttributes),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.2
),
availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height),
transition: .immediate
)
context.add(premiumText
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + premiumText.size.height / 2.0))
)
contentSize.height += premiumText.size.height
contentSize.height += spacing + 5.0
let premiumButton = premiumButton.update(
component: SolidRoundedButtonComponent(
title: premiumButtonTitle,
theme: SolidRoundedButtonComponent.Theme(
backgroundColor: .black,
backgroundColors: [
UIColor(rgb: 0x0077ff),
UIColor(rgb: 0x6b93ff),
UIColor(rgb: 0x8878ff),
UIColor(rgb: 0xe46ace)
],
foregroundColor: .white
),
font: .bold,
fontSize: 17.0,
height: 52.0,
cornerRadius: 26.0,
gloss: false,
glass: true,
iconName: nil,
animationName: nil,
iconPosition: .left,
action: {
component.openPremiumIntro()
component.dismiss()
}
),
availableSize: CGSize(width: context.availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0),
transition: context.transition
)
context.add(premiumButton
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + premiumButton.size.height / 2.0))
)
contentSize.height += premiumButton.size.height
contentSize.height += buttonInsets.bottom
state.playAnimationIfNeeded()
return contentSize
}
}
}
private final class SheetContainerComponent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let peerId: EnginePeer.Id
let subject: PremiumPrivacyScreen.Subject
let action: () -> Void
let openPremiumIntro: () -> Void
init(
context: AccountContext,
peerId: EnginePeer.Id,
subject: PremiumPrivacyScreen.Subject,
action: @escaping () -> Void,
openPremiumIntro: @escaping () -> Void
) {
self.context = context
self.peerId = peerId
self.subject = subject
self.action = action
self.openPremiumIntro = openPremiumIntro
}
static func ==(lhs: SheetContainerComponent, rhs: SheetContainerComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.peerId != rhs.peerId {
return false
}
if lhs.subject != rhs.subject {
return false
}
return true
}
static var body: Body {
let sheet = Child(SheetComponent<EnvironmentType>.self)
let animateOut = StoredActionSlot(Action<Void>.self)
let sheetExternalState = SheetComponent<EnvironmentType>.ExternalState()
return { context in
let environment = context.environment[EnvironmentType.self]
let controller = environment.controller
let sheet = sheet.update(
component: SheetComponent<EnvironmentType>(
content: AnyComponent<EnvironmentType>(SheetContent(
context: context.component.context,
peerId: context.component.peerId,
subject: context.component.subject,
action: context.component.action,
openPremiumIntro: context.component.openPremiumIntro,
dismiss: {
animateOut.invoke(Action { _ in
if let controller = controller() {
controller.dismiss(completion: nil)
}
})
}
)),
style: .glass,
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
followContentSizeChanges: true,
externalState: sheetExternalState,
animateOut: animateOut
),
environment: {
environment
SheetComponentEnvironment(
isDisplaying: environment.value.isVisible,
isCentered: environment.metrics.widthClass == .regular,
hasInputHeight: !environment.inputHeight.isZero,
regularMetricsSize: CGSize(width: 430.0, height: 900.0),
dismiss: { animated in
if animated {
animateOut.invoke(Action { _ in
if let controller = controller() {
controller.dismiss(completion: nil)
}
})
} else {
if let controller = controller() {
controller.dismiss(completion: nil)
}
}
}
)
},
availableSize: context.availableSize,
transition: context.transition
)
context.add(sheet
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
)
if let controller = controller(), !controller.automaticallyControlPresentationContextLayout {
let layout = ContainerViewLayout(
size: context.availableSize,
metrics: environment.metrics,
deviceMetrics: environment.deviceMetrics,
intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: max(environment.safeInsets.bottom, sheetExternalState.contentHeight), right: 0.0),
safeInsets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: 0.0, right: environment.safeInsets.right),
additionalInsets: .zero,
statusBarHeight: environment.statusBarHeight,
inputHeight: nil,
inputHeightIsInteractivellyChanging: false,
inVoiceOver: false
)
controller.presentationContext.containerLayoutUpdated(layout, transition: context.transition.containedViewLayoutTransition)
}
return context.availableSize
}
}
}
public class PremiumPrivacyScreen: ViewControllerComponentContainer {
public enum Subject: Equatable {
case presence
case readTime
}
private let context: AccountContext
private let peerId: EnginePeer.Id
private let subject: PremiumPrivacyScreen.Subject
private var action: (() -> Void)?
private var openPremiumIntro: (() -> Void)?
public init(
context: AccountContext,
peerId: EnginePeer.Id,
subject: PremiumPrivacyScreen.Subject,
action: @escaping () -> Void,
openPremiumIntro: @escaping () -> Void
) {
self.context = context
self.peerId = peerId
self.subject = subject
self.action = action
self.openPremiumIntro = openPremiumIntro
super.init(
context: context,
component: SheetContainerComponent(
context: context,
peerId: peerId,
subject: subject,
action: action,
openPremiumIntro: openPremiumIntro
),
navigationBarAppearance: .none,
statusBarStyle: .ignore,
theme: .default
)
self.navigationPresentation = .flatModal
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func viewDidLoad() {
super.viewDidLoad()
self.view.disablesInteractiveModalDismiss = true
}
public func dismissAnimated() {
if let view = self.node.hostView.findTaggedView(tag: SheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? SheetComponent<ViewControllerComponentContainer.Environment>.View {
view.dismissAnimated()
}
}
}
@@ -0,0 +1,370 @@
import Foundation
import UIKit
import SwiftSignalKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import AppBundle
import AvatarNode
import Markdown
import CheckNode
private func generateBoostIcon(theme: PresentationTheme) -> UIImage? {
if let image = UIImage(bundleImageName: "Premium/AvatarBoost") {
let size = CGSize(width: image.size.width + 4.0, height: image.size.height + 4.0)
return generateImage(size, contextGenerator: { size, context in
let bounds = CGRect(origin: .zero, size: size)
context.clear(bounds)
if let cgImage = image.cgImage {
context.draw(cgImage, in: CGRect(origin: CGPoint(x: 2.0, y: 2.0), size: image.size))
}
let lineWidth = 2.0 - UIScreenPixel
context.setLineWidth(lineWidth)
context.setStrokeColor(theme.actionSheet.opaqueItemBackgroundColor.cgColor)
context.strokeEllipse(in: bounds.insetBy(dx: lineWidth / 2.0 + UIScreenPixel, dy: lineWidth / 2.0 + UIScreenPixel))
}, opaque: false)
}
return nil
}
private final class PreviousBoostNode: ASDisplayNode {
let checkNode: InteractiveCheckNode
let avatarNode: AvatarNode
let labelNode: ImmediateTextNode
var pressed: (PreviousBoostNode) -> Void = { _ in }
init(context: AccountContext, theme: AlertControllerTheme, ptheme: PresentationTheme, peer: EnginePeer, badge: String?) {
self.checkNode = InteractiveCheckNode(theme: CheckNodeTheme(backgroundColor: theme.accentColor, strokeColor: theme.contrastColor, borderColor: theme.controlBorderColor, overlayBorder: false, hasInset: false, hasShadow: false))
self.checkNode.setSelected(false, animated: false)
self.labelNode = ImmediateTextNode()
self.labelNode.maximumNumberOfLines = 4
self.labelNode.isUserInteractionEnabled = true
self.labelNode.attributedText = NSAttributedString(string: peer.compactDisplayTitle, font: Font.semibold(13.0), textColor: theme.primaryColor)
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 13.0))
super.init()
self.addSubnode(self.checkNode)
self.addSubnode(self.avatarNode)
self.addSubnode(self.labelNode)
self.avatarNode.setPeer(context: context, theme: ptheme, peer: peer)
self.checkNode.valueChanged = { [weak self] value in
if let self {
if value {
self.pressed(self)
}
}
}
}
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
let checkSize = CGSize(width: 22.0, height: 22.0)
let condensedSize = CGSize(width: size.width - 76.0, height: size.height)
let avatarSize = CGSize(width: 30.0, height: 30.0)
let labelSize = self.labelNode.updateLayout(condensedSize)
transition.updateFrame(node: self.checkNode, frame: CGRect(origin: CGPoint(x: 12.0, y: -2.0), size: checkSize))
transition.updateFrame(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: 46.0, y: -8.0), size: avatarSize))
transition.updateFrame(node: self.labelNode, frame: CGRect(origin: CGPoint(x: 84.0, y: 0.0), size: labelSize))
return CGSize(width: size.width, height: checkSize.height)
}
func setChecked(_ checked: Bool) {
self.checkNode.setSelected(checked, animated: false)
}
}
private final class ReplaceBoostConfirmationAlertContentNode: AlertContentNode {
private let strings: PresentationStrings
private let text: String
private let textNode: ASTextNode
private let avatarNode: AvatarNode
private let arrowNode: ASImageNode
private let secondAvatarNode: AvatarNode
private let iconNode: ASImageNode
private let actionNodesSeparator: ASDisplayNode
private let actionNodes: [TextAlertContentActionNode]
private let actionVerticalSeparators: [ASDisplayNode]
private var boostNodes: [PreviousBoostNode] = []
private var validLayout: CGSize?
override var dismissOnOutsideTap: Bool {
return self.isUserInteractionEnabled
}
init(context: AccountContext, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, fromPeers: [EnginePeer], toPeer: EnginePeer, text: String, actions: [TextAlertAction]) {
self.strings = strings
self.text = text
self.textNode = ASTextNode()
self.textNode.maximumNumberOfLines = 0
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0))
self.arrowNode = ASImageNode()
self.arrowNode.displaysAsynchronously = false
self.arrowNode.displayWithoutProcessing = true
self.secondAvatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0))
self.iconNode = ASImageNode()
self.iconNode.displaysAsynchronously = false
self.iconNode.image = generateBoostIcon(theme: ptheme)
self.actionNodesSeparator = ASDisplayNode()
self.actionNodesSeparator.isLayerBacked = true
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
return TextAlertContentActionNode(theme: theme, action: action)
}
var actionVerticalSeparators: [ASDisplayNode] = []
if actions.count > 1 {
for _ in 0 ..< actions.count - 1 {
let separatorNode = ASDisplayNode()
separatorNode.isLayerBacked = true
actionVerticalSeparators.append(separatorNode)
}
}
self.actionVerticalSeparators = actionVerticalSeparators
var boostNodes: [PreviousBoostNode] = []
if fromPeers.count > 1 {
for peer in fromPeers {
let boostNode = PreviousBoostNode(context: context, theme: theme, ptheme: ptheme, peer: peer, badge: nil)
if boostNodes.isEmpty {
boostNode.setChecked(true)
}
boostNodes.append(boostNode)
}
}
self.boostNodes = boostNodes
super.init()
self.addSubnode(self.textNode)
self.addSubnode(self.avatarNode)
self.addSubnode(self.arrowNode)
self.addSubnode(self.secondAvatarNode)
self.addSubnode(self.iconNode)
self.addSubnode(self.actionNodesSeparator)
for actionNode in self.actionNodes {
self.addSubnode(actionNode)
}
for separatorNode in self.actionVerticalSeparators {
self.addSubnode(separatorNode)
}
for boostNode in self.boostNodes {
boostNode.pressed = { [weak self] sender in
if let self {
for node in self.boostNodes {
node.setChecked(node === sender)
}
}
}
self.addSubnode(boostNode)
}
self.updateTheme(theme)
self.avatarNode.setPeer(context: context, theme: ptheme, peer: fromPeers.first!)
self.secondAvatarNode.setPeer(context: context, theme: ptheme, peer: toPeer)
}
override func updateTheme(_ theme: AlertControllerTheme) {
self.textNode.attributedText = parseMarkdownIntoAttributedString(self.text, attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor),
bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: theme.primaryColor),
link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor),
linkAttribute: { url in
return ("URL", url)
}
), textAlignment: .center)
self.arrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Peer Info/AlertArrow"), color: theme.secondaryColor)
self.actionNodesSeparator.backgroundColor = theme.separatorColor
for actionNode in self.actionNodes {
actionNode.updateTheme(theme)
}
for separatorNode in self.actionVerticalSeparators {
separatorNode.backgroundColor = theme.separatorColor
}
if let size = self.validLayout {
_ = self.updateLayout(size: size, transition: .immediate)
}
}
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
var size = size
size.width = min(size.width, 270.0)
self.validLayout = size
var origin: CGPoint = CGPoint(x: 0.0, y: 20.0)
let avatarSize = CGSize(width: 60.0, height: 60.0)
self.avatarNode.updateSize(size: avatarSize)
let avatarFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) - 44.0, y: origin.y), size: avatarSize)
transition.updateFrame(node: self.avatarNode, frame: avatarFrame)
if let arrowImage = self.arrowNode.image {
let arrowFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - arrowImage.size.width) / 2.0), y: origin.y + floorToScreenPixels((avatarSize.height - arrowImage.size.height) / 2.0)), size: arrowImage.size)
transition.updateFrame(node: self.arrowNode, frame: arrowFrame)
}
let secondAvatarFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) + 44.0, y: origin.y), size: avatarSize)
transition.updateFrame(node: self.secondAvatarNode, frame: secondAvatarFrame)
if let icon = self.iconNode.image {
let iconFrame = CGRect(origin: CGPoint(x: avatarFrame.maxX + 4.0 - icon.size.width, y: avatarFrame.maxY + 4.0 - icon.size.height), size: icon.size)
transition.updateFrame(node: self.iconNode, frame: iconFrame)
}
origin.y += avatarSize.height + 10.0
var entriesHeight: CGFloat = 0.0
let textSize = self.textNode.measure(CGSize(width: size.width - 32.0, height: size.height))
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize))
origin.y += textSize.height + 10.0
let actionButtonHeight: CGFloat = 44.0
var minActionsWidth: CGFloat = 0.0
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
let actionTitleInsets: CGFloat = 8.0
var effectiveActionLayout = TextAlertContentActionLayout.horizontal
for actionNode in self.actionNodes {
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
effectiveActionLayout = .vertical
}
switch effectiveActionLayout {
case .horizontal:
minActionsWidth += actionTitleSize.width + actionTitleInsets
case .vertical:
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
}
}
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0)
let contentWidth = max(size.width, minActionsWidth)
if !self.boostNodes.isEmpty {
origin.y += 17.0
for boostNode in self.boostNodes {
let boostSize = boostNode.updateLayout(size: size, transition: transition)
transition.updateFrame(node: boostNode, frame: CGRect(origin: CGPoint(x: 36.0, y: origin.y), size: boostSize))
entriesHeight += boostSize.height + 20.0
origin.y += boostSize.height + 20.0
}
}
var actionsHeight: CGFloat = 0.0
switch effectiveActionLayout {
case .horizontal:
actionsHeight = actionButtonHeight
case .vertical:
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
}
let resultSize = CGSize(width: contentWidth, height: avatarSize.height + textSize.height + entriesHeight + actionsHeight + 16.0 + insets.top + insets.bottom)
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
var actionOffset: CGFloat = 0.0
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
var separatorIndex = -1
var nodeIndex = 0
for actionNode in self.actionNodes {
if separatorIndex >= 0 {
let separatorNode = self.actionVerticalSeparators[separatorIndex]
switch effectiveActionLayout {
case .horizontal:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
case .vertical:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
}
}
separatorIndex += 1
let currentActionWidth: CGFloat
switch effectiveActionLayout {
case .horizontal:
if nodeIndex == self.actionNodes.count - 1 {
currentActionWidth = resultSize.width - actionOffset
} else {
currentActionWidth = actionWidth
}
case .vertical:
currentActionWidth = resultSize.width
}
let actionNodeFrame: CGRect
switch effectiveActionLayout {
case .horizontal:
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += currentActionWidth
case .vertical:
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += actionButtonHeight
}
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
nodeIndex += 1
}
return resultSize
}
}
func replaceBoostConfirmationController(context: AccountContext, fromPeers: [EnginePeer], toPeer: EnginePeer, commit: @escaping () -> Void) -> AlertController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let strings = presentationData.strings
let text = strings.ChannelBoost_ReplaceBoost(fromPeers.first!.compactDisplayTitle, toPeer.compactDisplayTitle).string
var dismissImpl: ((Bool) -> Void)?
var contentNode: ReplaceBoostConfirmationAlertContentNode?
let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
dismissImpl?(true)
}), TextAlertAction(type: .defaultAction, title: strings.ChannelBoost_Replace, action: {
dismissImpl?(true)
commit()
})]
contentNode = ReplaceBoostConfirmationAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: strings, fromPeers: fromPeers, toPeer: toPeer, text: text, actions: actions)
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode!)
dismissImpl = { [weak controller] animated in
if animated {
controller?.dismissAnimated()
} else {
controller?.dismiss()
}
}
return controller
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,638 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import ComponentFlow
import TelegramCore
import AccountContext
import ReactionSelectionNode
import TelegramPresentationData
import AccountContext
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import ShimmerEffect
import StickerResources
final class StickersCarouselComponent: Component {
public typealias EnvironmentType = DemoPageEnvironment
let context: AccountContext
let stickers: [TelegramMediaFile]
let tapAction: () -> Void
public init(
context: AccountContext,
stickers: [TelegramMediaFile],
tapAction: @escaping () -> Void
) {
self.context = context
self.stickers = stickers
self.tapAction = tapAction
}
public static func ==(lhs: StickersCarouselComponent, rhs: StickersCarouselComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.stickers != rhs.stickers {
return false
}
return true
}
public final class View: UIView {
private var component: StickersCarouselComponent?
private var node: StickersCarouselNode?
public func update(component: StickersCarouselComponent, availableSize: CGSize, environment: Environment<DemoPageEnvironment>, transition: ComponentTransition) -> CGSize {
let isDisplaying = environment[DemoPageEnvironment.self].isDisplaying
if self.node == nil && !component.stickers.isEmpty {
let node = StickersCarouselNode(
context: component.context,
stickers: component.stickers,
tapAction: component.tapAction
)
self.node = node
self.addSubnode(node)
}
let isFirstTime = self.component == nil
self.component = component
if let node = self.node {
node.setVisible(isDisplaying)
node.frame = CGRect(origin: .zero, size: availableSize)
node.updateLayout(size: availableSize, transition: .immediate)
}
if isFirstTime {
self.node?.animateIn()
}
return availableSize
}
}
public func makeView() -> View {
return View()
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<DemoPageEnvironment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition)
}
}
private let itemSize = CGSize(width: 220.0, height: 220.0)
private class StickerNode: ASDisplayNode {
private let context: AccountContext
private let file: TelegramMediaFile
public var imageNode: TransformImageNode
public var animationNode: AnimatedStickerNode?
public var additionalAnimationNode: AnimatedStickerNode?
private var placeholderNode: StickerShimmerEffectNode
private let disposable = MetaDisposable()
private let effectDisposable = MetaDisposable()
private var setupTimestamp: Double?
init(context: AccountContext, file: TelegramMediaFile, forceIsPremium: Bool) {
self.context = context
self.file = file
self.imageNode = TransformImageNode()
if file.isPremiumSticker || forceIsPremium {
let animationNode = DefaultAnimatedStickerNodeImpl()
//let animationNode = DirectAnimatedStickerNode()
animationNode.automaticallyLoadFirstFrame = true
self.animationNode = animationNode
let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512)
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 240.0, height: 240.0))
let pathPrefix = context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id)
animationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: file.resource, isVideo: file.isVideoSticker), width: Int(fittedDimensions.width * 1.6), height: Int(fittedDimensions.height * 1.6), playbackMode: .loop, mode: .direct(cachePathPrefix: pathPrefix))
self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: context.account.postbox, userLocation: .other, file: file, small: false, size: fittedDimensions))
self.disposable.set(freeMediaFileResourceInteractiveFetched(account: self.context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start())
if let effect = file.videoThumbnails.first {
self.effectDisposable.set(freeMediaFileResourceInteractiveFetched(account: self.context.account, userLocation: .other, fileReference: .standalone(media: file), resource: effect.resource).start())
let source = AnimatedStickerResourceSource(account: self.context.account, resource: effect.resource, fitzModifier: nil)
let additionalAnimationNode: AnimatedStickerNode
additionalAnimationNode = DirectAnimatedStickerNode()
var pathPrefix: String?
pathPrefix = context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(effect.resource.id)
pathPrefix = nil
additionalAnimationNode.setup(source: source, width: Int(fittedDimensions.width * 1.5), height: Int(fittedDimensions.height * 1.5), playbackMode: .loop, mode: .direct(cachePathPrefix: pathPrefix))
self.additionalAnimationNode = additionalAnimationNode
}
} else {
self.animationNode = nil
}
self.placeholderNode = StickerShimmerEffectNode()
super.init()
self.isUserInteractionEnabled = false
self.addSubnode(self.imageNode)
if let animationNode = self.animationNode {
self.addSubnode(animationNode)
}
if let additionalAnimationNode = self.additionalAnimationNode {
self.addSubnode(additionalAnimationNode)
}
self.addSubnode(self.placeholderNode)
var firstTime = true
self.imageNode.imageUpdated = { [weak self] image in
guard let strongSelf = self else {
return
}
if image != nil {
strongSelf.removePlaceholder(animated: !firstTime)
}
firstTime = false
}
if let animationNode = self.animationNode {
animationNode.started = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.imageNode.alpha = 0.0
let current = CACurrentMediaTime()
if let setupTimestamp = strongSelf.setupTimestamp, current - setupTimestamp > 0.3 {
if !strongSelf.placeholderNode.alpha.isZero {
strongSelf.removePlaceholder(animated: true)
}
} else {
strongSelf.removePlaceholder(animated: false)
}
}
}
}
deinit {
self.disposable.dispose()
self.effectDisposable.dispose()
}
private func removePlaceholder(animated: Bool) {
if !animated {
self.placeholderNode.removeFromSupernode()
} else {
self.placeholderNode.alpha = 0.0
self.placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in
self?.placeholderNode.removeFromSupernode()
})
}
}
private var visibility: Bool = false
private var centrality: Bool = false
public func setCentral(_ central: Bool) {
self.centrality = central
self.updatePlayback()
}
public func setVisible(_ visible: Bool) {
self.visibility = visible
self.updatePlayback()
self.setupTimestamp = CACurrentMediaTime()
}
func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
if self.placeholderNode.supernode != nil {
self.placeholderNode.updateAbsoluteRect(rect, within: containerSize)
}
}
private func updatePlayback() {
self.animationNode?.visibility = self.visibility
if let additionalAnimationNode = self.additionalAnimationNode {
let wasVisible = additionalAnimationNode.visibility
let isVisible = self.visibility && self.centrality
if wasVisible && !isVisible {
additionalAnimationNode.alpha = 0.0
additionalAnimationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak additionalAnimationNode] _ in
additionalAnimationNode?.visibility = isVisible
})
} else if isVisible {
additionalAnimationNode.visibility = isVisible
if !wasVisible {
additionalAnimationNode.play(firstFrame: false, fromIndex: 0)
Queue.mainQueue().after(0.05, {
additionalAnimationNode.alpha = 1.0
})
}
}
}
}
public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
let boundingSize = itemSize
if let dimensitons = self.file.dimensions {
let imageSize = dimensitons.cgSize.aspectFitted(boundingSize)
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))()
let imageFrame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: 0.0), size: imageSize)
self.imageNode.frame = imageFrame
if let animationNode = self.animationNode {
animationNode.frame = imageFrame
animationNode.updateLayout(size: imageSize)
if let additionalAnimationNode = self.additionalAnimationNode {
additionalAnimationNode.frame = imageFrame.offsetBy(dx: -imageFrame.width * 0.245 + 21, dy: -1.0).insetBy(dx: -imageFrame.width * 0.245, dy: -imageFrame.height * 0.245)
additionalAnimationNode.updateLayout(size: additionalAnimationNode.frame.size)
}
}
if self.placeholderNode.supernode != nil {
let placeholderFrame = CGRect(origin: CGPoint(x: -10.0, y: 0.0), size: imageSize)
let thumbnailDimensions = PixelDimensions(width: 512, height: 512)
self.placeholderNode.update(backgroundColor: nil, foregroundColor: UIColor(rgb: 0xffffff, alpha: 0.2), shimmeringColor: UIColor(rgb: 0xffffff, alpha: 0.3), data: self.file.immediateThumbnailData, size: placeholderFrame.size, enableEffect: true, imageSize: thumbnailDimensions.cgSize)
self.placeholderNode.frame = placeholderFrame
}
}
}
}
private class StickersCarouselNode: ASDisplayNode, ASScrollViewDelegate {
private let context: AccountContext
private let stickers: [TelegramMediaFile]
private let tapAction: () -> Void
private var itemContainerNodes: [ASDisplayNode] = []
private var itemNodes: [Int: StickerNode] = [:]
private let scrollNode: ASScrollNode
private let tapNode: ASDisplayNode
private var animator: DisplayLinkAnimator?
private var currentPosition: CGFloat = 0.0
private var currentIndex: Int = 0
private var validLayout: CGSize?
private var playingIndices = Set<Int>()
private let positionDelta: Double
private var previousInteractionTimestamp: Double = 0.0
private var timer: SwiftSignalKit.Timer?
init(context: AccountContext, stickers: [TelegramMediaFile], tapAction: @escaping () -> Void) {
self.context = context
self.stickers = stickers
self.tapAction = tapAction
self.scrollNode = ASScrollNode()
self.tapNode = ASDisplayNode()
self.positionDelta = 1.0 / CGFloat(self.stickers.count)
super.init()
self.clipsToBounds = true
self.addSubnode(self.scrollNode)
self.scrollNode.addSubnode(self.tapNode)
for _ in self.stickers {
let containerNode = ASDisplayNode()
containerNode.isUserInteractionEnabled = false
self.addSubnode(containerNode)
self.itemContainerNodes.append(containerNode)
}
}
override func didLoad() {
super.didLoad()
self.scrollNode.view.delegate = self.wrappedScrollViewDelegate
self.scrollNode.view.showsHorizontalScrollIndicator = false
self.scrollNode.view.showsVerticalScrollIndicator = false
self.scrollNode.view.canCancelContentTouches = true
self.tapNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.stickerTapped(_:))))
}
@objc private func stickerTapped(_ gestureRecognizer: UITapGestureRecognizer) {
self.previousInteractionTimestamp = CACurrentMediaTime() + 1.0
let point = gestureRecognizer.location(in: self.view)
let size = self.bounds.size
if point.y > size.height / 3.0 && point.y < size.height - size.height / 3.0 {
self.tapAction()
return
}
guard self.animator == nil, self.scrollStartPosition == nil else {
return
}
guard let index = self.itemContainerNodes.firstIndex(where: { $0.frame.contains(point) }) else {
return
}
self.scrollTo(index, playAnimation: true, immediately: true, duration: 0.4)
}
func animateIn() {
self.scrollTo(1, playAnimation: true, immediately: true, duration: 0.5, clockwise: true)
if self.timer == nil {
self.previousInteractionTimestamp = CACurrentMediaTime()
self.timer = SwiftSignalKit.Timer(timeout: 0.2, repeat: true, completion: { [weak self] in
if let strongSelf = self {
let currentTimestamp = CACurrentMediaTime()
if currentTimestamp > strongSelf.previousInteractionTimestamp + 2.0 {
var nextIndex = strongSelf.currentIndex - 1
if nextIndex < 0 {
nextIndex = strongSelf.stickers.count + nextIndex
}
strongSelf.scrollTo(nextIndex, playAnimation: true, immediately: true, duration: 0.85, clockwise: true)
strongSelf.previousInteractionTimestamp = currentTimestamp
}
}
}, queue: Queue.mainQueue())
self.timer?.start()
}
}
func scrollTo(_ index: Int, playAnimation: Bool, immediately: Bool, duration: Double, clockwise: Bool? = nil) {
guard index >= 0 && index < self.stickers.count else {
return
}
self.currentIndex = index
let delta = self.positionDelta
let startPosition = self.currentPosition
let newPosition = delta * CGFloat(index)
var change = newPosition - startPosition
if let clockwise = clockwise {
if clockwise {
if change > 0.0 {
change = change - 1.0
}
} else {
if change < 0.0 {
change = 1.0 + change
}
}
} else {
if change > 0.5 {
change = change - 1.0
} else if change < -0.5 {
change = 1.0 + change
}
}
if immediately {
self.playSelectedSticker(index: index)
}
self.animator = DisplayLinkAnimator(duration: duration * UIView.animationDurationFactor(), from: 0.0, to: 1.0, update: { [weak self] t in
let t = listViewAnimationCurveSystem(t)
var updatedPosition = startPosition + change * t
while updatedPosition >= 1.0 {
updatedPosition -= 1.0
}
while updatedPosition < 0.0 {
updatedPosition += 1.0
}
self?.currentPosition = updatedPosition
if let size = self?.validLayout {
self?.updateLayout(size: size, transition: .immediate)
}
}, completion: { [weak self] in
self?.animator = nil
if playAnimation && !immediately {
self?.playSelectedSticker(index: index)
}
})
}
private var visibility = false
func setVisible(_ visible: Bool) {
guard self.visibility != visible else {
return
}
self.visibility = visible
if let size = self.validLayout {
self.updateLayout(size: size, transition: .immediate)
}
}
private var ignoreContentOffsetChange = false
private func resetScrollPosition() {
self.scrollStartPosition = nil
self.ignoreContentOffsetChange = true
self.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: 5000.0 - self.scrollNode.frame.height * 0.5)
self.ignoreContentOffsetChange = false
}
func playSelectedSticker(index: Int?) {
let index = index ?? max(0, Int(round(self.currentPosition / self.positionDelta)) % self.stickers.count)
guard !self.playingIndices.contains(index) else {
return
}
for (i, itemNode) in self.itemNodes {
let containerNode = self.itemContainerNodes[i]
let isCentral = i == index
itemNode.setCentral(isCentral)
if !isCentral {
itemNode.setVisible(false)
}
if isCentral {
containerNode.view.superview?.bringSubviewToFront(containerNode.view)
}
}
}
private var scrollStartPosition: (contentOffset: CGFloat, position: CGFloat)?
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
if self.scrollStartPosition == nil {
self.scrollStartPosition = (scrollView.contentOffset.y, self.currentPosition)
}
for (_, itemNode) in self.itemNodes {
itemNode.setCentral(false)
}
}
private let hapticFeedback = HapticFeedback()
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.isTracking {
self.previousInteractionTimestamp = CACurrentMediaTime() + 1.0
}
if let animator = self.animator {
animator.invalidate()
self.animator = nil
}
guard !self.ignoreContentOffsetChange, let (startContentOffset, startPosition) = self.scrollStartPosition else {
return
}
let delta = scrollView.contentOffset.y - startContentOffset
let positionDelta = delta * 0.0005
var updatedPosition = startPosition + positionDelta
while updatedPosition >= 1.0 {
updatedPosition -= 1.0
}
while updatedPosition < 0.0 {
updatedPosition += 1.0
}
self.currentPosition = updatedPosition
let indexDelta = self.positionDelta
let index = max(0, Int(round(self.currentPosition / indexDelta)) % self.stickers.count)
if index != self.currentIndex {
self.currentIndex = index
if self.scrollNode.view.isTracking || self.scrollNode.view.isDecelerating {
self.hapticFeedback.tap()
}
}
if let size = self.validLayout {
self.updateLayout(size: size, transition: .immediate)
}
}
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
guard let (startContentOffset, _) = self.scrollStartPosition, abs(velocity.y) > 0.0 else {
return
}
let delta = self.positionDelta
let scrollDelta = targetContentOffset.pointee.y - startContentOffset
let positionDelta = scrollDelta * 0.0005
let positionCounts = round(positionDelta / delta)
let adjustedPositionDelta = delta * positionCounts
let adjustedScrollDelta = adjustedPositionDelta * 2000.0
targetContentOffset.pointee = CGPoint(x: 0.0, y: startContentOffset + adjustedScrollDelta)
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate {
self.previousInteractionTimestamp = CACurrentMediaTime() + 1.0
self.resetScrollPosition()
let delta = self.positionDelta
let index = max(0, Int(round(self.currentPosition / delta)) % self.stickers.count)
self.scrollTo(index, playAnimation: true, immediately: true, duration: 0.2)
}
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
self.previousInteractionTimestamp = CACurrentMediaTime() + 1.0
self.resetScrollPosition()
self.playSelectedSticker(index: nil)
}
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
self.validLayout = size
self.scrollNode.frame = CGRect(origin: CGPoint(), size: size)
if self.scrollNode.view.contentSize.width.isZero {
self.scrollNode.view.contentSize = CGSize(width: size.width, height: 10000000.0)
self.tapNode.frame = CGRect(origin: CGPoint(), size: self.scrollNode.view.contentSize)
self.resetScrollPosition()
}
let delta = self.positionDelta
let bounds = CGRect(origin: .zero, size: size)
let areaSize = CGSize(width: floor(size.width * 4.0), height: size.height * 2.2)
for i in 0 ..< self.stickers.count {
let containerNode = self.itemContainerNodes[i]
var angle = CGFloat.pi * 0.5 + CGFloat(i) * delta * CGFloat.pi * 2.0 - self.currentPosition * CGFloat.pi * 2.0 - CGFloat.pi * 0.5
if angle < 0.0 {
angle = CGFloat.pi * 2.0 + angle
}
if angle > CGFloat.pi * 2.0 {
angle = angle - CGFloat.pi * 2.0
}
func calculateRelativeAngle(_ angle: CGFloat) -> CGFloat {
var relativeAngle = angle
if relativeAngle > CGFloat.pi {
relativeAngle = (2.0 * CGFloat.pi - relativeAngle) * -1.0
}
return relativeAngle
}
let relativeAngle = calculateRelativeAngle(angle)
let distance = abs(relativeAngle)
let point = CGPoint(
x: cos(angle),
y: sin(angle)
)
let itemFrame = CGRect(origin: CGPoint(x: -size.width - 0.5 * itemSize.width - 30.0 + point.x * areaSize.width * 0.5 - itemSize.width * 0.5, y: size.height * 0.5 + point.y * areaSize.height * 0.5 - itemSize.height * 0.5), size: itemSize)
containerNode.bounds = CGRect(origin: CGPoint(), size: itemFrame.size)
containerNode.position = CGPoint(x: itemFrame.midX, y: itemFrame.midY)
transition.updateTransformScale(node: containerNode, scale: 1.0 - distance * 0.75)
transition.updateAlpha(node: containerNode, alpha: 1.0 - distance * 0.6)
let isVisible = self.visibility && itemFrame.intersects(bounds)
if isVisible {
let itemNode: StickerNode
if let current = self.itemNodes[i] {
itemNode = current
} else {
itemNode = StickerNode(context: self.context, file: self.stickers[i], forceIsPremium: true)
containerNode.addSubnode(itemNode)
self.itemNodes[i] = itemNode
}
itemNode.updateAbsoluteRect(itemFrame, within: size)
itemNode.setVisible(isVisible)
let isCentral = self.scrollNode.view.isTracking || i == self.currentIndex
itemNode.setCentral(isCentral)
itemNode.frame = CGRect(origin: CGPoint(), size: itemFrame.size)
itemNode.updateLayout(size: itemFrame.size, transition: transition)
} else {
if let itemNode = self.itemNodes[i] {
itemNode.removeFromSupernode()
self.itemNodes[i] = nil
}
}
}
}
}
@@ -0,0 +1,700 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import SwiftSignalKit
import TelegramCore
import AccountContext
import MultilineTextComponent
import BlurredBackgroundComponent
import Markdown
import TelegramPresentationData
import BundleIconComponent
import AvatarNode
import AvatarStoryIndicatorComponent
import ScrollComponent
private final class AvatarComponent: Component {
let context: AccountContext
let theme: PresentationTheme
let peer: EnginePeer
init(context: AccountContext, theme: PresentationTheme, peer: EnginePeer) {
self.context = context
self.theme = theme
self.peer = peer
}
static func ==(lhs: AvatarComponent, rhs: AvatarComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.peer != rhs.peer {
return false
}
return true
}
final class View: UIView {
private let avatarNode: AvatarNode
private let indicator = ComponentView<Empty>()
private var component: AvatarComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0))
super.init(frame: frame)
self.addSubnode(self.avatarNode)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: AvatarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let size = CGSize(width: 78.0, height: 78.0)
self.avatarNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - size.width) / 2.0), y: -22.0), size: size)
self.avatarNode.setPeer(
context: component.context,
theme: component.theme,
peer: component.peer,
synchronousLoad: true
)
let colors = [
UIColor(rgb: 0xbb6de8),
UIColor(rgb: 0x738cff),
UIColor(rgb: 0x8f76ff)
]
let indicatorSize = self.indicator.update(
transition: .immediate,
component: AnyComponent(
AvatarStoryIndicatorComponent(
hasUnseen: true,
hasUnseenCloseFriendsItems: false,
hasLiveItems: false,
colors: AvatarStoryIndicatorComponent.Colors(unseenColors: colors, unseenCloseFriendsColors: colors, seenColors: colors),
activeLineWidth: 3.0,
inactiveLineWidth: 3.0,
counters: AvatarStoryIndicatorComponent.Counters(totalCount: 8, unseenCount: 8)
)
),
environment: {},
containerSize: CGSize(width: 78.0, height: 78.0)
)
if let view = self.indicator.view {
if view.superview == nil {
self.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - indicatorSize.width) / 2.0), y: -22.0), size: indicatorSize)
}
return CGSize(width: availableSize.width, height: 122.0)
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private final class ParagraphComponent: CombinedComponent {
let title: String
let titleColor: UIColor
let text: String
let textColor: UIColor
let iconName: String
let iconColor: UIColor
public init(
title: String,
titleColor: UIColor,
text: String,
textColor: UIColor,
iconName: String,
iconColor: UIColor
) {
self.title = title
self.titleColor = titleColor
self.text = text
self.textColor = textColor
self.iconName = iconName
self.iconColor = iconColor
}
static func ==(lhs: ParagraphComponent, rhs: ParagraphComponent) -> Bool {
if lhs.title != rhs.title {
return false
}
if lhs.titleColor != rhs.titleColor {
return false
}
if lhs.text != rhs.text {
return false
}
if lhs.textColor != rhs.textColor {
return false
}
if lhs.iconName != rhs.iconName {
return false
}
if lhs.iconColor != rhs.iconColor {
return false
}
return true
}
static var body: Body {
let title = Child(MultilineTextComponent.self)
let text = Child(MultilineTextComponent.self)
let icon = Child(BundleIconComponent.self)
return { context in
let component = context.component
let leftInset: CGFloat = 64.0
let rightInset: CGFloat = 32.0
let textSideInset: CGFloat = leftInset + 8.0
let spacing: CGFloat = 5.0
let textTopInset: CGFloat = 9.0
let title = title.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(
string: component.title,
font: Font.semibold(15.0),
textColor: component.titleColor,
paragraphAlignment: .natural
)),
horizontalAlignment: .center,
maximumNumberOfLines: 1
),
availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
let textFont = Font.regular(15.0)
let boldTextFont = Font.semibold(15.0)
let textColor = component.textColor
let markdownAttributes = MarkdownAttributes(
body: MarkdownAttributeSet(font: textFont, textColor: textColor),
bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor),
link: MarkdownAttributeSet(font: textFont, textColor: textColor),
linkAttribute: { _ in
return nil
}
)
let text = text.update(
component: MultilineTextComponent(
text: .markdown(text: component.text, attributes: markdownAttributes),
horizontalAlignment: .natural,
maximumNumberOfLines: 0,
lineSpacing: 0.2
),
availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: context.availableSize.height),
transition: .immediate
)
let icon = icon.update(
component: BundleIconComponent(
name: component.iconName,
tintColor: component.iconColor
),
availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height),
transition: .immediate
)
context.add(title
.position(CGPoint(x: textSideInset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0))
)
context.add(text
.position(CGPoint(x: textSideInset + text.size.width / 2.0, y: textTopInset + title.size.height + spacing + text.size.height / 2.0))
)
context.add(icon
.position(CGPoint(x: 47.0, y: textTopInset + 18.0))
)
return CGSize(width: context.availableSize.width, height: textTopInset + title.size.height + text.size.height + 25.0)
}
}
}
private final class StoriesListComponent: CombinedComponent {
typealias EnvironmentType = (Empty, ScrollChildEnvironment)
let context: AccountContext
let theme: PresentationTheme
let topInset: CGFloat
let bottomInset: CGFloat
init(context: AccountContext, theme: PresentationTheme, topInset: CGFloat, bottomInset: CGFloat) {
self.context = context
self.theme = theme
self.topInset = topInset
self.bottomInset = bottomInset
}
static func ==(lhs: StoriesListComponent, rhs: StoriesListComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.topInset != rhs.topInset {
return false
}
if lhs.bottomInset != rhs.bottomInset {
return false
}
return true
}
final class State: ComponentState {
private let context: AccountContext
private var disposable: Disposable?
var limits: EngineConfiguration.UserLimits = .defaultValue
var premiumLimits: EngineConfiguration.UserLimits = .defaultValue
var accountPeer: EnginePeer?
init(context: AccountContext) {
self.context = context
super.init()
self.disposable = (context.engine.data.get(
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false),
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true),
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)
)
|> deliverOnMainQueue).start(next: { [weak self] limits, premiumLimits, accountPeer in
if let strongSelf = self {
strongSelf.limits = limits
strongSelf.premiumLimits = premiumLimits
strongSelf.accountPeer = accountPeer
strongSelf.updated(transition: .immediate)
}
})
}
deinit {
self.disposable?.dispose()
}
}
func makeState() -> State {
return State(context: self.context)
}
static var body: Body {
let list = Child(List<Empty>.self)
return { context in
let theme = context.component.theme
let strings = context.component.context.sharedContext.currentPresentationData.with { $0 }.strings
let colors = [
UIColor(rgb: 0x0088ff),
UIColor(rgb: 0x798aff),
UIColor(rgb: 0xac64f3),
UIColor(rgb: 0xc456ae),
UIColor(rgb: 0xe95d44),
UIColor(rgb: 0xf2822a),
UIColor(rgb: 0xe79519),
UIColor(rgb: 0xe7ad19)
]
let titleColor = theme.list.itemPrimaryTextColor
let textColor = theme.list.itemSecondaryTextColor
var items: [AnyComponentWithIdentity<Empty>] = []
if let accountPeer = context.state.accountPeer {
items.append(
AnyComponentWithIdentity(
id: "avatar",
component: AnyComponent(AvatarComponent(
context: context.component.context,
theme: theme,
peer: accountPeer
))
)
)
}
items.append(
AnyComponentWithIdentity(
id: "order",
component: AnyComponent(ParagraphComponent(
title: strings.Premium_Stories_Order_Title,
titleColor: titleColor,
text: strings.Premium_Stories_Order_Text,
textColor: textColor,
iconName: "Premium/Stories/Order",
iconColor: colors[0]
))
)
)
items.append(
AnyComponentWithIdentity(
id: "stealth",
component: AnyComponent(ParagraphComponent(
title: strings.Premium_Stories_Stealth_Title,
titleColor: titleColor,
text: strings.Premium_Stories_Stealth_Text,
textColor: textColor,
iconName: "Premium/Stories/Stealth",
iconColor: colors[1]
))
)
)
items.append(
AnyComponentWithIdentity(
id: "quality",
component: AnyComponent(ParagraphComponent(
title: strings.Premium_Stories_Quality_Title,
titleColor: titleColor,
text: strings.Premium_Stories_Quality_Text,
textColor: textColor,
iconName: "Premium/Stories/Quality",
iconColor: colors[2]
))
)
)
items.append(
AnyComponentWithIdentity(
id: "views",
component: AnyComponent(ParagraphComponent(
title: strings.Premium_Stories_Views_Title,
titleColor: titleColor,
text: strings.Premium_Stories_Views_Text,
textColor: textColor,
iconName: "Premium/Stories/Views",
iconColor: colors[3]
))
)
)
items.append(
AnyComponentWithIdentity(
id: "expiration",
component: AnyComponent(ParagraphComponent(
title: strings.Premium_Stories_Expiration_Title,
titleColor: titleColor,
text: strings.Premium_Stories_Expiration_Text,
textColor: textColor,
iconName: "Premium/Stories/Expire",
iconColor: colors[4]
))
)
)
items.append(
AnyComponentWithIdentity(
id: "save",
component: AnyComponent(ParagraphComponent(
title: strings.Premium_Stories_Save_Title,
titleColor: titleColor,
text: strings.Premium_Stories_Save_Text,
textColor: textColor,
iconName: "Premium/Stories/Save",
iconColor: colors[5]
))
)
)
items.append(
AnyComponentWithIdentity(
id: "captions",
component: AnyComponent(ParagraphComponent(
title: strings.Premium_Stories_Captions_Title,
titleColor: titleColor,
text: strings.Premium_Stories_Captions_Text,
textColor: textColor,
iconName: "Premium/Stories/Caption",
iconColor: colors[6]
))
)
)
items.append(
AnyComponentWithIdentity(
id: "format",
component: AnyComponent(ParagraphComponent(
title: strings.Premium_Stories_Format_Title,
titleColor: titleColor,
text: strings.Premium_Stories_Format_Text,
textColor: textColor,
iconName: "Premium/Stories/Format",
iconColor: colors[7]
))
)
)
let list = list.update(
component: List(items),
availableSize: CGSize(width: context.availableSize.width, height: 10000.0),
transition: context.transition
)
let contentHeight = context.component.topInset + list.size.height + context.component.bottomInset
context.add(list
.position(CGPoint(x: list.size.width / 2.0, y: context.component.topInset + list.size.height / 2.0))
)
return CGSize(width: context.availableSize.width, height: contentHeight)
}
}
}
struct PageNeighbors: Equatable {
var leftIsList: Bool
var rightIsList: Bool
}
final class StoriesPageComponent: CombinedComponent {
typealias EnvironmentType = DemoPageEnvironment
let context: AccountContext
let theme: PresentationTheme
let neighbors: PageNeighbors
let bottomInset: CGFloat
let updatedBottomAlpha: (CGFloat) -> Void
let updatedDismissOffset: (CGFloat) -> Void
let updatedIsDisplaying: (Bool) -> Void
init(context: AccountContext, theme: PresentationTheme, neighbors: PageNeighbors, bottomInset: CGFloat, updatedBottomAlpha: @escaping (CGFloat) -> Void, updatedDismissOffset: @escaping (CGFloat) -> Void, updatedIsDisplaying: @escaping (Bool) -> Void) {
self.context = context
self.theme = theme
self.neighbors = neighbors
self.bottomInset = bottomInset
self.updatedBottomAlpha = updatedBottomAlpha
self.updatedDismissOffset = updatedDismissOffset
self.updatedIsDisplaying = updatedIsDisplaying
}
static func ==(lhs: StoriesPageComponent, rhs: StoriesPageComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.neighbors != rhs.neighbors {
return false
}
if lhs.bottomInset != rhs.bottomInset {
return false
}
return true
}
final class State: ComponentState {
let updateBottomAlpha: (CGFloat) -> Void
let updateDismissOffset: (CGFloat) -> Void
let updatedIsDisplaying: (Bool) -> Void
var resetScroll: ActionSlot<CGPoint?>?
var topContentOffset: CGFloat = 0.0
var bottomContentOffset: CGFloat = 100.0 {
didSet {
self.updateAlpha()
}
}
var position: CGFloat? {
didSet {
self.updateAlpha()
}
}
var isDisplaying = false {
didSet {
if oldValue != self.isDisplaying {
self.updatedIsDisplaying(self.isDisplaying)
if !self.isDisplaying {
self.resetScroll?.invoke(nil)
}
}
}
}
var neighbors = PageNeighbors(leftIsList: false, rightIsList: false)
init(updateBottomAlpha: @escaping (CGFloat) -> Void, updateDismissOffset: @escaping (CGFloat) -> Void, updateIsDisplaying: @escaping (Bool) -> Void) {
self.updateBottomAlpha = updateBottomAlpha
self.updateDismissOffset = updateDismissOffset
self.updatedIsDisplaying = updateIsDisplaying
super.init()
}
func updateAlpha() {
var dismissToLeft = false
if let position = self.position, position > 0.0 {
dismissToLeft = true
}
var dismissPosition = min(1.0, abs(self.position ?? 0.0) / 1.3333)
var position = min(1.0, abs(self.position ?? 0.0))
if position > 0.001, (dismissToLeft && self.neighbors.leftIsList) || (!dismissToLeft && self.neighbors.rightIsList) {
dismissPosition = 0.0
position = 1.0
}
self.updateDismissOffset(dismissPosition)
let verticalPosition = 1.0 - min(30.0, self.bottomContentOffset) / 30.0
let backgroundAlpha: CGFloat = max(position, verticalPosition)
self.updateBottomAlpha(backgroundAlpha)
}
}
func makeState() -> State {
return State(updateBottomAlpha: self.updatedBottomAlpha, updateDismissOffset: self.updatedDismissOffset, updateIsDisplaying: self.updatedIsDisplaying)
}
static var body: Body {
let background = Child(Rectangle.self)
let scroll = Child(ScrollComponent<Empty>.self)
let topPanel = Child(BlurredBackgroundComponent.self)
let topSeparator = Child(Rectangle.self)
let title = Child(MultilineTextComponent.self)
let resetScroll = ActionSlot<CGPoint?>()
return { context in
let state = context.state
let environment = context.environment[DemoPageEnvironment.self].value
state.neighbors = context.component.neighbors
state.resetScroll = resetScroll
state.position = environment.position
state.isDisplaying = environment.isDisplaying
let theme = context.component.theme
let strings = context.component.context.sharedContext.currentPresentationData.with { $0 }.strings
let topInset: CGFloat = 56.0
let scroll = scroll.update(
component: ScrollComponent<Empty>(
content: AnyComponent(
StoriesListComponent(
context: context.component.context,
theme: theme,
topInset: topInset,
bottomInset: context.component.bottomInset + 110.0
)
),
contentInsets: UIEdgeInsets(top: topInset, left: 0.0, bottom: 0.0, right: 0.0),
contentOffsetUpdated: { [weak state] topContentOffset, bottomContentOffset in
state?.topContentOffset = topContentOffset
state?.bottomContentOffset = bottomContentOffset
Queue.mainQueue().justDispatch {
state?.updated(transition: .immediate)
}
},
contentOffsetWillCommit: { _ in },
resetScroll: resetScroll
),
availableSize: context.availableSize,
transition: context.transition
)
let background = background.update(
component: Rectangle(color: theme.overallDarkAppearance ? theme.list.blocksBackgroundColor : theme.list.plainBackgroundColor),
availableSize: scroll.size,
transition: context.transition
)
context.add(background
.position(CGPoint(x: context.availableSize.width / 2.0, y: background.size.height / 2.0))
)
context.add(scroll
.position(CGPoint(x: context.availableSize.width / 2.0, y: scroll.size.height / 2.0))
)
let topPanel = topPanel.update(
component: BlurredBackgroundComponent(
color: theme.rootController.navigationBar.blurredBackgroundColor
),
availableSize: CGSize(width: context.availableSize.width, height: topInset),
transition: context.transition
)
let topSeparator = topSeparator.update(
component: Rectangle(
color: theme.rootController.navigationBar.separatorColor
),
availableSize: CGSize(width: context.availableSize.width, height: UIScreenPixel),
transition: context.transition
)
let title = title.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(string: strings.Premium_Stories_Title, font: Font.semibold(20.0), textColor: theme.rootController.navigationBar.primaryTextColor)),
horizontalAlignment: .center,
truncationType: .end,
maximumNumberOfLines: 1
),
availableSize: context.availableSize,
transition: context.transition
)
let topPanelAlpha: CGFloat
if state.topContentOffset > 78.0 {
topPanelAlpha = min(30.0, state.topContentOffset - 78.0) / 30.0
} else {
topPanelAlpha = 0.0
}
context.add(topPanel
.position(CGPoint(x: context.availableSize.width / 2.0, y: topPanel.size.height / 2.0))
.opacity(topPanelAlpha)
)
context.add(topSeparator
.position(CGPoint(x: context.availableSize.width / 2.0, y: topPanel.size.height))
.opacity(topPanelAlpha)
)
let titleTopOriginY = topPanel.size.height / 2.0
let titleBottomOriginY: CGFloat = 144.0
let titleOriginDelta = titleTopOriginY - titleBottomOriginY
let fraction = min(1.0, state.topContentOffset / abs(titleOriginDelta))
let titleOriginY: CGFloat = titleBottomOriginY + fraction * titleOriginDelta
let titleScale = 1.0 - max(0.0, fraction * 0.2)
context.add(title
.position(CGPoint(x: context.availableSize.width / 2.0, y: titleOriginY))
.scale(titleScale)
)
return scroll.size
}
}
}
@@ -0,0 +1,301 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import LegacyComponents
import ItemListUI
import PresentationDataUtils
final class SubscriptionsCountItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let strings: PresentationStrings
let value: Int32
let values: [Int32]
let sectionId: ItemListSectionId
let updated: (Int32) -> Void
init(theme: PresentationTheme, strings: PresentationStrings, value: Int32, values: [Int32], sectionId: ItemListSectionId, updated: @escaping (Int32) -> Void) {
self.theme = theme
self.strings = strings
self.value = value
self.values = values
self.sectionId = sectionId
self.updated = updated
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = SubscriptionsCountItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? SubscriptionsCountItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
}
private final class SubscriptionsCountItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private let textNodes: [TextNode]
private var sliderView: TGPhotoEditorSliderView?
private var item: SubscriptionsCountItem?
private var layoutParams: ListViewItemLayoutParams?
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.textNodes = (0 ..< 10).map { _ -> TextNode in
let textNode = TextNode()
textNode.isUserInteractionEnabled = false
textNode.displaysAsynchronously = false
return textNode
}
super.init(layerBacked: false, dynamicBounce: false)
self.textNodes.forEach(self.addSubnode)
}
func updateSliderView() {
if let sliderView = self.sliderView, let item = self.item {
sliderView.maximumValue = CGFloat(item.values.count - 1)
sliderView.positionsCount = item.values.count
var value: Int32 = 0
for i in 0 ..< item.values.count {
if item.values[i] >= item.value {
value = Int32(i)
break
}
}
sliderView.value = CGFloat(value)
sliderView.isUserInteractionEnabled = true
sliderView.alpha = 1.0
sliderView.layer.allowsGroupOpacity = false
}
}
override func didLoad() {
super.didLoad()
let sliderView = TGPhotoEditorSliderView()
sliderView.enablePanHandling = true
sliderView.trackCornerRadius = 2.0
sliderView.lineSize = 4.0
sliderView.dotSize = 8.0
sliderView.minimumValue = 0.0
sliderView.maximumValue = 6.0
sliderView.startValue = 0.0
sliderView.positionsCount = 7
sliderView.useLinesForPositions = true
sliderView.disablesInteractiveTransitionGestureRecognizer = true
if let item = self.item, let params = self.layoutParams {
var value: Int32 = 0
for i in 0 ..< item.values.count {
if item.values[i] >= item.value {
value = Int32(i)
break
}
}
sliderView.value = CGFloat(value)
sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor
sliderView.backColor = item.theme.list.itemSwitchColors.frameColor
sliderView.startColor = item.theme.list.itemSwitchColors.frameColor
sliderView.trackColor = item.theme.list.itemAccentColor
sliderView.knobImage = PresentationResourcesItemList.knobImage(item.theme)
sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: 37.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 15.0 * 2.0, height: 44.0))
sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX)
}
self.view.addSubview(sliderView)
sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged)
self.sliderView = sliderView
}
func asyncLayout() -> (_ item: SubscriptionsCountItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentItem = self.item
let makeTextLayouts = self.textNodes.map(TextNode.asyncLayout)
return { item, params, neighbors in
var themeUpdated = false
if currentItem?.theme !== item.theme {
themeUpdated = true
}
let contentSize: CGSize
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
var textLayoutAndApply: [(TextNodeLayout, () -> TextNode)] = []
for i in 0 ..< item.values.count {
let value = item.values[i]
let valueString: String = "\(value)"
let (textLayout, textApply) = makeTextLayouts[i](TextNodeLayoutArguments(attributedString: NSAttributedString(string: valueString, font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
textLayoutAndApply.append((textLayout, textApply))
}
contentSize = CGSize(width: params.width, height: 88.0)
insets = itemListNeighborsGroupedInsets(neighbors, params)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.layoutParams = params
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = 0.0
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
for (_, apply) in textLayoutAndApply {
let _ = apply()
}
let textNodes: [(TextNode, CGSize)] = textLayoutAndApply.map { layout, apply -> (TextNode, CGSize) in
let node = apply()
return (node, layout.size)
}
let delta = (params.width - params.leftInset - params.rightInset - 18.0 * 2.0) / CGFloat(max(item.values.count - 1, 1))
for i in 0 ..< strongSelf.textNodes.count {
guard i < item.values.count else {
strongSelf.textNodes[i].isHidden = true
continue
}
let (textNode, textSize) = textNodes[i]
textNode.isHidden = false
var position = params.leftInset + 18.0 + delta * CGFloat(i)
if i == textNodes.count - 1 {
position -= textSize.width / 2.0 + 2.0
} else if i > 0 {
position -= textSize.width / 2.0
}
textNode.frame = CGRect(origin: CGPoint(x: position, y: 15.0), size: textSize)
}
if let sliderView = strongSelf.sliderView {
if themeUpdated {
sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor
sliderView.backColor = item.theme.list.itemSwitchColors.frameColor
sliderView.trackColor = item.theme.list.itemAccentColor
sliderView.knobImage = PresentationResourcesItemList.knobImage(item.theme)
}
sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: 37.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 15.0 * 2.0, height: 44.0))
sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX)
strongSelf.updateSliderView()
}
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
@objc func sliderValueChanged() {
guard let sliderView = self.sliderView, let item = self.item else {
return
}
let value = Int(sliderView.value)
if value >= 0 && value < item.values.count {
self.item?.updated(item.values[value])
}
}
}
@@ -0,0 +1,86 @@
import Foundation
import UIKit
import SceneKit
import Display
import AppBundle
import SwiftSignalKit
import PremiumStarComponent
private let sceneVersion: Int = 1
final class SwirlStarsView: UIView, PhoneDemoDecorationView {
private let sceneView: SCNView
private var particles: SCNNode?
override init(frame: CGRect) {
self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size))
self.sceneView.backgroundColor = .clear
if let scene = loadCompressedScene(name: "swirl", version: sceneVersion) {
self.sceneView.scene = scene
}
self.sceneView.isUserInteractionEnabled = false
self.sceneView.preferredFramesPerSecond = 60
super.init(frame: frame)
self.alpha = 0.0
self.addSubview(self.sceneView)
self.particles = self.sceneView.scene?.rootNode.childNode(withName: "particles", recursively: false)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.particles = nil
}
func setVisible(_ visible: Bool) {
if visible, let particles = self.particles, particles.parent == nil {
self.sceneView.scene?.rootNode.addChildNode(particles)
}
self.setupAnimations()
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear)
transition.updateAlpha(layer: self.layer, alpha: visible ? 0.6 : 0.0, completion: { [weak self] finished in
if let strongSelf = self, finished && !visible && strongSelf.particles?.parent != nil {
strongSelf.particles?.removeFromParentNode()
if let node = strongSelf.sceneView.scene?.rootNode.childNode(withName: "star", recursively: false) {
node.removeAllAnimations()
}
}
})
}
func setupAnimations() {
guard let node = self.sceneView.scene?.rootNode.childNode(withName: "star", recursively: false), node.animationKeys.isEmpty else {
return
}
let initial = node.eulerAngles
let target = SCNVector3(x: node.eulerAngles.x + .pi * 2.0, y: node.eulerAngles.y, z: node.eulerAngles.z)
let animation = CABasicAnimation(keyPath: "eulerAngles")
animation.fromValue = NSValue(scnVector3: initial)
animation.toValue = NSValue(scnVector3: target)
animation.duration = 1.5
animation.timingFunction = CAMediaTimingFunction(name: .linear)
animation.fillMode = .forwards
animation.repeatCount = .infinity
node.addAnimation(animation, forKey: "rotation")
}
func resetAnimation() {
}
override func layoutSubviews() {
super.layoutSubviews()
self.sceneView.frame = CGRect(origin: .zero, size: self.frame.size)
}
}
@@ -0,0 +1,267 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import CheckNode
final class TodoChecksView: UIView, PhoneDemoDecorationView {
private struct Particle {
var id: Int64
var trackIndex: Int
var position: CGPoint
var scale: CGFloat
var alpha: CGFloat
var direction: CGPoint
var velocity: CGFloat
var rotation: CGFloat
var currentTime: CGFloat
var lifeTime: CGFloat
var checkTime: CGFloat?
var didSetup: Bool = false
init(
trackIndex: Int,
position: CGPoint,
scale: CGFloat,
alpha: CGFloat,
direction: CGPoint,
velocity: CGFloat,
rotation: CGFloat,
currentTime: CGFloat,
lifeTime: CGFloat,
checkTime: CGFloat?
) {
self.id = Int64.random(in: 0 ..< .max)
self.trackIndex = trackIndex
self.position = position
self.scale = scale
self.alpha = alpha
self.direction = direction
self.velocity = velocity
self.rotation = rotation
self.currentTime = currentTime
self.lifeTime = lifeTime
self.checkTime = checkTime
}
mutating func update(deltaTime: CGFloat) {
var position = self.position
position.x += self.direction.x * self.velocity * deltaTime
position.y += self.direction.y * self.velocity * deltaTime
self.position = position
self.currentTime += deltaTime
}
mutating func setup() {
self.didSetup = true
}
}
private final class ParticleSet {
private let size: CGSize
var particles: [Particle] = []
init(size: CGSize, preAdvance: Bool) {
self.size = size
self.generateParticles(preAdvance: preAdvance)
}
private func generateParticles(preAdvance: Bool) {
let maxDirections = 16
if self.particles.count < maxDirections {
var allTrackIndices: [Int] = Array(repeating: 0, count: maxDirections)
for i in 0 ..< maxDirections {
allTrackIndices[i] = i
}
var takenIndexCount = 0
for particle in self.particles {
allTrackIndices[particle.trackIndex] = -1
takenIndexCount += 1
}
var availableTrackIndices: [Int] = []
availableTrackIndices.reserveCapacity(maxDirections - takenIndexCount)
for index in allTrackIndices {
if index != -1 {
availableTrackIndices.append(index)
}
}
if !availableTrackIndices.isEmpty {
availableTrackIndices.shuffle()
for takeIndex in availableTrackIndices {
let directionIndex = takeIndex
let angle: CGFloat
if directionIndex < 8 {
angle = (CGFloat(directionIndex) / 5.0 - 0.5) * 2.0 * (CGFloat.pi / 4.0)
} else {
angle = CGFloat.pi + (CGFloat(directionIndex - 6) / 5.0 - 0.5) * 2.0 * (CGFloat.pi / 4.0)
}
let lifeTimeMultiplier = 1.0
let scale = 1.0
let direction = CGPoint(x: cos(angle), y: sin(angle))
let velocity = CGFloat.random(in: 18.0 ..< 22.0)
let lifeTime = CGFloat.random(in: 3.2 ... 4.2)
var position = CGPoint(x: self.size.width / 2.0, y: self.size.height / 2.0 + 40.0)
var initialOffset: CGFloat = 0.5
if preAdvance {
initialOffset = CGFloat.random(in: 0.7 ... 0.7)
} else {
initialOffset = CGFloat.random(in: 0.60 ... 0.72)
}
position.x += direction.x * initialOffset * 250.0
position.y += direction.y * initialOffset * 330.0
var checkTime: CGFloat?
let p = CGFloat.random(in: 0.0 ... 1.0)
if p < 0.2 {
checkTime = 0.0
} else if p < 0.6 {
checkTime = 1.2 + CGFloat.random(in: 0.1 ... 0.6)
}
let particle = Particle(
trackIndex: directionIndex,
position: position,
scale: scale,
alpha: 0.3,
direction: direction,
velocity: velocity,
rotation: CGFloat.random(in: -0.18 ... 0.2),
currentTime: 0.0,
lifeTime: lifeTime * lifeTimeMultiplier,
checkTime: checkTime
)
self.particles.append(particle)
}
}
}
}
func update(deltaTime: CGFloat) {
for i in (0 ..< self.particles.count).reversed() {
self.particles[i].update(deltaTime: deltaTime)
if self.particles[i].currentTime > self.particles[i].lifeTime {
self.particles.remove(at: i)
}
}
self.generateParticles(preAdvance: false)
}
}
private var displayLink: SharedDisplayLinkDriver.Link?
private var particleSet: ParticleSet?
private var particleLayers: [CheckLayer] = []
private var particleMap: [Int64: CheckLayer] = [:]
private var size: CGSize?
private let large: Bool = false
override init(frame: CGRect) {
super.init(frame: frame)
self.particleSet = ParticleSet(size: frame.size, preAdvance: false)
self.displayLink = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] delta in
self?.update(deltaTime: CGFloat(delta))
})
}
required init?(coder: NSCoder) {
preconditionFailure()
}
fileprivate func update(size: CGSize) {
self.size = size
}
private func update(deltaTime: CGFloat) {
guard let particleSet = self.particleSet else {
return
}
particleSet.update(deltaTime: deltaTime)
var validIds = Set<Int64>()
for i in 0 ..< particleSet.particles.count {
validIds.insert(particleSet.particles[i].id)
}
for id in self.particleMap.keys {
if !validIds.contains(id) {
self.particleMap[id]?.isHidden = true
self.particleMap.removeValue(forKey: id)
}
}
for i in 0 ..< particleSet.particles.count {
let particle = particleSet.particles[i]
let particleLayer: CheckLayer
if let assignedLayer = self.particleMap[particle.id] {
particleLayer = assignedLayer
} else {
if i < self.particleLayers.count, let availableLayer = self.particleLayers.first(where: { $0.isHidden }) {
particleLayer = availableLayer
particleLayer.isHidden = false
} else {
particleLayer = CheckLayer()
particleLayer.animateScale = false
particleLayer.theme = CheckNodeTheme(backgroundColor: .white, strokeColor: .clear, borderColor: .white, overlayBorder: false, hasInset: false, hasShadow: false)
particleLayer.bounds = CGRect(origin: CGPoint(), size: CGSize(width: 22.0, height: 22.0))
self.particleLayers.append(particleLayer)
self.layer.addSublayer(particleLayer)
}
self.particleMap[particle.id] = particleLayer
}
if !particle.didSetup {
particleLayer.setSelected(false, animated: false)
particleSet.particles[i].setup()
}
particleLayer.position = particle.position
particleLayer.opacity = Float(particle.alpha)
let particleScale = min(1.0, particle.currentTime / 0.3) * min(1.0, (particle.lifeTime - particle.currentTime) / 0.2) * particle.scale
var transform = CATransform3DMakeScale(particleScale, particleScale, 1.0)
transform = CATransform3DRotate(transform, particle.rotation, 0.0, 0.0, 1.0)
particleLayer.transform = transform
if let checkTime = particle.checkTime, particle.currentTime >= checkTime, !particleLayer.selected {
particleLayer.setSelected(true, animated: true)
}
}
}
private var visible = false
func setVisible(_ visible: Bool) {
guard self.visible != visible else {
return
}
self.visible = visible
self.displayLink?.isPaused = !visible
// let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear)
// transition.updateAlpha(layer: self.containerView.layer, alpha: visible ? 1.0 : 0.0, completion: { [weak self] finished in
// if let strongSelf = self, finished && !visible && !strongSelf.visible {
// for view in strongSelf.containerView.subviews {
// view.removeFromSuperview()
// }
// }
// })
}
func resetAnimation() {
}
}