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";
|
||||
|
||||
"StickerPacksSettings.SuggestAnimatedEmoji" = "Suggest Animated Emoji";
|
||||
|
||||
"Premium.Annual" = "Annual";
|
||||
"Premium.Monthly" = "Monthly";
|
||||
|
@ -2,10 +2,33 @@ import Foundation
|
||||
import CoreText
|
||||
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 {
|
||||
var isEmoji: Bool {
|
||||
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
|
||||
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
|
||||
@ -44,35 +67,38 @@ public extension String {
|
||||
}
|
||||
|
||||
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 {
|
||||
return self.unicodeScalars.contains { $0.isEmoji }
|
||||
return self.contains { $0.isEmoji }
|
||||
//return self.unicodeScalars.contains { $0.isEmoji }
|
||||
}
|
||||
|
||||
var containsOnlyEmoji: Bool {
|
||||
guard !self.isEmpty else {
|
||||
return false
|
||||
}
|
||||
var nextShouldBeVariationSelector = false
|
||||
for scalar in self.unicodeScalars {
|
||||
if nextShouldBeVariationSelector {
|
||||
if scalar == UnicodeScalar.VariationSelector {
|
||||
nextShouldBeVariationSelector = false
|
||||
continue
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if !scalar.isEmoji && scalar.maybeEmoji {
|
||||
nextShouldBeVariationSelector = true
|
||||
}
|
||||
else if !scalar.isEmoji && scalar != UnicodeScalar.ZeroWidthJoiner {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return !nextShouldBeVariationSelector
|
||||
return !self.isEmpty && !self.contains { !$0.isEmoji }
|
||||
// guard !self.isEmpty else {
|
||||
// return false
|
||||
// }
|
||||
// var nextShouldBeVariationSelector = false
|
||||
// for scalar in self.unicodeScalars {
|
||||
// if nextShouldBeVariationSelector {
|
||||
// if scalar == UnicodeScalar.VariationSelector {
|
||||
// nextShouldBeVariationSelector = false
|
||||
// continue
|
||||
// } else {
|
||||
// return false
|
||||
// }
|
||||
// }
|
||||
// if !scalar.isEmoji && scalar.maybeEmoji {
|
||||
// nextShouldBeVariationSelector = true
|
||||
// }
|
||||
// else if !scalar.isEmoji && scalar != UnicodeScalar.ZeroWidthJoiner {
|
||||
// return false
|
||||
// }
|
||||
// }
|
||||
// return !nextShouldBeVariationSelector
|
||||
}
|
||||
|
||||
var emojis: [String] {
|
||||
|
@ -7,6 +7,7 @@ import TelegramCore
|
||||
import TelegramStringFormatting
|
||||
|
||||
private let productIdentifiers = [
|
||||
"org.telegram.telegramPremium.annual",
|
||||
"org.telegram.telegramPremium.monthly",
|
||||
"org.telegram.telegramPremium.twelveMonths",
|
||||
"org.telegram.telegramPremium.sixMonths",
|
||||
@ -50,7 +51,7 @@ public final class InAppPurchaseManager: NSObject {
|
||||
} else if #available(iOS 11.2, *) {
|
||||
return self.skProduct.subscriptionPeriod != nil
|
||||
} 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.setup()
|
||||
|
||||
let panGestureRecoginzer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))
|
||||
self.addGestureRecognizer(panGestureRecoginzer)
|
||||
|
||||
|
||||
let tapGestureRecoginzer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))
|
||||
self.addGestureRecognizer(tapGestureRecoginzer)
|
||||
|
||||
@ -134,58 +131,6 @@ class GiftAvatarComponent: Component {
|
||||
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() {
|
||||
guard let url = getAppBundle().url(forResource: "gift", withExtension: "scn"), let scene = try? SCNScene(url: url, options: nil) else {
|
||||
return
|
||||
@ -210,6 +155,8 @@ class GiftAvatarComponent: Component {
|
||||
}
|
||||
|
||||
private func onReady() {
|
||||
self.setupScaleAnimation()
|
||||
|
||||
self.playAppearanceAnimation(explode: true)
|
||||
|
||||
self.previousInteractionTimestamp = CACurrentMediaTime()
|
||||
@ -224,6 +171,18 @@ class GiftAvatarComponent: Component {
|
||||
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) {
|
||||
guard let scene = self.sceneView.scene else {
|
||||
return
|
||||
@ -233,23 +192,50 @@ class GiftAvatarComponent: Component {
|
||||
self.previousInteractionTimestamp = currentTime
|
||||
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 let particleSystem = particles.particleSystems?.first {
|
||||
particleSystem.particleColorVariation = SCNVector4(0.15, 0.2, 0.15, 0.3)
|
||||
particleSystem.speedFactor = 2.0
|
||||
particleSystem.particleVelocity = 2.2
|
||||
particleSystem.birthRate = 4.0
|
||||
particleSystem.particleLifeSpan = 2.0
|
||||
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, let leftBottomParticleSystem = particlesBottomLeft.particleSystems?.first, let rightBottomParticleSystem = particlesBottomRight.particleSystems?.first {
|
||||
leftParticleSystem.speedFactor = 2.0
|
||||
leftParticleSystem.particleVelocity = 1.6
|
||||
leftParticleSystem.birthRate = 60.0
|
||||
leftParticleSystem.particleLifeSpan = 4.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
|
||||
Queue.mainQueue().after(1.0) {
|
||||
node.physicsField?.isActive = false
|
||||
particles.particleSystems?.first?.birthRate = 1.2
|
||||
particleSystem.particleVelocity = 1.0
|
||||
particleSystem.particleLifeSpan = 4.0
|
||||
|
||||
let animation = POPBasicAnimation()
|
||||
animation.property = (POPAnimatableProperty.property(withName: "speedFactor", initializer: { property in
|
||||
leftParticleSystem.birthRate = 12.0
|
||||
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
|
||||
values?.pointee = (particleSystem as! SCNParticleSystem).speedFactor
|
||||
}
|
||||
@ -258,11 +244,27 @@ class GiftAvatarComponent: Component {
|
||||
}
|
||||
property?.threshold = 0.01
|
||||
}) as! POPAnimatableProperty)
|
||||
animation.fromValue = 2.0 as NSNumber
|
||||
animation.toValue = 1.0 as NSNumber
|
||||
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
|
||||
animation.duration = 0.5
|
||||
particleSystem.pop_add(animation, forKey: "speedFactor")
|
||||
leftAnimation.fromValue = 1.2 as NSNumber
|
||||
leftAnimation.toValue = 0.85 as NSNumber
|
||||
leftAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
|
||||
leftAnimation.duration = 0.5
|
||||
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] }
|
||||
let index: Int
|
||||
switch component.source {
|
||||
case .intro:
|
||||
case .intro, .gift:
|
||||
index = items.firstIndex(where: { (component.subject as AnyHashable) == $0.content.id }) ?? 0
|
||||
case .other:
|
||||
items = items.filter { item in
|
||||
@ -981,6 +981,8 @@ private final class DemoSheetContent: CombinedComponent {
|
||||
switch component.source {
|
||||
case let .intro(price):
|
||||
buttonText = strings.Premium_SubscribeFor(price ?? "–").string
|
||||
case let .gift(price):
|
||||
buttonText = strings.Premium_Gift_GiftSubscription(price ?? "–").string
|
||||
case .other:
|
||||
switch component.subject {
|
||||
case .uniqueReactions:
|
||||
@ -1173,6 +1175,7 @@ public class PremiumDemoScreen: ViewControllerComponentContainer {
|
||||
|
||||
public enum Source: Equatable {
|
||||
case intro(String?)
|
||||
case gift(String?)
|
||||
case other
|
||||
}
|
||||
|
||||
|
@ -17,498 +17,7 @@ import Markdown
|
||||
import InAppPurchaseManager
|
||||
import ConfettiEffect
|
||||
import TextFormat
|
||||
import CheckNode
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
import UniversalMediaPlayer
|
||||
|
||||
private final class PremiumGiftScreenContentComponent: CombinedComponent {
|
||||
typealias EnvironmentType = (ViewControllerComponentContainer.Environment, ScrollChildEnvironment)
|
||||
@ -520,14 +29,16 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent {
|
||||
|
||||
let present: (ViewController) -> 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.peer = peer
|
||||
self.products = products
|
||||
self.selectedProductId = selectedProductId
|
||||
self.present = present
|
||||
self.selectProduct = selectProduct
|
||||
self.buy = buy
|
||||
}
|
||||
|
||||
static func ==(lhs: PremiumGiftScreenContentComponent, rhs: PremiumGiftScreenContentComponent) -> Bool {
|
||||
@ -546,12 +57,97 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent {
|
||||
|
||||
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 {
|
||||
let overscroll = Child(Rectangle.self)
|
||||
let fade = Child(RoundedRectangle.self)
|
||||
let text = Child(MultilineTextComponent.self)
|
||||
let section = Child(ProductGroupComponent.self)
|
||||
let optionsSection = Child(SectionGroupComponent.self)
|
||||
let perksSection = Child(SectionGroupComponent.self)
|
||||
|
||||
return { context in
|
||||
let sideInset: CGFloat = 16.0
|
||||
@ -596,7 +192,9 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent {
|
||||
size.height += 183.0 + 10.0 + environment.navigationHeight - 56.0
|
||||
|
||||
let textColor = theme.list.itemPrimaryTextColor
|
||||
let titleColor = theme.list.itemPrimaryTextColor
|
||||
let subtitleColor = theme.list.itemSecondaryTextColor
|
||||
let arrowColor = theme.list.disclosureArrowColor
|
||||
|
||||
let textFont = Font.regular(15.0)
|
||||
let boldTextFont = Font.semibold(15.0)
|
||||
@ -624,16 +222,16 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent {
|
||||
size.height += text.size.height
|
||||
size.height += 21.0
|
||||
|
||||
var items: [ProductGroupComponent.Item] = []
|
||||
|
||||
let gradientColors: [UIColor] = [
|
||||
UIColor(rgb: 0x8e77ff),
|
||||
UIColor(rgb: 0x9a6fff),
|
||||
UIColor(rgb: 0xb36eee)
|
||||
]
|
||||
var items: [SectionGroupComponent.Item] = []
|
||||
|
||||
var i = 0
|
||||
if let products = component.products {
|
||||
let gradientColors: [UIColor] = [
|
||||
UIColor(rgb: 0x8e77ff),
|
||||
UIColor(rgb: 0x9a6fff),
|
||||
UIColor(rgb: 0xb36eee)
|
||||
]
|
||||
|
||||
let shortestOptionPrice: Int64
|
||||
if let product = products.last {
|
||||
shortestOptionPrice = Int64(Float(product.storeProduct.priceCurrencyAndAmount.amount) / Float(product.months))
|
||||
@ -657,14 +255,13 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent {
|
||||
discount = ""
|
||||
}
|
||||
|
||||
items.append(ProductGroupComponent.Item(
|
||||
items.append(SectionGroupComponent.Item(
|
||||
AnyComponentWithIdentity(
|
||||
id: product.id,
|
||||
component: AnyComponent(
|
||||
GiftComponent(
|
||||
PremiumOptionComponent(
|
||||
title: giftTitle,
|
||||
totalPrice: product.price,
|
||||
perMonthPrice: strings.Premium_Gift_PricePerMonth(product.pricePerMonth).string,
|
||||
discount: discount,
|
||||
selected: product.id == component.selectedProductId,
|
||||
primaryTextColor: textColor,
|
||||
@ -683,24 +280,150 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent {
|
||||
}
|
||||
}
|
||||
|
||||
let section = section.update(
|
||||
component: ProductGroupComponent(
|
||||
let optionsSection = optionsSection.update(
|
||||
component: SectionGroupComponent(
|
||||
items: items,
|
||||
backgroundColor: environment.theme.list.itemBlocksBackgroundColor,
|
||||
selectionColor: environment.theme.list.itemHighlightedBackgroundColor
|
||||
selectionColor: environment.theme.list.itemHighlightedBackgroundColor,
|
||||
separatorColor: environment.theme.list.itemBlocksSeparatorColor
|
||||
),
|
||||
environment: {},
|
||||
availableSize: CGSize(width: availableWidth - sideInsets, height: .greatestFiniteMagnitude),
|
||||
transition: context.transition
|
||||
)
|
||||
context.add(section
|
||||
.position(CGPoint(x: availableWidth / 2.0, y: size.height + section.size.height / 2.0))
|
||||
context.add(optionsSection
|
||||
.position(CGPoint(x: availableWidth / 2.0, y: size.height + optionsSection.size.height / 2.0))
|
||||
.clipsToBounds(true)
|
||||
.cornerRadius(10.0)
|
||||
)
|
||||
size.height += section.size.height
|
||||
size.height += optionsSection.size.height
|
||||
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 += scrollEnvironment.insets.bottom
|
||||
@ -964,7 +687,6 @@ private final class PremiumGiftScreenComponent: CombinedComponent {
|
||||
let bottomPanel = Child(BlurredRectangle.self)
|
||||
let bottomSeparator = Child(Rectangle.self)
|
||||
let button = Child(SolidRoundedButtonComponent.self)
|
||||
let termsText = Child(MultilineTextComponent.self)
|
||||
|
||||
return { context in
|
||||
let environment = context.environment[EnvironmentType.self].value
|
||||
@ -1024,6 +746,8 @@ private final class PremiumGiftScreenComponent: CombinedComponent {
|
||||
present: context.component.present,
|
||||
selectProduct: { [weak state] productId in
|
||||
state?.selectProduct(id: productId)
|
||||
}, buy: { [weak state] in
|
||||
state?.buy()
|
||||
}
|
||||
)),
|
||||
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
|
||||
if let bottomContentOffset = state.bottomContentOffset, context.availableSize.width > 320.0 {
|
||||
if let bottomContentOffset = state.bottomContentOffset {
|
||||
bottomPanelAlpha = min(16.0, bottomContentOffset) / 16.0
|
||||
} else {
|
||||
bottomPanelAlpha = 0.0
|
||||
bottomPanelAlpha = 1.0
|
||||
}
|
||||
|
||||
context.add(bottomPanel
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height / 2.0))
|
||||
.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
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height))
|
||||
.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
|
||||
.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
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ import ConfettiEffect
|
||||
import TextFormat
|
||||
import InstantPageCache
|
||||
import UniversalMediaPlayer
|
||||
import CheckNode
|
||||
|
||||
public enum PremiumSource: Equatable {
|
||||
case settings
|
||||
@ -248,7 +249,7 @@ enum PremiumPerk: CaseIterable {
|
||||
}
|
||||
}
|
||||
|
||||
private struct PremiumIntroConfiguration {
|
||||
struct PremiumIntroConfiguration {
|
||||
static var defaultValue: PremiumIntroConfiguration {
|
||||
return PremiumIntroConfiguration(perks: [
|
||||
.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 let content: AnyComponentWithIdentity<Empty>
|
||||
public let action: () -> Void
|
||||
@ -462,7 +738,7 @@ private final class SectionGroupComponent: Component {
|
||||
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 {
|
||||
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 iconBackgroundColors: [UIColor]
|
||||
let title: String
|
||||
@ -646,18 +922,22 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
||||
let source: PremiumSource
|
||||
let isPremium: Bool?
|
||||
let otherPeerName: String?
|
||||
let price: String?
|
||||
let products: [InAppPurchaseManager.Product]?
|
||||
let selectedProductId: String?
|
||||
let present: (ViewController) -> Void
|
||||
let selectProduct: (String) -> Void
|
||||
let buy: () -> 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.source = source
|
||||
self.isPremium = isPremium
|
||||
self.otherPeerName = otherPeerName
|
||||
self.price = price
|
||||
self.products = products
|
||||
self.selectedProductId = selectedProductId
|
||||
self.present = present
|
||||
self.selectProduct = selectProduct
|
||||
self.buy = buy
|
||||
self.updateIsFocused = updateIsFocused
|
||||
}
|
||||
@ -675,7 +955,10 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
||||
if lhs.otherPeerName != rhs.otherPeerName {
|
||||
return false
|
||||
}
|
||||
if lhs.price != rhs.price {
|
||||
if lhs.products != rhs.products {
|
||||
return false
|
||||
}
|
||||
if lhs.selectedProductId != rhs.selectedProductId {
|
||||
return false
|
||||
}
|
||||
|
||||
@ -685,7 +968,9 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
||||
final class State: ComponentState {
|
||||
private let context: AccountContext
|
||||
|
||||
var price: String?
|
||||
var products: [InAppPurchaseManager.Product]?
|
||||
var selectedProductId: String?
|
||||
|
||||
var isPremium: Bool?
|
||||
|
||||
private var disposable: Disposable?
|
||||
@ -695,6 +980,10 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
||||
private var stickersDisposable: Disposable?
|
||||
private var preloadDisposableSet = DisposableSet()
|
||||
|
||||
var price: String? {
|
||||
return self.products?.first(where: { $0.id == self.selectedProductId })?.price
|
||||
}
|
||||
|
||||
init(context: AccountContext, source: PremiumSource) {
|
||||
self.context = context
|
||||
|
||||
@ -773,7 +1062,8 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
||||
let overscroll = Child(Rectangle.self)
|
||||
let fade = Child(RoundedRectangle.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 infoTitle = 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 environment = context.environment[ViewControllerComponentContainer.Environment.self].value
|
||||
let state = context.state
|
||||
state.price = context.component.price
|
||||
state.products = context.component.products
|
||||
state.selectedProductId = context.component.selectedProductId
|
||||
state.isPremium = context.component.isPremium
|
||||
|
||||
let theme = environment.theme
|
||||
@ -886,18 +1177,101 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
||||
UIColor(rgb: 0x54A3FF),
|
||||
UIColor(rgb: 0x54A3FF)
|
||||
]
|
||||
|
||||
var items: [SectionGroupComponent.Item] = []
|
||||
|
||||
|
||||
let accountContext = context.component.context
|
||||
let present = context.component.present
|
||||
let selectProduct = context.component.selectProduct
|
||||
let buy = context.component.buy
|
||||
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 perksItems: [SectionGroupComponent.Item] = []
|
||||
for perk in state.configuration.perks {
|
||||
let iconBackgroundColors = gradientColors[i]
|
||||
items.append(SectionGroupComponent.Item(
|
||||
perksItems.append(SectionGroupComponent.Item(
|
||||
AnyComponentWithIdentity(
|
||||
id: perk.identifier,
|
||||
component: AnyComponent(
|
||||
@ -982,9 +1356,9 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
||||
i += 1
|
||||
}
|
||||
|
||||
let section = section.update(
|
||||
let perksSection = perksSection.update(
|
||||
component: SectionGroupComponent(
|
||||
items: items,
|
||||
items: perksItems,
|
||||
backgroundColor: environment.theme.list.itemBlocksBackgroundColor,
|
||||
selectionColor: environment.theme.list.itemHighlightedBackgroundColor,
|
||||
separatorColor: environment.theme.list.itemBlocksSeparatorColor
|
||||
@ -993,12 +1367,12 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
||||
availableSize: CGSize(width: availableWidth - sideInsets, height: .greatestFiniteMagnitude),
|
||||
transition: context.transition
|
||||
)
|
||||
context.add(section
|
||||
.position(CGPoint(x: availableWidth / 2.0, y: size.height + section.size.height / 2.0))
|
||||
context.add(perksSection
|
||||
.position(CGPoint(x: availableWidth / 2.0, y: size.height + perksSection.size.height / 2.0))
|
||||
.clipsToBounds(true)
|
||||
.cornerRadius(10.0)
|
||||
)
|
||||
size.height += section.size.height
|
||||
size.height += perksSection.size.height
|
||||
size.height += 23.0
|
||||
|
||||
let textSideInset: CGFloat = 16.0
|
||||
@ -1244,7 +1618,10 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
||||
var hasIdleAnimations = true
|
||||
|
||||
var inProgress = false
|
||||
var premiumProduct: InAppPurchaseManager.Product?
|
||||
|
||||
var products: [InAppPurchaseManager.Product]?
|
||||
var selectedProductId: String?
|
||||
|
||||
var isPremium: Bool?
|
||||
var otherPeerName: String?
|
||||
|
||||
@ -1252,6 +1629,10 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
||||
private var paymentDisposable = 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) {
|
||||
self.context = context
|
||||
self.updateInProgress = updateInProgress
|
||||
@ -1295,7 +1676,10 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
||||
otherPeerName
|
||||
).start(next: { [weak self] products, isPremium, otherPeerName in
|
||||
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.otherPeerName = otherPeerName
|
||||
strongSelf.updated(transition: .immediate)
|
||||
@ -1311,7 +1695,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
||||
|
||||
func buy() {
|
||||
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
|
||||
}
|
||||
|
||||
@ -1412,6 +1796,11 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
||||
self.hasIdleAnimations = !isFocused
|
||||
self.updated(transition: .immediate)
|
||||
}
|
||||
|
||||
func selectProduct(_ productId: String) {
|
||||
self.selectedProductId = productId
|
||||
self.updated(transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
func makeState() -> State {
|
||||
@ -1543,7 +1932,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
||||
let bottomPanelPadding: CGFloat = 12.0
|
||||
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 scrollContent = scrollContent.update(
|
||||
component: ScrollComponent<EnvironmentType>(
|
||||
content: AnyComponent(PremiumIntroScreenContentComponent(
|
||||
@ -1551,8 +1940,12 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
||||
source: context.component.source,
|
||||
isPremium: state.isPremium,
|
||||
otherPeerName: state.otherPeerName,
|
||||
price: state.premiumProduct?.price,
|
||||
products: state.products,
|
||||
selectedProductId: state.selectedProductId,
|
||||
present: context.component.present,
|
||||
selectProduct: { [weak state] productId in
|
||||
state?.selectProduct(productId)
|
||||
},
|
||||
buy: { [weak state] in
|
||||
state?.buy()
|
||||
},
|
||||
@ -1655,7 +2048,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
||||
let sideInset: CGFloat = 16.0
|
||||
let button = button.update(
|
||||
component: SolidRoundedButtonComponent(
|
||||
title: environment.strings.Premium_SubscribeFor(state.premiumProduct?.price ?? "—").string,
|
||||
title: environment.strings.Premium_SubscribeFor(state.price ?? "—").string,
|
||||
theme: SolidRoundedButtonComponent.Theme(
|
||||
backgroundColor: UIColor(rgb: 0x8878ff),
|
||||
backgroundColors: [
|
||||
|
@ -46,16 +46,18 @@ private func generateDiffuseTexture() -> UIImage {
|
||||
}
|
||||
|
||||
class PremiumStarComponent: Component {
|
||||
let isIntro: Bool
|
||||
let isVisible: Bool
|
||||
let hasIdleAnimations: Bool
|
||||
|
||||
init(isVisible: Bool, hasIdleAnimations: Bool) {
|
||||
init(isIntro: Bool, isVisible: Bool, hasIdleAnimations: Bool) {
|
||||
self.isIntro = isIntro
|
||||
self.isVisible = isVisible
|
||||
self.hasIdleAnimations = hasIdleAnimations
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -84,7 +86,11 @@ class PremiumStarComponent: Component {
|
||||
private var timer: SwiftSignalKit.Timer?
|
||||
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.backgroundColor = .clear
|
||||
self.sceneView.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
|
||||
@ -433,30 +439,48 @@ class PremiumStarComponent: Component {
|
||||
self.previousInteractionTimestamp = currentTime
|
||||
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 let leftParticleSystem = particlesLeft.particleSystems?.first, let rightParticleSystem = particlesRight.particleSystems?.first {
|
||||
leftParticleSystem.speedFactor = 1.3
|
||||
leftParticleSystem.particleVelocity = 2.4
|
||||
leftParticleSystem.birthRate = 24.0
|
||||
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, let leftBottomParticleSystem = particlesBottomLeft.particleSystems?.first, let rightBottomParticleSystem = particlesBottomRight.particleSystems?.first {
|
||||
leftParticleSystem.speedFactor = 2.0
|
||||
leftParticleSystem.particleVelocity = 1.6
|
||||
leftParticleSystem.birthRate = 60.0
|
||||
leftParticleSystem.particleLifeSpan = 4.0
|
||||
|
||||
rightParticleSystem.speedFactor = 1.3
|
||||
rightParticleSystem.particleVelocity = 2.4
|
||||
rightParticleSystem.birthRate = 24.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
|
||||
Queue.mainQueue().after(1.0) {
|
||||
node.physicsField?.isActive = false
|
||||
|
||||
leftParticleSystem.birthRate = 9.0
|
||||
leftParticleSystem.birthRate = 12.0
|
||||
leftParticleSystem.particleVelocity = 1.2
|
||||
leftParticleSystem.particleLifeSpan = 3.0
|
||||
|
||||
rightParticleSystem.birthRate = 9.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
|
||||
@ -541,7 +565,7 @@ class PremiumStarComponent: Component {
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -691,9 +691,13 @@ private final class StickerPackContainer: ASDisplayNode {
|
||||
|
||||
let backgroundAlpha: CGFloat
|
||||
switch offset {
|
||||
case let .known(value):
|
||||
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)
|
||||
backgroundAlpha = min(10.0, bottomOffsetY) / 10.0
|
||||
case .known:
|
||||
let topPosition = self.view.convert(self.topContainerNode.frame, to: self.view).minY
|
||||
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:
|
||||
backgroundAlpha = 1.0
|
||||
}
|
||||
|
@ -712,7 +712,9 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
if !alreadySeen {
|
||||
item.controllerInteraction.seenOneTimeAnimatedMedia.insert(item.message.id)
|
||||
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 {
|
||||
Queue.mainQueue().after(0.1) {
|
||||
self.playPremiumStickerAnimation()
|
||||
@ -979,7 +981,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
|
||||
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 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:
|
||||
multiplier = 1.0
|
||||
case 2:
|
||||
multiplier = 0.7
|
||||
multiplier = 0.84
|
||||
case 3:
|
||||
multiplier = 0.52
|
||||
multiplier = 0.69
|
||||
case 4:
|
||||
multiplier = 0.37
|
||||
multiplier = 0.53
|
||||
case 5:
|
||||
multiplier = 0.28
|
||||
multiplier = 0.46
|
||||
case 6:
|
||||
multiplier = 0.25
|
||||
multiplier = 0.38
|
||||
case 7:
|
||||
multiplier = 0.23
|
||||
multiplier = 0.32
|
||||
case 8:
|
||||
multiplier = 0.21
|
||||
multiplier = 0.27
|
||||
case 9:
|
||||
multiplier = 0.19
|
||||
multiplier = 0.24
|
||||
default:
|
||||
multiplier = 0.19
|
||||
multiplier = 0.21
|
||||
}
|
||||
return floor(basicSize * multiplier)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user