mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Various improvements
This commit is contained in:
parent
455a3f89d5
commit
4629b403cc
@ -7957,3 +7957,6 @@ Sorry for the inconvenience.";
|
|||||||
"KeyCommand.ExitFullscreen" = "Exit Fullscreen";
|
"KeyCommand.ExitFullscreen" = "Exit Fullscreen";
|
||||||
|
|
||||||
"StickerPacksSettings.SuggestAnimatedEmoji" = "Suggest Animated Emoji";
|
"StickerPacksSettings.SuggestAnimatedEmoji" = "Suggest Animated Emoji";
|
||||||
|
|
||||||
|
"Premium.Annual" = "Annual";
|
||||||
|
"Premium.Monthly" = "Monthly";
|
||||||
|
@ -2,10 +2,33 @@ import Foundation
|
|||||||
import CoreText
|
import CoreText
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
|
|
||||||
|
extension Character {
|
||||||
|
var isSimpleEmoji: Bool {
|
||||||
|
guard let firstScalar = unicodeScalars.first else { return false }
|
||||||
|
if #available(iOS 10.2, *) {
|
||||||
|
return (firstScalar.properties.isEmoji && firstScalar.value > 0x238C) || firstScalar.isEmoji
|
||||||
|
} else {
|
||||||
|
return firstScalar.isEmoji
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isCombinedIntoEmoji: Bool {
|
||||||
|
if #available(iOS 10.2, *) {
|
||||||
|
return self.unicodeScalars.count > 1 && self.unicodeScalars.first?.properties.isEmoji ?? false
|
||||||
|
} else {
|
||||||
|
return self.unicodeScalars.count > 1 && self.unicodeScalars.first?.isEmoji ?? false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isEmoji: Bool {
|
||||||
|
return self.isSimpleEmoji || self.isCombinedIntoEmoji
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public extension UnicodeScalar {
|
public extension UnicodeScalar {
|
||||||
var isEmoji: Bool {
|
var isEmoji: Bool {
|
||||||
switch self.value {
|
switch self.value {
|
||||||
case 0x1F600...0x1F64F, 0x1F300...0x1F5FF, 0x1F680...0x1F6FF, 0x1F1E6...0x1F1FF, 0xE0020...0xE007F, 0xFE00...0xFE0F, 0x1F900...0x1F9FF, 0x1F018...0x1F0F5, 0x1F200...0x1F270, 65024...65039, 9100...9300, 8400...8447, 0x1F004, 0x1F18E, 0x1F191...0x1F19A, 0x1F5E8, 0x1FA70...0x1FA73, 0x1FA78...0x1FA7A, 0x1FA80...0x1FA82, 0x1FA90...0x1FA95, 0x1F382, 0x1FAF1, 0x1FAF2:
|
case 0x1F600...0x1F64F, 0x1F300...0x1F5FF, 0x1F680...0x1F6FF, 0x1F1E6...0x1F1FF, 0xE0020...0xE007F, 0xFE00...0xFE0F, 0x1F900...0x1F9FF, 0x1F018...0x1F0F5, 0x1F200...0x1F270, 65024...65039, 9100...9300, 8400...8447, 0x1F004, 0x1F18E, 0x1F191...0x1F19A, 0x1F5E8, 0x1FA70...0x1FA73, 0x1FA78...0x1FA7A, 0x1FA80...0x1FA82, 0x1FA90...0x1FA95, 0x1FAE0, 0x1FAF1, 0x1FAF2, 0x1F382:
|
||||||
return true
|
return true
|
||||||
case 0x2603, 0x265F, 0x267E, 0x2692, 0x26C4, 0x26C8, 0x26CE, 0x26CF, 0x26D1...0x26D3, 0x26E9, 0x26F0...0x26F9, 0x2705, 0x270A, 0x270B, 0x2728, 0x274E, 0x2753...0x2755, 0x274C, 0x2795...0x2797, 0x27B0, 0x27BF:
|
case 0x2603, 0x265F, 0x267E, 0x2692, 0x26C4, 0x26C8, 0x26CE, 0x26CF, 0x26D1...0x26D3, 0x26E9, 0x26F0...0x26F9, 0x2705, 0x270A, 0x270B, 0x2728, 0x274E, 0x2753...0x2755, 0x274C, 0x2795...0x2797, 0x27B0, 0x27BF:
|
||||||
return true
|
return true
|
||||||
@ -44,35 +67,38 @@ public extension String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var isSingleEmoji: Bool {
|
var isSingleEmoji: Bool {
|
||||||
return self.emojis.count == 1 && self.containsEmoji
|
return self.count == 1 && self.containsEmoji
|
||||||
|
// return self.emojis.count == 1 && self.containsEmoji
|
||||||
}
|
}
|
||||||
|
|
||||||
var containsEmoji: Bool {
|
var containsEmoji: Bool {
|
||||||
return self.unicodeScalars.contains { $0.isEmoji }
|
return self.contains { $0.isEmoji }
|
||||||
|
//return self.unicodeScalars.contains { $0.isEmoji }
|
||||||
}
|
}
|
||||||
|
|
||||||
var containsOnlyEmoji: Bool {
|
var containsOnlyEmoji: Bool {
|
||||||
guard !self.isEmpty else {
|
return !self.isEmpty && !self.contains { !$0.isEmoji }
|
||||||
return false
|
// guard !self.isEmpty else {
|
||||||
}
|
// return false
|
||||||
var nextShouldBeVariationSelector = false
|
// }
|
||||||
for scalar in self.unicodeScalars {
|
// var nextShouldBeVariationSelector = false
|
||||||
if nextShouldBeVariationSelector {
|
// for scalar in self.unicodeScalars {
|
||||||
if scalar == UnicodeScalar.VariationSelector {
|
// if nextShouldBeVariationSelector {
|
||||||
nextShouldBeVariationSelector = false
|
// if scalar == UnicodeScalar.VariationSelector {
|
||||||
continue
|
// nextShouldBeVariationSelector = false
|
||||||
} else {
|
// continue
|
||||||
return false
|
// } else {
|
||||||
}
|
// return false
|
||||||
}
|
// }
|
||||||
if !scalar.isEmoji && scalar.maybeEmoji {
|
// }
|
||||||
nextShouldBeVariationSelector = true
|
// if !scalar.isEmoji && scalar.maybeEmoji {
|
||||||
}
|
// nextShouldBeVariationSelector = true
|
||||||
else if !scalar.isEmoji && scalar != UnicodeScalar.ZeroWidthJoiner {
|
// }
|
||||||
return false
|
// else if !scalar.isEmoji && scalar != UnicodeScalar.ZeroWidthJoiner {
|
||||||
}
|
// return false
|
||||||
}
|
// }
|
||||||
return !nextShouldBeVariationSelector
|
// }
|
||||||
|
// return !nextShouldBeVariationSelector
|
||||||
}
|
}
|
||||||
|
|
||||||
var emojis: [String] {
|
var emojis: [String] {
|
||||||
|
@ -7,6 +7,7 @@ import TelegramCore
|
|||||||
import TelegramStringFormatting
|
import TelegramStringFormatting
|
||||||
|
|
||||||
private let productIdentifiers = [
|
private let productIdentifiers = [
|
||||||
|
"org.telegram.telegramPremium.annual",
|
||||||
"org.telegram.telegramPremium.monthly",
|
"org.telegram.telegramPremium.monthly",
|
||||||
"org.telegram.telegramPremium.twelveMonths",
|
"org.telegram.telegramPremium.twelveMonths",
|
||||||
"org.telegram.telegramPremium.sixMonths",
|
"org.telegram.telegramPremium.sixMonths",
|
||||||
@ -50,7 +51,7 @@ public final class InAppPurchaseManager: NSObject {
|
|||||||
} else if #available(iOS 11.2, *) {
|
} else if #available(iOS 11.2, *) {
|
||||||
return self.skProduct.subscriptionPeriod != nil
|
return self.skProduct.subscriptionPeriod != nil
|
||||||
} else {
|
} else {
|
||||||
return self.id.contains(".monthly")
|
return self.id.hasSuffix(".monthly") || self.id.hasSuffix(".annual")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Binary file not shown.
Binary file not shown.
@ -108,10 +108,7 @@ class GiftAvatarComponent: Component {
|
|||||||
self.addSubview(self.avatarNode.view)
|
self.addSubview(self.avatarNode.view)
|
||||||
|
|
||||||
self.setup()
|
self.setup()
|
||||||
|
|
||||||
let panGestureRecoginzer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))
|
|
||||||
self.addGestureRecognizer(panGestureRecoginzer)
|
|
||||||
|
|
||||||
let tapGestureRecoginzer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))
|
let tapGestureRecoginzer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))
|
||||||
self.addGestureRecognizer(tapGestureRecoginzer)
|
self.addGestureRecognizer(tapGestureRecoginzer)
|
||||||
|
|
||||||
@ -134,58 +131,6 @@ class GiftAvatarComponent: Component {
|
|||||||
self.playAppearanceAnimation(velocity: nil, mirror: false, explode: true)
|
self.playAppearanceAnimation(velocity: nil, mirror: false, explode: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var previousYaw: Float = 0.0
|
|
||||||
@objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
|
|
||||||
guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
self.previousInteractionTimestamp = CACurrentMediaTime()
|
|
||||||
|
|
||||||
if #available(iOS 11.0, *) {
|
|
||||||
node.removeAnimation(forKey: "rotate", blendOutDuration: 0.1)
|
|
||||||
node.removeAnimation(forKey: "tapRotate", blendOutDuration: 0.1)
|
|
||||||
} else {
|
|
||||||
node.removeAllAnimations()
|
|
||||||
}
|
|
||||||
|
|
||||||
switch gesture.state {
|
|
||||||
case .began:
|
|
||||||
self.previousYaw = 0.0
|
|
||||||
case .changed:
|
|
||||||
let translation = gesture.translation(in: gesture.view)
|
|
||||||
let yawPan = deg2rad(Float(translation.x))
|
|
||||||
|
|
||||||
func rubberBandingOffset(offset: CGFloat, bandingStart: CGFloat) -> CGFloat {
|
|
||||||
let bandedOffset = offset - bandingStart
|
|
||||||
let range: CGFloat = 60.0
|
|
||||||
let coefficient: CGFloat = 0.4
|
|
||||||
return bandingStart + (1.0 - (1.0 / ((bandedOffset * coefficient / range) + 1.0))) * range
|
|
||||||
}
|
|
||||||
|
|
||||||
var pitchTranslation = rubberBandingOffset(offset: abs(translation.y), bandingStart: 0.0)
|
|
||||||
if translation.y < 0.0 {
|
|
||||||
pitchTranslation *= -1.0
|
|
||||||
}
|
|
||||||
let pitchPan = deg2rad(Float(pitchTranslation))
|
|
||||||
|
|
||||||
self.previousYaw = yawPan
|
|
||||||
node.eulerAngles = SCNVector3(pitchPan, yawPan, 0.0)
|
|
||||||
case .ended:
|
|
||||||
let velocity = gesture.velocity(in: gesture.view)
|
|
||||||
|
|
||||||
var smallAngle = false
|
|
||||||
if (self.previousYaw < .pi / 2 && self.previousYaw > -.pi / 2) && abs(velocity.x) < 200 {
|
|
||||||
smallAngle = true
|
|
||||||
}
|
|
||||||
|
|
||||||
self.playAppearanceAnimation(velocity: velocity.x, smallAngle: smallAngle, explode: !smallAngle && abs(velocity.x) > 600)
|
|
||||||
node.eulerAngles = SCNVector3(0.0, 0.0, 0.0)
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func setup() {
|
private func setup() {
|
||||||
guard let url = getAppBundle().url(forResource: "gift", withExtension: "scn"), let scene = try? SCNScene(url: url, options: nil) else {
|
guard let url = getAppBundle().url(forResource: "gift", withExtension: "scn"), let scene = try? SCNScene(url: url, options: nil) else {
|
||||||
return
|
return
|
||||||
@ -210,6 +155,8 @@ class GiftAvatarComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func onReady() {
|
private func onReady() {
|
||||||
|
self.setupScaleAnimation()
|
||||||
|
|
||||||
self.playAppearanceAnimation(explode: true)
|
self.playAppearanceAnimation(explode: true)
|
||||||
|
|
||||||
self.previousInteractionTimestamp = CACurrentMediaTime()
|
self.previousInteractionTimestamp = CACurrentMediaTime()
|
||||||
@ -224,6 +171,18 @@ class GiftAvatarComponent: Component {
|
|||||||
self.timer?.start()
|
self.timer?.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func setupScaleAnimation() {
|
||||||
|
let animation = CABasicAnimation(keyPath: "transform.scale")
|
||||||
|
animation.duration = 2.0
|
||||||
|
animation.fromValue = 1.0 //NSValue(scnVector3: SCNVector3(x: 0.1, y: 0.1, z: 0.1))
|
||||||
|
animation.toValue = 1.15 //NSValue(scnVector3: SCNVector3(x: 0.115, y: 0.115, z: 0.115))
|
||||||
|
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
||||||
|
animation.autoreverses = true
|
||||||
|
animation.repeatCount = .infinity
|
||||||
|
|
||||||
|
self.avatarNode.view.layer.add(animation, forKey: "scale")
|
||||||
|
}
|
||||||
|
|
||||||
private func playAppearanceAnimation(velocity: CGFloat? = nil, smallAngle: Bool = false, mirror: Bool = false, explode: Bool = false) {
|
private func playAppearanceAnimation(velocity: CGFloat? = nil, smallAngle: Bool = false, mirror: Bool = false, explode: Bool = false) {
|
||||||
guard let scene = self.sceneView.scene else {
|
guard let scene = self.sceneView.scene else {
|
||||||
return
|
return
|
||||||
@ -233,23 +192,50 @@ class GiftAvatarComponent: Component {
|
|||||||
self.previousInteractionTimestamp = currentTime
|
self.previousInteractionTimestamp = currentTime
|
||||||
self.delayTapsTill = currentTime + 0.85
|
self.delayTapsTill = currentTime + 0.85
|
||||||
|
|
||||||
if explode, let node = scene.rootNode.childNode(withName: "swirl", recursively: false), let particles = scene.rootNode.childNode(withName: "particles", recursively: false) {
|
if explode, let node = scene.rootNode.childNode(withName: "swirl", recursively: false), let particlesLeft = scene.rootNode.childNode(withName: "particles_left", recursively: false), let particlesRight = scene.rootNode.childNode(withName: "particles_right", recursively: false), let particlesBottomLeft = scene.rootNode.childNode(withName: "particles_left_bottom", recursively: false), let particlesBottomRight = scene.rootNode.childNode(withName: "particles_right_bottom", recursively: false) {
|
||||||
if let particleSystem = particles.particleSystems?.first {
|
if let leftParticleSystem = particlesLeft.particleSystems?.first, let rightParticleSystem = particlesRight.particleSystems?.first, let leftBottomParticleSystem = particlesBottomLeft.particleSystems?.first, let rightBottomParticleSystem = particlesBottomRight.particleSystems?.first {
|
||||||
particleSystem.particleColorVariation = SCNVector4(0.15, 0.2, 0.15, 0.3)
|
leftParticleSystem.speedFactor = 2.0
|
||||||
particleSystem.speedFactor = 2.0
|
leftParticleSystem.particleVelocity = 1.6
|
||||||
particleSystem.particleVelocity = 2.2
|
leftParticleSystem.birthRate = 60.0
|
||||||
particleSystem.birthRate = 4.0
|
leftParticleSystem.particleLifeSpan = 4.0
|
||||||
particleSystem.particleLifeSpan = 2.0
|
|
||||||
|
rightParticleSystem.speedFactor = 2.0
|
||||||
|
rightParticleSystem.particleVelocity = 1.6
|
||||||
|
rightParticleSystem.birthRate = 60.0
|
||||||
|
rightParticleSystem.particleLifeSpan = 4.0
|
||||||
|
|
||||||
|
// leftBottomParticleSystem.speedFactor = 2.0
|
||||||
|
leftBottomParticleSystem.particleVelocity = 1.6
|
||||||
|
leftBottomParticleSystem.birthRate = 24.0
|
||||||
|
leftBottomParticleSystem.particleLifeSpan = 7.0
|
||||||
|
|
||||||
|
// rightBottomParticleSystem.speedFactor = 2.0
|
||||||
|
rightBottomParticleSystem.particleVelocity = 1.6
|
||||||
|
rightBottomParticleSystem.birthRate = 24.0
|
||||||
|
rightBottomParticleSystem.particleLifeSpan = 7.0
|
||||||
|
|
||||||
node.physicsField?.isActive = true
|
node.physicsField?.isActive = true
|
||||||
Queue.mainQueue().after(1.0) {
|
Queue.mainQueue().after(1.0) {
|
||||||
node.physicsField?.isActive = false
|
node.physicsField?.isActive = false
|
||||||
particles.particleSystems?.first?.birthRate = 1.2
|
|
||||||
particleSystem.particleVelocity = 1.0
|
|
||||||
particleSystem.particleLifeSpan = 4.0
|
|
||||||
|
|
||||||
let animation = POPBasicAnimation()
|
leftParticleSystem.birthRate = 12.0
|
||||||
animation.property = (POPAnimatableProperty.property(withName: "speedFactor", initializer: { property in
|
leftParticleSystem.particleVelocity = 1.2
|
||||||
|
leftParticleSystem.particleLifeSpan = 3.0
|
||||||
|
|
||||||
|
rightParticleSystem.birthRate = 12.0
|
||||||
|
rightParticleSystem.particleVelocity = 1.2
|
||||||
|
rightParticleSystem.particleLifeSpan = 3.0
|
||||||
|
|
||||||
|
leftBottomParticleSystem.particleVelocity = 1.2
|
||||||
|
leftBottomParticleSystem.birthRate = 7.0
|
||||||
|
leftBottomParticleSystem.particleLifeSpan = 5.0
|
||||||
|
|
||||||
|
rightBottomParticleSystem.particleVelocity = 1.2
|
||||||
|
rightBottomParticleSystem.birthRate = 7.0
|
||||||
|
rightBottomParticleSystem.particleLifeSpan = 5.0
|
||||||
|
|
||||||
|
let leftAnimation = POPBasicAnimation()
|
||||||
|
leftAnimation.property = (POPAnimatableProperty.property(withName: "speedFactor", initializer: { property in
|
||||||
property?.readBlock = { particleSystem, values in
|
property?.readBlock = { particleSystem, values in
|
||||||
values?.pointee = (particleSystem as! SCNParticleSystem).speedFactor
|
values?.pointee = (particleSystem as! SCNParticleSystem).speedFactor
|
||||||
}
|
}
|
||||||
@ -258,11 +244,27 @@ class GiftAvatarComponent: Component {
|
|||||||
}
|
}
|
||||||
property?.threshold = 0.01
|
property?.threshold = 0.01
|
||||||
}) as! POPAnimatableProperty)
|
}) as! POPAnimatableProperty)
|
||||||
animation.fromValue = 2.0 as NSNumber
|
leftAnimation.fromValue = 1.2 as NSNumber
|
||||||
animation.toValue = 1.0 as NSNumber
|
leftAnimation.toValue = 0.85 as NSNumber
|
||||||
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
|
leftAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
|
||||||
animation.duration = 0.5
|
leftAnimation.duration = 0.5
|
||||||
particleSystem.pop_add(animation, forKey: "speedFactor")
|
leftParticleSystem.pop_add(leftAnimation, forKey: "speedFactor")
|
||||||
|
|
||||||
|
let rightAnimation = POPBasicAnimation()
|
||||||
|
rightAnimation.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)
|
||||||
|
rightAnimation.fromValue = 1.2 as NSNumber
|
||||||
|
rightAnimation.toValue = 0.85 as NSNumber
|
||||||
|
rightAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
|
||||||
|
rightAnimation.duration = 0.5
|
||||||
|
rightParticleSystem.pop_add(rightAnimation, forKey: "speedFactor")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -919,7 +919,7 @@ private final class DemoSheetContent: CombinedComponent {
|
|||||||
var items: [DemoPagerComponent.Item] = component.order.compactMap { availableItems[$0] }
|
var items: [DemoPagerComponent.Item] = component.order.compactMap { availableItems[$0] }
|
||||||
let index: Int
|
let index: Int
|
||||||
switch component.source {
|
switch component.source {
|
||||||
case .intro:
|
case .intro, .gift:
|
||||||
index = items.firstIndex(where: { (component.subject as AnyHashable) == $0.content.id }) ?? 0
|
index = items.firstIndex(where: { (component.subject as AnyHashable) == $0.content.id }) ?? 0
|
||||||
case .other:
|
case .other:
|
||||||
items = items.filter { item in
|
items = items.filter { item in
|
||||||
@ -981,6 +981,8 @@ private final class DemoSheetContent: CombinedComponent {
|
|||||||
switch component.source {
|
switch component.source {
|
||||||
case let .intro(price):
|
case let .intro(price):
|
||||||
buttonText = strings.Premium_SubscribeFor(price ?? "–").string
|
buttonText = strings.Premium_SubscribeFor(price ?? "–").string
|
||||||
|
case let .gift(price):
|
||||||
|
buttonText = strings.Premium_Gift_GiftSubscription(price ?? "–").string
|
||||||
case .other:
|
case .other:
|
||||||
switch component.subject {
|
switch component.subject {
|
||||||
case .uniqueReactions:
|
case .uniqueReactions:
|
||||||
@ -1173,6 +1175,7 @@ public class PremiumDemoScreen: ViewControllerComponentContainer {
|
|||||||
|
|
||||||
public enum Source: Equatable {
|
public enum Source: Equatable {
|
||||||
case intro(String?)
|
case intro(String?)
|
||||||
|
case gift(String?)
|
||||||
case other
|
case other
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,498 +17,7 @@ import Markdown
|
|||||||
import InAppPurchaseManager
|
import InAppPurchaseManager
|
||||||
import ConfettiEffect
|
import ConfettiEffect
|
||||||
import TextFormat
|
import TextFormat
|
||||||
import CheckNode
|
import UniversalMediaPlayer
|
||||||
|
|
||||||
private final class ProductGroupComponent: Component {
|
|
||||||
public final class Item: Equatable {
|
|
||||||
public let content: AnyComponentWithIdentity<Empty>
|
|
||||||
public let action: () -> Void
|
|
||||||
|
|
||||||
public init(_ content: AnyComponentWithIdentity<Empty>, action: @escaping () -> Void) {
|
|
||||||
self.content = content
|
|
||||||
self.action = action
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func ==(lhs: Item, rhs: Item) -> Bool {
|
|
||||||
if lhs.content != rhs.content {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let items: [Item]
|
|
||||||
let backgroundColor: UIColor
|
|
||||||
let selectionColor: UIColor
|
|
||||||
|
|
||||||
init(
|
|
||||||
items: [Item],
|
|
||||||
backgroundColor: UIColor,
|
|
||||||
selectionColor: UIColor
|
|
||||||
) {
|
|
||||||
self.items = items
|
|
||||||
self.backgroundColor = backgroundColor
|
|
||||||
self.selectionColor = selectionColor
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func ==(lhs: ProductGroupComponent, rhs: ProductGroupComponent) -> Bool {
|
|
||||||
if lhs.items != rhs.items {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if lhs.backgroundColor != rhs.backgroundColor {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if lhs.selectionColor != rhs.selectionColor {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
public final class View: UIView {
|
|
||||||
private var buttonViews: [AnyHashable: HighlightTrackingButton] = [:]
|
|
||||||
private var itemViews: [AnyHashable: ComponentHostView<Empty>] = [:]
|
|
||||||
|
|
||||||
private var component: ProductGroupComponent?
|
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
|
||||||
super.init(frame: frame)
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func buttonPressed(_ sender: HighlightTrackingButton) {
|
|
||||||
guard let component = self.component else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if let (id, _) = self.buttonViews.first(where: { $0.value === sender }), let item = component.items.first(where: { $0.content.id == id }) {
|
|
||||||
item.action()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func update(component: ProductGroupComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
|
||||||
let spacing: CGFloat = 16.0
|
|
||||||
var size = CGSize(width: availableSize.width, height: 0.0)
|
|
||||||
|
|
||||||
var validIds: [AnyHashable] = []
|
|
||||||
|
|
||||||
var i = 0
|
|
||||||
for item in component.items {
|
|
||||||
validIds.append(item.content.id)
|
|
||||||
|
|
||||||
let buttonView: HighlightTrackingButton
|
|
||||||
let itemView: ComponentHostView<Empty>
|
|
||||||
var itemTransition = transition
|
|
||||||
|
|
||||||
if let current = self.buttonViews[item.content.id] {
|
|
||||||
buttonView = current
|
|
||||||
} else {
|
|
||||||
buttonView = HighlightTrackingButton()
|
|
||||||
buttonView.clipsToBounds = true
|
|
||||||
buttonView.layer.cornerRadius = 10.0
|
|
||||||
if #available(iOS 13.0, *) {
|
|
||||||
buttonView.layer.cornerCurve = .continuous
|
|
||||||
}
|
|
||||||
buttonView.isMultipleTouchEnabled = false
|
|
||||||
buttonView.isExclusiveTouch = true
|
|
||||||
buttonView.addTarget(self, action: #selector(self.buttonPressed(_:)), for: .touchUpInside)
|
|
||||||
self.buttonViews[item.content.id] = buttonView
|
|
||||||
self.addSubview(buttonView)
|
|
||||||
}
|
|
||||||
buttonView.backgroundColor = component.backgroundColor
|
|
||||||
|
|
||||||
if let current = self.itemViews[item.content.id] {
|
|
||||||
itemView = current
|
|
||||||
} else {
|
|
||||||
itemTransition = transition.withAnimation(.none)
|
|
||||||
itemView = ComponentHostView<Empty>()
|
|
||||||
self.itemViews[item.content.id] = itemView
|
|
||||||
self.addSubview(itemView)
|
|
||||||
}
|
|
||||||
let itemSize = itemView.update(
|
|
||||||
transition: itemTransition,
|
|
||||||
component: item.content.component,
|
|
||||||
environment: {},
|
|
||||||
containerSize: CGSize(width: size.width, height: .greatestFiniteMagnitude)
|
|
||||||
)
|
|
||||||
|
|
||||||
let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height), size: itemSize)
|
|
||||||
buttonView.frame = CGRect(origin: itemFrame.origin, size: CGSize(width: availableSize.width, height: itemSize.height + UIScreenPixel))
|
|
||||||
itemView.frame = CGRect(origin: CGPoint(x: itemFrame.minX, y: itemFrame.minY + floor((itemFrame.height - itemSize.height) / 2.0)), size: itemSize)
|
|
||||||
itemView.isUserInteractionEnabled = false
|
|
||||||
|
|
||||||
buttonView.highligthedChanged = { [weak buttonView] highlighted in
|
|
||||||
if highlighted {
|
|
||||||
buttonView?.backgroundColor = component.selectionColor
|
|
||||||
} else {
|
|
||||||
UIView.animate(withDuration: 0.3, animations: {
|
|
||||||
buttonView?.backgroundColor = component.backgroundColor
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
size.height += itemSize.height + spacing
|
|
||||||
|
|
||||||
i += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
size.height -= spacing
|
|
||||||
|
|
||||||
var removeIds: [AnyHashable] = []
|
|
||||||
for (id, itemView) in self.itemViews {
|
|
||||||
if !validIds.contains(id) {
|
|
||||||
removeIds.append(id)
|
|
||||||
itemView.removeFromSuperview()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for id in removeIds {
|
|
||||||
self.itemViews.removeValue(forKey: id)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.component = component
|
|
||||||
|
|
||||||
return size
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func makeView() -> View {
|
|
||||||
return View(frame: CGRect())
|
|
||||||
}
|
|
||||||
|
|
||||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
|
||||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final class GiftComponent: CombinedComponent {
|
|
||||||
let title: String
|
|
||||||
let totalPrice: String
|
|
||||||
let perMonthPrice: String
|
|
||||||
let discount: String
|
|
||||||
let selected: Bool
|
|
||||||
let primaryTextColor: UIColor
|
|
||||||
let secondaryTextColor: UIColor
|
|
||||||
let accentColor: UIColor
|
|
||||||
let checkForegroundColor: UIColor
|
|
||||||
let checkBorderColor: UIColor
|
|
||||||
|
|
||||||
init(
|
|
||||||
title: String,
|
|
||||||
totalPrice: String,
|
|
||||||
perMonthPrice: String,
|
|
||||||
discount: String,
|
|
||||||
selected: Bool,
|
|
||||||
primaryTextColor: UIColor,
|
|
||||||
secondaryTextColor: UIColor,
|
|
||||||
accentColor: UIColor,
|
|
||||||
checkForegroundColor: UIColor,
|
|
||||||
checkBorderColor: UIColor
|
|
||||||
) {
|
|
||||||
self.title = title
|
|
||||||
self.totalPrice = totalPrice
|
|
||||||
self.perMonthPrice = perMonthPrice
|
|
||||||
self.discount = discount
|
|
||||||
self.selected = selected
|
|
||||||
self.primaryTextColor = primaryTextColor
|
|
||||||
self.secondaryTextColor = secondaryTextColor
|
|
||||||
self.accentColor = accentColor
|
|
||||||
self.checkForegroundColor = checkForegroundColor
|
|
||||||
self.checkBorderColor = checkBorderColor
|
|
||||||
}
|
|
||||||
|
|
||||||
static func ==(lhs: GiftComponent, rhs: GiftComponent) -> Bool {
|
|
||||||
if lhs.title != rhs.title {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if lhs.totalPrice != rhs.totalPrice {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if lhs.perMonthPrice != rhs.perMonthPrice {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if lhs.discount != rhs.discount {
|
|
||||||
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 discountBackground = Child(RoundedRectangle.self)
|
|
||||||
let discount = Child(MultilineTextComponent.self)
|
|
||||||
let subtitle = Child(MultilineTextComponent.self)
|
|
||||||
let label = Child(MultilineTextComponent.self)
|
|
||||||
let selection = Child(RoundedRectangle.self)
|
|
||||||
|
|
||||||
return { context in
|
|
||||||
let component = context.component
|
|
||||||
|
|
||||||
let insets = UIEdgeInsets(top: 9.0, left: 62.0, bottom: 12.0, right: 16.0)
|
|
||||||
|
|
||||||
let spacing: CGFloat = 2.0
|
|
||||||
|
|
||||||
let label = label.update(
|
|
||||||
component: MultilineTextComponent(
|
|
||||||
text: .plain(
|
|
||||||
NSAttributedString(
|
|
||||||
string: component.totalPrice,
|
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
context.add(discountBackground
|
|
||||||
.position(CGPoint(x: insets.left + discountSize.width / 2.0, y: insets.top + title.size.height + spacing + discountSize.height / 2.0))
|
|
||||||
)
|
|
||||||
|
|
||||||
context.add(discount
|
|
||||||
.position(CGPoint(x: insets.left + discountSize.width / 2.0, y: insets.top + title.size.height + spacing + discountSize.height / 2.0))
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
discountSize = CGSize(width: 0.0, height: 18.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
let subtitle = subtitle.update(
|
|
||||||
component: MultilineTextComponent(
|
|
||||||
text: .plain(
|
|
||||||
NSAttributedString(
|
|
||||||
string: component.perMonthPrice,
|
|
||||||
font: Font.regular(13),
|
|
||||||
textColor: component.secondaryTextColor
|
|
||||||
)
|
|
||||||
),
|
|
||||||
maximumNumberOfLines: 1
|
|
||||||
),
|
|
||||||
availableSize: CGSize(width: context.availableSize.width - insets.left - insets.right - label.size.width - discountSize.width, height: context.availableSize.height),
|
|
||||||
transition: context.transition
|
|
||||||
)
|
|
||||||
|
|
||||||
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))
|
|
||||||
)
|
|
||||||
|
|
||||||
context.add(subtitle
|
|
||||||
.position(CGPoint(x: insets.left + (discountSize.width.isZero ? 0.0 : discountSize.width + 7.0) + subtitle.size.width / 2.0, y: insets.top + title.size.height + spacing + discountSize.height / 2.0))
|
|
||||||
)
|
|
||||||
|
|
||||||
let size = CGSize(width: context.availableSize.width, height: insets.top + title.size.height + spacing + subtitle.size.height + insets.bottom)
|
|
||||||
let distance = context.availableSize.width - insets.left - insets.right - label.size.width - subtitle.size.width - discountSize.width - 7.0
|
|
||||||
|
|
||||||
let labelOriginY: CGFloat
|
|
||||||
if distance > 8.0 {
|
|
||||||
labelOriginY = size.height / 2.0
|
|
||||||
} else {
|
|
||||||
labelOriginY = insets.top + title.size.height / 2.0
|
|
||||||
}
|
|
||||||
|
|
||||||
context.add(label
|
|
||||||
.position(CGPoint(x: context.availableSize.width - insets.right - label.size.width / 2.0, y: labelOriginY))
|
|
||||||
)
|
|
||||||
|
|
||||||
context.add(check
|
|
||||||
.position(CGPoint(x: 20.0 + check.size.width / 2.0, y: size.height / 2.0))
|
|
||||||
)
|
|
||||||
|
|
||||||
if component.selected {
|
|
||||||
let selection = selection.update(
|
|
||||||
component: RoundedRectangle(
|
|
||||||
color: component.accentColor,
|
|
||||||
cornerRadius: 10.0,
|
|
||||||
stroke: 2.0
|
|
||||||
),
|
|
||||||
availableSize: size,
|
|
||||||
transition: context.transition
|
|
||||||
)
|
|
||||||
context.add(selection
|
|
||||||
.position(CGPoint(x: 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: Transition) -> 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: Transition) -> CGSize {
|
|
||||||
return view.update(component: self, availableSize: availableSize, transition: transition)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final class PremiumGiftScreenContentComponent: CombinedComponent {
|
private final class PremiumGiftScreenContentComponent: CombinedComponent {
|
||||||
typealias EnvironmentType = (ViewControllerComponentContainer.Environment, ScrollChildEnvironment)
|
typealias EnvironmentType = (ViewControllerComponentContainer.Environment, ScrollChildEnvironment)
|
||||||
@ -520,14 +29,16 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent {
|
|||||||
|
|
||||||
let present: (ViewController) -> Void
|
let present: (ViewController) -> Void
|
||||||
let selectProduct: (String) -> Void
|
let selectProduct: (String) -> Void
|
||||||
|
let buy: () -> Void
|
||||||
|
|
||||||
init(context: AccountContext, peer: EnginePeer?, products: [PremiumGiftProduct]?, selectedProductId: String?, present: @escaping (ViewController) -> Void, selectProduct: @escaping (String) -> Void) {
|
init(context: AccountContext, peer: EnginePeer?, products: [PremiumGiftProduct]?, selectedProductId: String?, present: @escaping (ViewController) -> Void, selectProduct: @escaping (String) -> Void, buy: @escaping () -> Void) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.peer = peer
|
self.peer = peer
|
||||||
self.products = products
|
self.products = products
|
||||||
self.selectedProductId = selectedProductId
|
self.selectedProductId = selectedProductId
|
||||||
self.present = present
|
self.present = present
|
||||||
self.selectProduct = selectProduct
|
self.selectProduct = selectProduct
|
||||||
|
self.buy = buy
|
||||||
}
|
}
|
||||||
|
|
||||||
static func ==(lhs: PremiumGiftScreenContentComponent, rhs: PremiumGiftScreenContentComponent) -> Bool {
|
static func ==(lhs: PremiumGiftScreenContentComponent, rhs: PremiumGiftScreenContentComponent) -> Bool {
|
||||||
@ -546,12 +57,97 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent {
|
|||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final class State: ComponentState {
|
||||||
|
private let context: AccountContext
|
||||||
|
|
||||||
|
private var disposable: Disposable?
|
||||||
|
private(set) var configuration = PremiumIntroConfiguration.defaultValue
|
||||||
|
private(set) var promoConfiguration: PremiumPromoConfiguration?
|
||||||
|
|
||||||
|
private var stickersDisposable: Disposable?
|
||||||
|
private var preloadDisposableSet = DisposableSet()
|
||||||
|
|
||||||
|
init(context: AccountContext) {
|
||||||
|
self.context = context
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
self.disposable = (context.engine.data.subscribe(
|
||||||
|
TelegramEngine.EngineData.Item.Configuration.App(),
|
||||||
|
TelegramEngine.EngineData.Item.Configuration.PremiumPromo()
|
||||||
|
)
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] appConfiguration, promoConfiguration in
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.configuration = PremiumIntroConfiguration.with(appConfiguration: appConfiguration)
|
||||||
|
strongSelf.promoConfiguration = promoConfiguration
|
||||||
|
strongSelf.updated(transition: .immediate)
|
||||||
|
|
||||||
|
// if let identifier = source.identifier {
|
||||||
|
// var jsonString: String = "{"
|
||||||
|
// jsonString += "\"source\": \"\(identifier)\","
|
||||||
|
//
|
||||||
|
// jsonString += "\"data\": {\"premium_promo_order\":["
|
||||||
|
// var isFirst = true
|
||||||
|
// for perk in strongSelf.configuration.perks {
|
||||||
|
// if !isFirst {
|
||||||
|
// jsonString += ","
|
||||||
|
// }
|
||||||
|
// isFirst = false
|
||||||
|
// jsonString += "\"\(perk.identifier)\""
|
||||||
|
// }
|
||||||
|
// jsonString += "]}}"
|
||||||
|
//
|
||||||
|
// if let data = jsonString.data(using: .utf8), let json = JSON(data: data) {
|
||||||
|
// addAppLogEvent(postbox: strongSelf.context.account.postbox, type: "premium.promo_screen_show", data: json)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
for (_, video) in promoConfiguration.videos {
|
||||||
|
strongSelf.preloadDisposableSet.add(preloadVideoResource(postbox: context.account.postbox, resourceReference: .standalone(resource: video.resource), duration: 3.0).start())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let _ = updatePremiumPromoConfigurationOnce(account: context.account).start()
|
||||||
|
|
||||||
|
let stickersKey: PostboxViewKey = .orderedItemList(id: Namespaces.OrderedItemList.CloudPremiumStickers)
|
||||||
|
self.stickersDisposable = (self.context.account.postbox.combinedView(keys: [stickersKey])
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] views in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let view = views.views[stickersKey] as? OrderedItemListView {
|
||||||
|
for item in view.items {
|
||||||
|
if let mediaItem = item.contents.get(RecentMediaItem.self) {
|
||||||
|
let file = mediaItem.media
|
||||||
|
strongSelf.preloadDisposableSet.add(freeMediaFileResourceInteractiveFetched(account: context.account, fileReference: .standalone(media: file), resource: file.resource).start())
|
||||||
|
if let effect = file.videoThumbnails.first {
|
||||||
|
strongSelf.preloadDisposableSet.add(freeMediaFileResourceInteractiveFetched(account: context.account, fileReference: .standalone(media: file), resource: effect.resource).start())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
self.disposable?.dispose()
|
||||||
|
self.preloadDisposableSet.dispose()
|
||||||
|
self.stickersDisposable?.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeState() -> State {
|
||||||
|
return State(context: self.context)
|
||||||
|
}
|
||||||
|
|
||||||
static var body: Body {
|
static var body: Body {
|
||||||
let overscroll = Child(Rectangle.self)
|
let overscroll = Child(Rectangle.self)
|
||||||
let fade = Child(RoundedRectangle.self)
|
let fade = Child(RoundedRectangle.self)
|
||||||
let text = Child(MultilineTextComponent.self)
|
let text = Child(MultilineTextComponent.self)
|
||||||
let section = Child(ProductGroupComponent.self)
|
let optionsSection = Child(SectionGroupComponent.self)
|
||||||
|
let perksSection = Child(SectionGroupComponent.self)
|
||||||
|
|
||||||
return { context in
|
return { context in
|
||||||
let sideInset: CGFloat = 16.0
|
let sideInset: CGFloat = 16.0
|
||||||
@ -596,7 +192,9 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent {
|
|||||||
size.height += 183.0 + 10.0 + environment.navigationHeight - 56.0
|
size.height += 183.0 + 10.0 + environment.navigationHeight - 56.0
|
||||||
|
|
||||||
let textColor = theme.list.itemPrimaryTextColor
|
let textColor = theme.list.itemPrimaryTextColor
|
||||||
|
let titleColor = theme.list.itemPrimaryTextColor
|
||||||
let subtitleColor = theme.list.itemSecondaryTextColor
|
let subtitleColor = theme.list.itemSecondaryTextColor
|
||||||
|
let arrowColor = theme.list.disclosureArrowColor
|
||||||
|
|
||||||
let textFont = Font.regular(15.0)
|
let textFont = Font.regular(15.0)
|
||||||
let boldTextFont = Font.semibold(15.0)
|
let boldTextFont = Font.semibold(15.0)
|
||||||
@ -624,16 +222,16 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent {
|
|||||||
size.height += text.size.height
|
size.height += text.size.height
|
||||||
size.height += 21.0
|
size.height += 21.0
|
||||||
|
|
||||||
var items: [ProductGroupComponent.Item] = []
|
var items: [SectionGroupComponent.Item] = []
|
||||||
|
|
||||||
let gradientColors: [UIColor] = [
|
|
||||||
UIColor(rgb: 0x8e77ff),
|
|
||||||
UIColor(rgb: 0x9a6fff),
|
|
||||||
UIColor(rgb: 0xb36eee)
|
|
||||||
]
|
|
||||||
|
|
||||||
var i = 0
|
var i = 0
|
||||||
if let products = component.products {
|
if let products = component.products {
|
||||||
|
let gradientColors: [UIColor] = [
|
||||||
|
UIColor(rgb: 0x8e77ff),
|
||||||
|
UIColor(rgb: 0x9a6fff),
|
||||||
|
UIColor(rgb: 0xb36eee)
|
||||||
|
]
|
||||||
|
|
||||||
let shortestOptionPrice: Int64
|
let shortestOptionPrice: Int64
|
||||||
if let product = products.last {
|
if let product = products.last {
|
||||||
shortestOptionPrice = Int64(Float(product.storeProduct.priceCurrencyAndAmount.amount) / Float(product.months))
|
shortestOptionPrice = Int64(Float(product.storeProduct.priceCurrencyAndAmount.amount) / Float(product.months))
|
||||||
@ -657,14 +255,13 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent {
|
|||||||
discount = ""
|
discount = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
items.append(ProductGroupComponent.Item(
|
items.append(SectionGroupComponent.Item(
|
||||||
AnyComponentWithIdentity(
|
AnyComponentWithIdentity(
|
||||||
id: product.id,
|
id: product.id,
|
||||||
component: AnyComponent(
|
component: AnyComponent(
|
||||||
GiftComponent(
|
PremiumOptionComponent(
|
||||||
title: giftTitle,
|
title: giftTitle,
|
||||||
totalPrice: product.price,
|
totalPrice: product.price,
|
||||||
perMonthPrice: strings.Premium_Gift_PricePerMonth(product.pricePerMonth).string,
|
|
||||||
discount: discount,
|
discount: discount,
|
||||||
selected: product.id == component.selectedProductId,
|
selected: product.id == component.selectedProductId,
|
||||||
primaryTextColor: textColor,
|
primaryTextColor: textColor,
|
||||||
@ -683,24 +280,150 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let section = section.update(
|
let optionsSection = optionsSection.update(
|
||||||
component: ProductGroupComponent(
|
component: SectionGroupComponent(
|
||||||
items: items,
|
items: items,
|
||||||
backgroundColor: environment.theme.list.itemBlocksBackgroundColor,
|
backgroundColor: environment.theme.list.itemBlocksBackgroundColor,
|
||||||
selectionColor: environment.theme.list.itemHighlightedBackgroundColor
|
selectionColor: environment.theme.list.itemHighlightedBackgroundColor,
|
||||||
|
separatorColor: environment.theme.list.itemBlocksSeparatorColor
|
||||||
),
|
),
|
||||||
environment: {},
|
environment: {},
|
||||||
availableSize: CGSize(width: availableWidth - sideInsets, height: .greatestFiniteMagnitude),
|
availableSize: CGSize(width: availableWidth - sideInsets, height: .greatestFiniteMagnitude),
|
||||||
transition: context.transition
|
transition: context.transition
|
||||||
)
|
)
|
||||||
context.add(section
|
context.add(optionsSection
|
||||||
.position(CGPoint(x: availableWidth / 2.0, y: size.height + section.size.height / 2.0))
|
.position(CGPoint(x: availableWidth / 2.0, y: size.height + optionsSection.size.height / 2.0))
|
||||||
.clipsToBounds(true)
|
.clipsToBounds(true)
|
||||||
.cornerRadius(10.0)
|
.cornerRadius(10.0)
|
||||||
)
|
)
|
||||||
size.height += section.size.height
|
size.height += optionsSection.size.height
|
||||||
size.height += 23.0
|
size.height += 23.0
|
||||||
|
|
||||||
|
let state = context.state
|
||||||
|
let accountContext = context.component.context
|
||||||
|
let present = context.component.present
|
||||||
|
let buy = context.component.buy
|
||||||
|
|
||||||
|
let price = context.component.products?.first(where: { $0.id == context.component.selectedProductId })?.price
|
||||||
|
|
||||||
|
let gradientColors: [UIColor] = [
|
||||||
|
UIColor(rgb: 0xF27C30),
|
||||||
|
UIColor(rgb: 0xE36850),
|
||||||
|
UIColor(rgb: 0xD15078),
|
||||||
|
UIColor(rgb: 0xC14998),
|
||||||
|
UIColor(rgb: 0xB24CB5),
|
||||||
|
UIColor(rgb: 0xA34ED0),
|
||||||
|
UIColor(rgb: 0x9054E9),
|
||||||
|
UIColor(rgb: 0x7561EB),
|
||||||
|
UIColor(rgb: 0x5A6EEE),
|
||||||
|
UIColor(rgb: 0x548DFF),
|
||||||
|
UIColor(rgb: 0x54A3FF),
|
||||||
|
UIColor(rgb: 0x54A3FF)
|
||||||
|
]
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
var perksItems: [SectionGroupComponent.Item] = []
|
||||||
|
for perk in state.configuration.perks {
|
||||||
|
let iconBackgroundColors = gradientColors[i]
|
||||||
|
perksItems.append(SectionGroupComponent.Item(
|
||||||
|
AnyComponentWithIdentity(
|
||||||
|
id: perk.identifier,
|
||||||
|
component: AnyComponent(
|
||||||
|
PerkComponent(
|
||||||
|
iconName: perk.iconName,
|
||||||
|
iconBackgroundColors: [
|
||||||
|
iconBackgroundColors
|
||||||
|
],
|
||||||
|
title: perk.title(strings: strings),
|
||||||
|
titleColor: titleColor,
|
||||||
|
subtitle: perk.subtitle(strings: strings),
|
||||||
|
subtitleColor: subtitleColor,
|
||||||
|
arrowColor: arrowColor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
action: { [weak state] in
|
||||||
|
var demoSubject: PremiumDemoScreen.Subject
|
||||||
|
switch perk {
|
||||||
|
case .doubleLimits:
|
||||||
|
var dismissImpl: (() -> Void)?
|
||||||
|
let controller = PremimLimitsListScreen(context: accountContext, buttonText: strings.Premium_Gift_GiftSubscription(price ?? "–").string, isPremium: false)
|
||||||
|
controller.action = {
|
||||||
|
dismissImpl?()
|
||||||
|
buy()
|
||||||
|
}
|
||||||
|
controller.disposed = {
|
||||||
|
// updateIsFocused(false)
|
||||||
|
}
|
||||||
|
present(controller)
|
||||||
|
dismissImpl = { [weak controller] in
|
||||||
|
controller?.dismiss(animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
// updateIsFocused(true)
|
||||||
|
return
|
||||||
|
case .moreUpload:
|
||||||
|
demoSubject = .moreUpload
|
||||||
|
case .fasterDownload:
|
||||||
|
demoSubject = .fasterDownload
|
||||||
|
case .voiceToText:
|
||||||
|
demoSubject = .voiceToText
|
||||||
|
case .noAds:
|
||||||
|
demoSubject = .noAds
|
||||||
|
case .uniqueReactions:
|
||||||
|
demoSubject = .uniqueReactions
|
||||||
|
case .premiumStickers:
|
||||||
|
demoSubject = .premiumStickers
|
||||||
|
case .advancedChatManagement:
|
||||||
|
demoSubject = .advancedChatManagement
|
||||||
|
case .profileBadge:
|
||||||
|
demoSubject = .profileBadge
|
||||||
|
case .animatedUserpics:
|
||||||
|
demoSubject = .animatedUserpics
|
||||||
|
case .appIcons:
|
||||||
|
demoSubject = .appIcons
|
||||||
|
case .animatedEmoji:
|
||||||
|
demoSubject = .animatedEmoji
|
||||||
|
}
|
||||||
|
|
||||||
|
let controller = PremiumDemoScreen(
|
||||||
|
context: accountContext,
|
||||||
|
subject: demoSubject,
|
||||||
|
source: .gift(price),
|
||||||
|
order: state?.configuration.perks,
|
||||||
|
action: {
|
||||||
|
buy()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
controller.disposed = {
|
||||||
|
// updateIsFocused(false)
|
||||||
|
}
|
||||||
|
present(controller)
|
||||||
|
// updateIsFocused(true)
|
||||||
|
|
||||||
|
addAppLogEvent(postbox: accountContext.account.postbox, type: "premium.promo_screen_tap", data: ["item": perk.identifier])
|
||||||
|
}
|
||||||
|
))
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
let perksSection = perksSection.update(
|
||||||
|
component: SectionGroupComponent(
|
||||||
|
items: perksItems,
|
||||||
|
backgroundColor: environment.theme.list.itemBlocksBackgroundColor,
|
||||||
|
selectionColor: environment.theme.list.itemHighlightedBackgroundColor,
|
||||||
|
separatorColor: environment.theme.list.itemBlocksSeparatorColor
|
||||||
|
),
|
||||||
|
environment: {},
|
||||||
|
availableSize: CGSize(width: availableWidth - sideInsets, height: .greatestFiniteMagnitude),
|
||||||
|
transition: context.transition
|
||||||
|
)
|
||||||
|
context.add(perksSection
|
||||||
|
.position(CGPoint(x: availableWidth / 2.0, y: size.height + perksSection.size.height / 2.0))
|
||||||
|
.clipsToBounds(true)
|
||||||
|
.cornerRadius(10.0)
|
||||||
|
)
|
||||||
|
|
||||||
|
size.height += perksSection.size.height
|
||||||
|
|
||||||
size.height += 10.0
|
size.height += 10.0
|
||||||
size.height += scrollEnvironment.insets.bottom
|
size.height += scrollEnvironment.insets.bottom
|
||||||
@ -964,7 +687,6 @@ private final class PremiumGiftScreenComponent: CombinedComponent {
|
|||||||
let bottomPanel = Child(BlurredRectangle.self)
|
let bottomPanel = Child(BlurredRectangle.self)
|
||||||
let bottomSeparator = Child(Rectangle.self)
|
let bottomSeparator = Child(Rectangle.self)
|
||||||
let button = Child(SolidRoundedButtonComponent.self)
|
let button = Child(SolidRoundedButtonComponent.self)
|
||||||
let termsText = Child(MultilineTextComponent.self)
|
|
||||||
|
|
||||||
return { context in
|
return { context in
|
||||||
let environment = context.environment[EnvironmentType.self].value
|
let environment = context.environment[EnvironmentType.self].value
|
||||||
@ -1024,6 +746,8 @@ private final class PremiumGiftScreenComponent: CombinedComponent {
|
|||||||
present: context.component.present,
|
present: context.component.present,
|
||||||
selectProduct: { [weak state] productId in
|
selectProduct: { [weak state] productId in
|
||||||
state?.selectProduct(id: productId)
|
state?.selectProduct(id: productId)
|
||||||
|
}, buy: { [weak state] in
|
||||||
|
state?.buy()
|
||||||
}
|
}
|
||||||
)),
|
)),
|
||||||
contentInsets: UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: bottomPanelHeight, right: 0.0),
|
contentInsets: UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: bottomPanelHeight, right: 0.0),
|
||||||
@ -1151,103 +875,24 @@ private final class PremiumGiftScreenComponent: CombinedComponent {
|
|||||||
)
|
)
|
||||||
|
|
||||||
let bottomPanelAlpha: CGFloat
|
let bottomPanelAlpha: CGFloat
|
||||||
if let bottomContentOffset = state.bottomContentOffset, context.availableSize.width > 320.0 {
|
if let bottomContentOffset = state.bottomContentOffset {
|
||||||
bottomPanelAlpha = min(16.0, bottomContentOffset) / 16.0
|
bottomPanelAlpha = min(16.0, bottomContentOffset) / 16.0
|
||||||
} else {
|
} else {
|
||||||
bottomPanelAlpha = 0.0
|
bottomPanelAlpha = 1.0
|
||||||
}
|
}
|
||||||
|
|
||||||
context.add(bottomPanel
|
context.add(bottomPanel
|
||||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height / 2.0))
|
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height / 2.0))
|
||||||
.opacity(bottomPanelAlpha)
|
.opacity(bottomPanelAlpha)
|
||||||
.disappear(Transition.Disappear { view, transition, completion in
|
|
||||||
if case .none = transition.animation {
|
|
||||||
completion()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
view.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: bottomPanel.size.height), duration: 0.2, removeOnCompletion: false, additive: true, completion: { _ in
|
|
||||||
completion()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
context.add(bottomSeparator
|
context.add(bottomSeparator
|
||||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height))
|
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height))
|
||||||
.opacity(bottomPanelAlpha)
|
.opacity(bottomPanelAlpha)
|
||||||
.disappear(Transition.Disappear { view, transition, completion in
|
|
||||||
if case .none = transition.animation {
|
|
||||||
completion()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
view.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: bottomPanel.size.height), duration: 0.2, removeOnCompletion: false, additive: true, completion: { _ in
|
|
||||||
completion()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
context.add(button
|
context.add(button
|
||||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height + bottomPanelPadding + button.size.height / 2.0))
|
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height + bottomPanelPadding + button.size.height / 2.0))
|
||||||
.disappear(Transition.Disappear { view, transition, completion in
|
|
||||||
if case .none = transition.animation {
|
|
||||||
completion()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
view.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: bottomPanel.size.height), duration: 0.2, removeOnCompletion: false, additive: true, completion: { _ in
|
|
||||||
completion()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if let _ = context.state.peer {
|
|
||||||
let accountContext = context.component.context
|
|
||||||
let present = context.component.present
|
|
||||||
|
|
||||||
let sideInset: CGFloat = 16.0
|
|
||||||
let textSideInset: CGFloat = 16.0
|
|
||||||
let availableWidth = context.availableSize.width
|
|
||||||
let sideInsets = sideInset * 2.0 + environment.safeInsets.left + environment.safeInsets.right
|
|
||||||
|
|
||||||
if availableWidth > 320.0 {
|
|
||||||
let termsFont = Font.regular(13.0)
|
|
||||||
let termsTextColor = environment.theme.list.freeTextColor
|
|
||||||
let termsMarkdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: termsFont, textColor: termsTextColor), bold: MarkdownAttributeSet(font: termsFont, textColor: termsTextColor), link: MarkdownAttributeSet(font: termsFont, textColor: environment.theme.list.itemAccentColor), linkAttribute: { contents in
|
|
||||||
return (TelegramTextAttributes.URL, contents)
|
|
||||||
})
|
|
||||||
|
|
||||||
let termsString: MultilineTextComponent.TextContent = .markdown(
|
|
||||||
text: environment.strings.Premium_Gift_Info,
|
|
||||||
attributes: termsMarkdownAttributes
|
|
||||||
)
|
|
||||||
|
|
||||||
let termsText = termsText.update(
|
|
||||||
component: MultilineTextComponent(
|
|
||||||
text: termsString,
|
|
||||||
horizontalAlignment: .center,
|
|
||||||
maximumNumberOfLines: 0,
|
|
||||||
lineSpacing: 0.0,
|
|
||||||
highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.3),
|
|
||||||
highlightAction: { attributes in
|
|
||||||
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
|
|
||||||
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
},
|
|
||||||
tapAction: { attributes, _ in
|
|
||||||
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
|
|
||||||
let controller = PremiumIntroScreen(context: accountContext, source: .giftTerms)
|
|
||||||
present(controller)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
),
|
|
||||||
environment: {},
|
|
||||||
availableSize: CGSize(width: availableWidth - sideInsets - textSideInset * 2.0, height: .greatestFiniteMagnitude),
|
|
||||||
transition: context.transition
|
|
||||||
)
|
|
||||||
context.add(termsText
|
|
||||||
.position(CGPoint(x: sideInset + environment.safeInsets.left + textSideInset + termsText.size.width / 2.0, y: context.availableSize.height - bottomPanel.size.height - termsText.size.height))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return context.availableSize
|
return context.availableSize
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ import ConfettiEffect
|
|||||||
import TextFormat
|
import TextFormat
|
||||||
import InstantPageCache
|
import InstantPageCache
|
||||||
import UniversalMediaPlayer
|
import UniversalMediaPlayer
|
||||||
|
import CheckNode
|
||||||
|
|
||||||
public enum PremiumSource: Equatable {
|
public enum PremiumSource: Equatable {
|
||||||
case settings
|
case settings
|
||||||
@ -248,7 +249,7 @@ enum PremiumPerk: CaseIterable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct PremiumIntroConfiguration {
|
struct PremiumIntroConfiguration {
|
||||||
static var defaultValue: PremiumIntroConfiguration {
|
static var defaultValue: PremiumIntroConfiguration {
|
||||||
return PremiumIntroConfiguration(perks: [
|
return PremiumIntroConfiguration(perks: [
|
||||||
.doubleLimits,
|
.doubleLimits,
|
||||||
@ -298,7 +299,282 @@ private struct PremiumIntroConfiguration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class SectionGroupComponent: Component {
|
final class PremiumOptionComponent: CombinedComponent {
|
||||||
|
let title: String
|
||||||
|
let totalPrice: String
|
||||||
|
let discount: String
|
||||||
|
let selected: Bool
|
||||||
|
let primaryTextColor: UIColor
|
||||||
|
let secondaryTextColor: UIColor
|
||||||
|
let accentColor: UIColor
|
||||||
|
let checkForegroundColor: UIColor
|
||||||
|
let checkBorderColor: UIColor
|
||||||
|
|
||||||
|
init(
|
||||||
|
title: String,
|
||||||
|
totalPrice: String,
|
||||||
|
discount: String,
|
||||||
|
selected: Bool,
|
||||||
|
primaryTextColor: UIColor,
|
||||||
|
secondaryTextColor: UIColor,
|
||||||
|
accentColor: UIColor,
|
||||||
|
checkForegroundColor: UIColor,
|
||||||
|
checkBorderColor: UIColor
|
||||||
|
) {
|
||||||
|
self.title = title
|
||||||
|
self.totalPrice = totalPrice
|
||||||
|
self.discount = discount
|
||||||
|
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.totalPrice != rhs.totalPrice {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.discount != rhs.discount {
|
||||||
|
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 discountBackground = Child(RoundedRectangle.self)
|
||||||
|
let discount = Child(MultilineTextComponent.self)
|
||||||
|
let label = Child(MultilineTextComponent.self)
|
||||||
|
|
||||||
|
return { context in
|
||||||
|
let component = context.component
|
||||||
|
|
||||||
|
let insets = UIEdgeInsets(top: 11.0, left: 46.0, bottom: 13.0, right: 16.0)
|
||||||
|
|
||||||
|
let label = label.update(
|
||||||
|
component: MultilineTextComponent(
|
||||||
|
text: .plain(
|
||||||
|
NSAttributedString(
|
||||||
|
string: component.totalPrice,
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
context.add(discountBackground
|
||||||
|
.position(CGPoint(x: insets.left + title.size.width + 6.0 + discountSize.width / 2.0, y: insets.top + title.size.height / 2.0))
|
||||||
|
)
|
||||||
|
|
||||||
|
context.add(discount
|
||||||
|
.position(CGPoint(x: insets.left + title.size.width + 6.0 + discountSize.width / 2.0, y: insets.top + title.size.height / 2.0))
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
discountSize = CGSize(width: 0.0, height: 18.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 + insets.bottom)
|
||||||
|
|
||||||
|
context.add(label
|
||||||
|
.position(CGPoint(x: context.availableSize.width - insets.right - label.size.width / 2.0, y: size.height / 2.0))
|
||||||
|
)
|
||||||
|
|
||||||
|
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: Transition) -> 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: Transition) -> CGSize {
|
||||||
|
return view.update(component: self, availableSize: availableSize, transition: transition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class SectionGroupComponent: Component {
|
||||||
public final class Item: Equatable {
|
public final class Item: Equatable {
|
||||||
public let content: AnyComponentWithIdentity<Empty>
|
public let content: AnyComponentWithIdentity<Empty>
|
||||||
public let action: () -> Void
|
public let action: () -> Void
|
||||||
@ -462,7 +738,7 @@ private final class SectionGroupComponent: Component {
|
|||||||
self.itemViews.removeValue(forKey: id)
|
self.itemViews.removeValue(forKey: id)
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.separatorViews.count > component.items.count - 1 {
|
if !self.separatorViews.isEmpty, self.separatorViews.count > component.items.count - 1 {
|
||||||
for i in (component.items.count - 1) ..< self.separatorViews.count {
|
for i in (component.items.count - 1) ..< self.separatorViews.count {
|
||||||
self.separatorViews[i].removeFromSuperview()
|
self.separatorViews[i].removeFromSuperview()
|
||||||
}
|
}
|
||||||
@ -484,7 +760,7 @@ private final class SectionGroupComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class PerkComponent: CombinedComponent {
|
final class PerkComponent: CombinedComponent {
|
||||||
let iconName: String
|
let iconName: String
|
||||||
let iconBackgroundColors: [UIColor]
|
let iconBackgroundColors: [UIColor]
|
||||||
let title: String
|
let title: String
|
||||||
@ -646,18 +922,22 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
|||||||
let source: PremiumSource
|
let source: PremiumSource
|
||||||
let isPremium: Bool?
|
let isPremium: Bool?
|
||||||
let otherPeerName: String?
|
let otherPeerName: String?
|
||||||
let price: String?
|
let products: [InAppPurchaseManager.Product]?
|
||||||
|
let selectedProductId: String?
|
||||||
let present: (ViewController) -> Void
|
let present: (ViewController) -> Void
|
||||||
|
let selectProduct: (String) -> Void
|
||||||
let buy: () -> Void
|
let buy: () -> Void
|
||||||
let updateIsFocused: (Bool) -> Void
|
let updateIsFocused: (Bool) -> Void
|
||||||
|
|
||||||
init(context: AccountContext, source: PremiumSource, isPremium: Bool?, otherPeerName: String?, price: String?, present: @escaping (ViewController) -> Void, buy: @escaping () -> Void, updateIsFocused: @escaping (Bool) -> Void) {
|
init(context: AccountContext, source: PremiumSource, isPremium: Bool?, otherPeerName: String?, products: [InAppPurchaseManager.Product]?, selectedProductId: String?, present: @escaping (ViewController) -> Void, selectProduct: @escaping (String) -> Void, buy: @escaping () -> Void, updateIsFocused: @escaping (Bool) -> Void) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.source = source
|
self.source = source
|
||||||
self.isPremium = isPremium
|
self.isPremium = isPremium
|
||||||
self.otherPeerName = otherPeerName
|
self.otherPeerName = otherPeerName
|
||||||
self.price = price
|
self.products = products
|
||||||
|
self.selectedProductId = selectedProductId
|
||||||
self.present = present
|
self.present = present
|
||||||
|
self.selectProduct = selectProduct
|
||||||
self.buy = buy
|
self.buy = buy
|
||||||
self.updateIsFocused = updateIsFocused
|
self.updateIsFocused = updateIsFocused
|
||||||
}
|
}
|
||||||
@ -675,7 +955,10 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
|||||||
if lhs.otherPeerName != rhs.otherPeerName {
|
if lhs.otherPeerName != rhs.otherPeerName {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if lhs.price != rhs.price {
|
if lhs.products != rhs.products {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.selectedProductId != rhs.selectedProductId {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -685,7 +968,9 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
|||||||
final class State: ComponentState {
|
final class State: ComponentState {
|
||||||
private let context: AccountContext
|
private let context: AccountContext
|
||||||
|
|
||||||
var price: String?
|
var products: [InAppPurchaseManager.Product]?
|
||||||
|
var selectedProductId: String?
|
||||||
|
|
||||||
var isPremium: Bool?
|
var isPremium: Bool?
|
||||||
|
|
||||||
private var disposable: Disposable?
|
private var disposable: Disposable?
|
||||||
@ -695,6 +980,10 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
|||||||
private var stickersDisposable: Disposable?
|
private var stickersDisposable: Disposable?
|
||||||
private var preloadDisposableSet = DisposableSet()
|
private var preloadDisposableSet = DisposableSet()
|
||||||
|
|
||||||
|
var price: String? {
|
||||||
|
return self.products?.first(where: { $0.id == self.selectedProductId })?.price
|
||||||
|
}
|
||||||
|
|
||||||
init(context: AccountContext, source: PremiumSource) {
|
init(context: AccountContext, source: PremiumSource) {
|
||||||
self.context = context
|
self.context = context
|
||||||
|
|
||||||
@ -773,7 +1062,8 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
|||||||
let overscroll = Child(Rectangle.self)
|
let overscroll = Child(Rectangle.self)
|
||||||
let fade = Child(RoundedRectangle.self)
|
let fade = Child(RoundedRectangle.self)
|
||||||
let text = Child(MultilineTextComponent.self)
|
let text = Child(MultilineTextComponent.self)
|
||||||
let section = Child(SectionGroupComponent.self)
|
let optionsSection = Child(SectionGroupComponent.self)
|
||||||
|
let perksSection = Child(SectionGroupComponent.self)
|
||||||
let infoBackground = Child(RoundedRectangle.self)
|
let infoBackground = Child(RoundedRectangle.self)
|
||||||
let infoTitle = Child(MultilineTextComponent.self)
|
let infoTitle = Child(MultilineTextComponent.self)
|
||||||
let infoText = Child(MultilineTextComponent.self)
|
let infoText = Child(MultilineTextComponent.self)
|
||||||
@ -785,7 +1075,8 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
|||||||
let scrollEnvironment = context.environment[ScrollChildEnvironment.self].value
|
let scrollEnvironment = context.environment[ScrollChildEnvironment.self].value
|
||||||
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
|
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
|
||||||
let state = context.state
|
let state = context.state
|
||||||
state.price = context.component.price
|
state.products = context.component.products
|
||||||
|
state.selectedProductId = context.component.selectedProductId
|
||||||
state.isPremium = context.component.isPremium
|
state.isPremium = context.component.isPremium
|
||||||
|
|
||||||
let theme = environment.theme
|
let theme = environment.theme
|
||||||
@ -886,18 +1177,101 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
|||||||
UIColor(rgb: 0x54A3FF),
|
UIColor(rgb: 0x54A3FF),
|
||||||
UIColor(rgb: 0x54A3FF)
|
UIColor(rgb: 0x54A3FF)
|
||||||
]
|
]
|
||||||
|
|
||||||
var items: [SectionGroupComponent.Item] = []
|
|
||||||
|
|
||||||
let accountContext = context.component.context
|
let accountContext = context.component.context
|
||||||
let present = context.component.present
|
let present = context.component.present
|
||||||
|
let selectProduct = context.component.selectProduct
|
||||||
let buy = context.component.buy
|
let buy = context.component.buy
|
||||||
let updateIsFocused = context.component.updateIsFocused
|
let updateIsFocused = context.component.updateIsFocused
|
||||||
|
|
||||||
|
if state.isPremium == true {
|
||||||
|
|
||||||
|
} else if let products = state.products {
|
||||||
|
var optionsItems: [SectionGroupComponent.Item] = []
|
||||||
|
let gradientColors: [UIColor] = [
|
||||||
|
UIColor(rgb: 0x8e77ff),
|
||||||
|
UIColor(rgb: 0x9a6fff),
|
||||||
|
UIColor(rgb: 0xb36eee)
|
||||||
|
]
|
||||||
|
|
||||||
|
let shortestOptionPrice: Int64
|
||||||
|
if let product = products.first(where: { $0.id.hasSuffix(".monthly") }) {
|
||||||
|
shortestOptionPrice = Int64(Float(product.priceCurrencyAndAmount.amount))
|
||||||
|
} else {
|
||||||
|
shortestOptionPrice = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
var i = 0
|
||||||
|
for product in products {
|
||||||
|
let giftTitle: String
|
||||||
|
let months: Float
|
||||||
|
if product.id.hasSuffix(".monthly") {
|
||||||
|
giftTitle = strings.Premium_Monthly
|
||||||
|
months = 1
|
||||||
|
} else {
|
||||||
|
giftTitle = strings.Premium_Annual
|
||||||
|
months = 12
|
||||||
|
}
|
||||||
|
|
||||||
|
let discountValue = Int((1.0 - Float(product.priceCurrencyAndAmount.amount) / months / Float(shortestOptionPrice)) * 100.0)
|
||||||
|
let discount: String
|
||||||
|
if discountValue > 0 {
|
||||||
|
discount = "-\(discountValue)%"
|
||||||
|
} else {
|
||||||
|
discount = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
optionsItems.append(
|
||||||
|
SectionGroupComponent.Item(
|
||||||
|
AnyComponentWithIdentity(
|
||||||
|
id: product.id,
|
||||||
|
component: AnyComponent(
|
||||||
|
PremiumOptionComponent(
|
||||||
|
title: giftTitle,
|
||||||
|
totalPrice: product.price,
|
||||||
|
discount: discount,
|
||||||
|
selected: product.id == state.selectedProductId,
|
||||||
|
primaryTextColor: textColor,
|
||||||
|
secondaryTextColor: subtitleColor,
|
||||||
|
accentColor: gradientColors[i],
|
||||||
|
checkForegroundColor: environment.theme.list.itemCheckColors.foregroundColor,
|
||||||
|
checkBorderColor: environment.theme.list.itemCheckColors.strokeColor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
action: {
|
||||||
|
selectProduct(product.id)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
let optionsSection = optionsSection.update(
|
||||||
|
component: SectionGroupComponent(
|
||||||
|
items: optionsItems,
|
||||||
|
backgroundColor: environment.theme.list.itemBlocksBackgroundColor,
|
||||||
|
selectionColor: environment.theme.list.itemHighlightedBackgroundColor,
|
||||||
|
separatorColor: environment.theme.list.itemBlocksSeparatorColor
|
||||||
|
),
|
||||||
|
environment: {},
|
||||||
|
availableSize: CGSize(width: availableWidth - sideInsets, height: .greatestFiniteMagnitude),
|
||||||
|
transition: context.transition
|
||||||
|
)
|
||||||
|
context.add(optionsSection
|
||||||
|
.position(CGPoint(x: availableWidth / 2.0, y: size.height + optionsSection.size.height / 2.0))
|
||||||
|
.clipsToBounds(true)
|
||||||
|
.cornerRadius(10.0)
|
||||||
|
)
|
||||||
|
size.height += optionsSection.size.height
|
||||||
|
size.height += 26.0
|
||||||
|
}
|
||||||
|
|
||||||
var i = 0
|
var i = 0
|
||||||
|
var perksItems: [SectionGroupComponent.Item] = []
|
||||||
for perk in state.configuration.perks {
|
for perk in state.configuration.perks {
|
||||||
let iconBackgroundColors = gradientColors[i]
|
let iconBackgroundColors = gradientColors[i]
|
||||||
items.append(SectionGroupComponent.Item(
|
perksItems.append(SectionGroupComponent.Item(
|
||||||
AnyComponentWithIdentity(
|
AnyComponentWithIdentity(
|
||||||
id: perk.identifier,
|
id: perk.identifier,
|
||||||
component: AnyComponent(
|
component: AnyComponent(
|
||||||
@ -982,9 +1356,9 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
|||||||
i += 1
|
i += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
let section = section.update(
|
let perksSection = perksSection.update(
|
||||||
component: SectionGroupComponent(
|
component: SectionGroupComponent(
|
||||||
items: items,
|
items: perksItems,
|
||||||
backgroundColor: environment.theme.list.itemBlocksBackgroundColor,
|
backgroundColor: environment.theme.list.itemBlocksBackgroundColor,
|
||||||
selectionColor: environment.theme.list.itemHighlightedBackgroundColor,
|
selectionColor: environment.theme.list.itemHighlightedBackgroundColor,
|
||||||
separatorColor: environment.theme.list.itemBlocksSeparatorColor
|
separatorColor: environment.theme.list.itemBlocksSeparatorColor
|
||||||
@ -993,12 +1367,12 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
|||||||
availableSize: CGSize(width: availableWidth - sideInsets, height: .greatestFiniteMagnitude),
|
availableSize: CGSize(width: availableWidth - sideInsets, height: .greatestFiniteMagnitude),
|
||||||
transition: context.transition
|
transition: context.transition
|
||||||
)
|
)
|
||||||
context.add(section
|
context.add(perksSection
|
||||||
.position(CGPoint(x: availableWidth / 2.0, y: size.height + section.size.height / 2.0))
|
.position(CGPoint(x: availableWidth / 2.0, y: size.height + perksSection.size.height / 2.0))
|
||||||
.clipsToBounds(true)
|
.clipsToBounds(true)
|
||||||
.cornerRadius(10.0)
|
.cornerRadius(10.0)
|
||||||
)
|
)
|
||||||
size.height += section.size.height
|
size.height += perksSection.size.height
|
||||||
size.height += 23.0
|
size.height += 23.0
|
||||||
|
|
||||||
let textSideInset: CGFloat = 16.0
|
let textSideInset: CGFloat = 16.0
|
||||||
@ -1244,7 +1618,10 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
|||||||
var hasIdleAnimations = true
|
var hasIdleAnimations = true
|
||||||
|
|
||||||
var inProgress = false
|
var inProgress = false
|
||||||
var premiumProduct: InAppPurchaseManager.Product?
|
|
||||||
|
var products: [InAppPurchaseManager.Product]?
|
||||||
|
var selectedProductId: String?
|
||||||
|
|
||||||
var isPremium: Bool?
|
var isPremium: Bool?
|
||||||
var otherPeerName: String?
|
var otherPeerName: String?
|
||||||
|
|
||||||
@ -1252,6 +1629,10 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
|||||||
private var paymentDisposable = MetaDisposable()
|
private var paymentDisposable = MetaDisposable()
|
||||||
private var activationDisposable = MetaDisposable()
|
private var activationDisposable = MetaDisposable()
|
||||||
|
|
||||||
|
var price: String? {
|
||||||
|
return self.products?.first(where: { $0.id == self.selectedProductId })?.price
|
||||||
|
}
|
||||||
|
|
||||||
init(context: AccountContext, source: PremiumSource, updateInProgress: @escaping (Bool) -> Void, present: @escaping (ViewController) -> Void, completion: @escaping () -> Void) {
|
init(context: AccountContext, source: PremiumSource, updateInProgress: @escaping (Bool) -> Void, present: @escaping (ViewController) -> Void, completion: @escaping () -> Void) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.updateInProgress = updateInProgress
|
self.updateInProgress = updateInProgress
|
||||||
@ -1295,7 +1676,10 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
|||||||
otherPeerName
|
otherPeerName
|
||||||
).start(next: { [weak self] products, isPremium, otherPeerName in
|
).start(next: { [weak self] products, isPremium, otherPeerName in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
strongSelf.premiumProduct = products.first(where: { $0.isSubscription })
|
if strongSelf.products == nil {
|
||||||
|
strongSelf.selectedProductId = products.first?.id
|
||||||
|
}
|
||||||
|
strongSelf.products = products.filter { $0.isSubscription }
|
||||||
strongSelf.isPremium = isPremium
|
strongSelf.isPremium = isPremium
|
||||||
strongSelf.otherPeerName = otherPeerName
|
strongSelf.otherPeerName = otherPeerName
|
||||||
strongSelf.updated(transition: .immediate)
|
strongSelf.updated(transition: .immediate)
|
||||||
@ -1311,7 +1695,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
|||||||
|
|
||||||
func buy() {
|
func buy() {
|
||||||
guard let inAppPurchaseManager = self.context.inAppPurchaseManager,
|
guard let inAppPurchaseManager = self.context.inAppPurchaseManager,
|
||||||
let premiumProduct = self.premiumProduct, !self.inProgress else {
|
let premiumProduct = self.products?.first(where: { $0.id == self.selectedProductId }), !self.inProgress else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1412,6 +1796,11 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
|||||||
self.hasIdleAnimations = !isFocused
|
self.hasIdleAnimations = !isFocused
|
||||||
self.updated(transition: .immediate)
|
self.updated(transition: .immediate)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func selectProduct(_ productId: String) {
|
||||||
|
self.selectedProductId = productId
|
||||||
|
self.updated(transition: .immediate)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeState() -> State {
|
func makeState() -> State {
|
||||||
@ -1543,7 +1932,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
|||||||
let bottomPanelPadding: CGFloat = 12.0
|
let bottomPanelPadding: CGFloat = 12.0
|
||||||
let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding
|
let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding
|
||||||
let bottomPanelHeight: CGFloat = state.isPremium == true ? bottomInset : bottomPanelPadding + 50.0 + bottomInset
|
let bottomPanelHeight: CGFloat = state.isPremium == true ? bottomInset : bottomPanelPadding + 50.0 + bottomInset
|
||||||
|
|
||||||
let scrollContent = scrollContent.update(
|
let scrollContent = scrollContent.update(
|
||||||
component: ScrollComponent<EnvironmentType>(
|
component: ScrollComponent<EnvironmentType>(
|
||||||
content: AnyComponent(PremiumIntroScreenContentComponent(
|
content: AnyComponent(PremiumIntroScreenContentComponent(
|
||||||
@ -1551,8 +1940,12 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
|||||||
source: context.component.source,
|
source: context.component.source,
|
||||||
isPremium: state.isPremium,
|
isPremium: state.isPremium,
|
||||||
otherPeerName: state.otherPeerName,
|
otherPeerName: state.otherPeerName,
|
||||||
price: state.premiumProduct?.price,
|
products: state.products,
|
||||||
|
selectedProductId: state.selectedProductId,
|
||||||
present: context.component.present,
|
present: context.component.present,
|
||||||
|
selectProduct: { [weak state] productId in
|
||||||
|
state?.selectProduct(productId)
|
||||||
|
},
|
||||||
buy: { [weak state] in
|
buy: { [weak state] in
|
||||||
state?.buy()
|
state?.buy()
|
||||||
},
|
},
|
||||||
@ -1655,7 +2048,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
|||||||
let sideInset: CGFloat = 16.0
|
let sideInset: CGFloat = 16.0
|
||||||
let button = button.update(
|
let button = button.update(
|
||||||
component: SolidRoundedButtonComponent(
|
component: SolidRoundedButtonComponent(
|
||||||
title: environment.strings.Premium_SubscribeFor(state.premiumProduct?.price ?? "—").string,
|
title: environment.strings.Premium_SubscribeFor(state.price ?? "—").string,
|
||||||
theme: SolidRoundedButtonComponent.Theme(
|
theme: SolidRoundedButtonComponent.Theme(
|
||||||
backgroundColor: UIColor(rgb: 0x8878ff),
|
backgroundColor: UIColor(rgb: 0x8878ff),
|
||||||
backgroundColors: [
|
backgroundColors: [
|
||||||
|
@ -46,16 +46,18 @@ private func generateDiffuseTexture() -> UIImage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class PremiumStarComponent: Component {
|
class PremiumStarComponent: Component {
|
||||||
|
let isIntro: Bool
|
||||||
let isVisible: Bool
|
let isVisible: Bool
|
||||||
let hasIdleAnimations: Bool
|
let hasIdleAnimations: Bool
|
||||||
|
|
||||||
init(isVisible: Bool, hasIdleAnimations: Bool) {
|
init(isIntro: Bool, isVisible: Bool, hasIdleAnimations: Bool) {
|
||||||
|
self.isIntro = isIntro
|
||||||
self.isVisible = isVisible
|
self.isVisible = isVisible
|
||||||
self.hasIdleAnimations = hasIdleAnimations
|
self.hasIdleAnimations = hasIdleAnimations
|
||||||
}
|
}
|
||||||
|
|
||||||
static func ==(lhs: PremiumStarComponent, rhs: PremiumStarComponent) -> Bool {
|
static func ==(lhs: PremiumStarComponent, rhs: PremiumStarComponent) -> Bool {
|
||||||
return lhs.isVisible == rhs.isVisible && lhs.hasIdleAnimations == rhs.hasIdleAnimations
|
return lhs.isIntro == rhs.isIntro && lhs.isVisible == rhs.isVisible && lhs.hasIdleAnimations == rhs.hasIdleAnimations
|
||||||
}
|
}
|
||||||
|
|
||||||
final class View: UIView, SCNSceneRendererDelegate, ComponentTaggedView {
|
final class View: UIView, SCNSceneRendererDelegate, ComponentTaggedView {
|
||||||
@ -84,7 +86,11 @@ class PremiumStarComponent: Component {
|
|||||||
private var timer: SwiftSignalKit.Timer?
|
private var timer: SwiftSignalKit.Timer?
|
||||||
private var hasIdleAnimations = false
|
private var hasIdleAnimations = false
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
private let isIntro: Bool
|
||||||
|
|
||||||
|
init(frame: CGRect, isIntro: Bool) {
|
||||||
|
self.isIntro = isIntro
|
||||||
|
|
||||||
self.sceneView = SCNView(frame: CGRect(origin: .zero, size: CGSize(width: 64.0, height: 64.0)))
|
self.sceneView = SCNView(frame: CGRect(origin: .zero, size: CGSize(width: 64.0, height: 64.0)))
|
||||||
self.sceneView.backgroundColor = .clear
|
self.sceneView.backgroundColor = .clear
|
||||||
self.sceneView.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
|
self.sceneView.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
|
||||||
@ -433,30 +439,48 @@ class PremiumStarComponent: Component {
|
|||||||
self.previousInteractionTimestamp = currentTime
|
self.previousInteractionTimestamp = currentTime
|
||||||
self.delayTapsTill = currentTime + 0.85
|
self.delayTapsTill = currentTime + 0.85
|
||||||
|
|
||||||
if explode, let node = scene.rootNode.childNode(withName: "swirl", recursively: false), let particlesLeft = scene.rootNode.childNode(withName: "particles_left", recursively: false), let particlesRight = scene.rootNode.childNode(withName: "particles_right", recursively: false) {
|
if explode, let node = scene.rootNode.childNode(withName: "swirl", recursively: false), let particlesLeft = scene.rootNode.childNode(withName: "particles_left", recursively: false), let particlesRight = scene.rootNode.childNode(withName: "particles_right", recursively: false), let particlesBottomLeft = scene.rootNode.childNode(withName: "particles_left_bottom", recursively: false), let particlesBottomRight = scene.rootNode.childNode(withName: "particles_right_bottom", recursively: false) {
|
||||||
if let leftParticleSystem = particlesLeft.particleSystems?.first, let rightParticleSystem = particlesRight.particleSystems?.first {
|
if let leftParticleSystem = particlesLeft.particleSystems?.first, let rightParticleSystem = particlesRight.particleSystems?.first, let leftBottomParticleSystem = particlesBottomLeft.particleSystems?.first, let rightBottomParticleSystem = particlesBottomRight.particleSystems?.first {
|
||||||
leftParticleSystem.speedFactor = 1.3
|
leftParticleSystem.speedFactor = 2.0
|
||||||
leftParticleSystem.particleVelocity = 2.4
|
leftParticleSystem.particleVelocity = 1.6
|
||||||
leftParticleSystem.birthRate = 24.0
|
leftParticleSystem.birthRate = 60.0
|
||||||
leftParticleSystem.particleLifeSpan = 4.0
|
leftParticleSystem.particleLifeSpan = 4.0
|
||||||
|
|
||||||
rightParticleSystem.speedFactor = 1.3
|
rightParticleSystem.speedFactor = 2.0
|
||||||
rightParticleSystem.particleVelocity = 2.4
|
rightParticleSystem.particleVelocity = 1.6
|
||||||
rightParticleSystem.birthRate = 24.0
|
rightParticleSystem.birthRate = 60.0
|
||||||
rightParticleSystem.particleLifeSpan = 4.0
|
rightParticleSystem.particleLifeSpan = 4.0
|
||||||
|
|
||||||
|
// leftBottomParticleSystem.speedFactor = 2.0
|
||||||
|
leftBottomParticleSystem.particleVelocity = 1.6
|
||||||
|
leftBottomParticleSystem.birthRate = 24.0
|
||||||
|
leftBottomParticleSystem.particleLifeSpan = 7.0
|
||||||
|
|
||||||
|
// rightBottomParticleSystem.speedFactor = 2.0
|
||||||
|
rightBottomParticleSystem.particleVelocity = 1.6
|
||||||
|
rightBottomParticleSystem.birthRate = 24.0
|
||||||
|
rightBottomParticleSystem.particleLifeSpan = 7.0
|
||||||
|
|
||||||
node.physicsField?.isActive = true
|
node.physicsField?.isActive = true
|
||||||
Queue.mainQueue().after(1.0) {
|
Queue.mainQueue().after(1.0) {
|
||||||
node.physicsField?.isActive = false
|
node.physicsField?.isActive = false
|
||||||
|
|
||||||
leftParticleSystem.birthRate = 9.0
|
leftParticleSystem.birthRate = 12.0
|
||||||
leftParticleSystem.particleVelocity = 1.2
|
leftParticleSystem.particleVelocity = 1.2
|
||||||
leftParticleSystem.particleLifeSpan = 3.0
|
leftParticleSystem.particleLifeSpan = 3.0
|
||||||
|
|
||||||
rightParticleSystem.birthRate = 9.0
|
rightParticleSystem.birthRate = 12.0
|
||||||
rightParticleSystem.particleVelocity = 1.2
|
rightParticleSystem.particleVelocity = 1.2
|
||||||
rightParticleSystem.particleLifeSpan = 3.0
|
rightParticleSystem.particleLifeSpan = 3.0
|
||||||
|
|
||||||
|
leftBottomParticleSystem.particleVelocity = 1.2
|
||||||
|
leftBottomParticleSystem.birthRate = 7.0
|
||||||
|
leftBottomParticleSystem.particleLifeSpan = 5.0
|
||||||
|
|
||||||
|
rightBottomParticleSystem.particleVelocity = 1.2
|
||||||
|
rightBottomParticleSystem.birthRate = 7.0
|
||||||
|
rightBottomParticleSystem.particleLifeSpan = 5.0
|
||||||
|
|
||||||
let leftAnimation = POPBasicAnimation()
|
let leftAnimation = POPBasicAnimation()
|
||||||
leftAnimation.property = (POPAnimatableProperty.property(withName: "speedFactor", initializer: { property in
|
leftAnimation.property = (POPAnimatableProperty.property(withName: "speedFactor", initializer: { property in
|
||||||
property?.readBlock = { particleSystem, values in
|
property?.readBlock = { particleSystem, values in
|
||||||
@ -541,7 +565,7 @@ class PremiumStarComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func makeView() -> View {
|
func makeView() -> View {
|
||||||
return View(frame: CGRect())
|
return View(frame: CGRect(), isIntro: self.isIntro)
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||||
|
@ -691,9 +691,13 @@ private final class StickerPackContainer: ASDisplayNode {
|
|||||||
|
|
||||||
let backgroundAlpha: CGFloat
|
let backgroundAlpha: CGFloat
|
||||||
switch offset {
|
switch offset {
|
||||||
case let .known(value):
|
case .known:
|
||||||
let bottomOffsetY = max(0.0, self.gridNode.scrollView.contentSize.height + self.gridNode.scrollView.contentInset.top + self.gridNode.scrollView.contentInset.bottom - value - self.gridNode.scrollView.frame.height - 10.0)
|
let topPosition = self.view.convert(self.topContainerNode.frame, to: self.view).minY
|
||||||
backgroundAlpha = min(10.0, bottomOffsetY) / 10.0
|
let bottomPosition = self.actionAreaBackgroundNode.view.convert(self.actionAreaBackgroundNode.bounds, to: self.view).minY
|
||||||
|
let bottomEdgePosition = topPosition + self.topContainerNode.frame.height + self.gridNode.scrollView.contentSize.height
|
||||||
|
let bottomOffset = bottomPosition - bottomEdgePosition
|
||||||
|
|
||||||
|
backgroundAlpha = min(10.0, max(0.0, -1.0 * bottomOffset)) / 10.0
|
||||||
case .unknown, .none:
|
case .unknown, .none:
|
||||||
backgroundAlpha = 1.0
|
backgroundAlpha = 1.0
|
||||||
}
|
}
|
||||||
|
@ -712,7 +712,9 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||||||
if !alreadySeen {
|
if !alreadySeen {
|
||||||
item.controllerInteraction.seenOneTimeAnimatedMedia.insert(item.message.id)
|
item.controllerInteraction.seenOneTimeAnimatedMedia.insert(item.message.id)
|
||||||
if let emojiString = self.emojiString, emojiString.count == 1 {
|
if let emojiString = self.emojiString, emojiString.count == 1 {
|
||||||
self.playAdditionalEmojiAnimation(index: 1)
|
if item.message.id.peerId.namespace == Namespaces.Peer.CloudUser {
|
||||||
|
self.playAdditionalEmojiAnimation(index: 1)
|
||||||
|
}
|
||||||
} else if let file = file, file.isPremiumSticker {
|
} else if let file = file, file.isPremiumSticker {
|
||||||
Queue.mainQueue().after(0.1) {
|
Queue.mainQueue().after(0.1) {
|
||||||
self.playPremiumStickerAnimation()
|
self.playPremiumStickerAnimation()
|
||||||
@ -979,7 +981,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||||||
|
|
||||||
tmpWidth -= deliveryFailedInset
|
tmpWidth -= deliveryFailedInset
|
||||||
|
|
||||||
let maximumContentWidth = floor(tmpWidth - layoutConstants.bubble.edgeInset - layoutConstants.bubble.edgeInset - layoutConstants.bubble.contentInsets.left - layoutConstants.bubble.contentInsets.right - avatarInset) - 70.0
|
let maximumContentWidth = floor(tmpWidth - layoutConstants.bubble.edgeInset - layoutConstants.bubble.edgeInset - layoutConstants.bubble.contentInsets.left - layoutConstants.bubble.contentInsets.right - avatarInset)
|
||||||
|
|
||||||
let font = Font.regular(fontSizeForEmojiString(item.message.text))
|
let font = Font.regular(fontSizeForEmojiString(item.message.text))
|
||||||
let attributedText = stringWithAppliedEntities(item.message.text, entities: item.message.textEntitiesAttribute?.entities ?? [], baseColor: .black, linkColor: .black, baseFont: font, linkFont: font, boldFont: font, italicFont: font, boldItalicFont: font, fixedFont: font, blockQuoteFont: font, message: item.message)
|
let attributedText = stringWithAppliedEntities(item.message.text, entities: item.message.textEntitiesAttribute?.entities ?? [], baseColor: .black, linkColor: .black, baseFont: font, linkFont: font, boldFont: font, italicFont: font, boldItalicFont: font, fixedFont: font, blockQuoteFont: font, message: item.message)
|
||||||
@ -2658,23 +2660,23 @@ private func fontSizeForEmojiString(_ string: String) -> CGFloat {
|
|||||||
case 1:
|
case 1:
|
||||||
multiplier = 1.0
|
multiplier = 1.0
|
||||||
case 2:
|
case 2:
|
||||||
multiplier = 0.7
|
multiplier = 0.84
|
||||||
case 3:
|
case 3:
|
||||||
multiplier = 0.52
|
multiplier = 0.69
|
||||||
case 4:
|
case 4:
|
||||||
multiplier = 0.37
|
multiplier = 0.53
|
||||||
case 5:
|
case 5:
|
||||||
multiplier = 0.28
|
multiplier = 0.46
|
||||||
case 6:
|
case 6:
|
||||||
multiplier = 0.25
|
multiplier = 0.38
|
||||||
case 7:
|
case 7:
|
||||||
multiplier = 0.23
|
multiplier = 0.32
|
||||||
case 8:
|
case 8:
|
||||||
multiplier = 0.21
|
multiplier = 0.27
|
||||||
case 9:
|
case 9:
|
||||||
multiplier = 0.19
|
multiplier = 0.24
|
||||||
default:
|
default:
|
||||||
multiplier = 0.19
|
multiplier = 0.21
|
||||||
}
|
}
|
||||||
return floor(basicSize * multiplier)
|
return floor(basicSize * multiplier)
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user