Merge commit '38c8b7738fa5358b204b0edd43e98eb6d10726bc'

# Conflicts:
#	Telegram/Telegram-iOS/en.lproj/Localizable.strings
This commit is contained in:
Ali 2022-08-12 23:41:33 +04:00
commit 67ce20f302
12 changed files with 860 additions and 749 deletions

View File

@ -7959,3 +7959,6 @@ Sorry for the inconvenience.";
"StickerPacksSettings.SuggestAnimatedEmoji" = "Suggest Animated Emoji";
"Emoji.FrequentlyUsed" = "Recently Used";
"Premium.Annual" = "Annual";
"Premium.Monthly" = "Monthly";

View File

@ -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] {

View File

@ -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")
}
}

View File

@ -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")
}
}
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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 {
@ -1440,9 +1829,14 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
if let topContentOffset = state.topContentOffset, topContentOffset >= 123.0 {
starIsVisible = false
}
var isIntro = true
if case .profile = context.component.source {
isIntro = false
}
let star = star.update(
component: PremiumStarComponent(isVisible: starIsVisible, hasIdleAnimations: state.hasIdleAnimations),
component: PremiumStarComponent(isIntro: isIntro, isVisible: starIsVisible, hasIdleAnimations: state.hasIdleAnimations),
availableSize: CGSize(width: min(390.0, context.availableSize.width), height: 220.0),
transition: context.transition
)
@ -1543,7 +1937,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 +1945,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 +2053,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: [

View File

@ -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)
@ -369,10 +375,13 @@ class PremiumStarComponent: Component {
return
}
let fromScale: Float = self.isIntro ? 0.1 : 0.08
let toScale: Float = self.isIntro ? 0.115 : 0.092
let animation = CABasicAnimation(keyPath: "scale")
animation.duration = 2.0
animation.fromValue = NSValue(scnVector3: SCNVector3(x: 0.1, y: 0.1, z: 0.1))
animation.toValue = NSValue(scnVector3: SCNVector3(x: 0.115, y: 0.115, z: 0.115))
animation.fromValue = NSValue(scnVector3: SCNVector3(x: fromScale, y: fromScale, z: fromScale))
animation.toValue = NSValue(scnVector3: SCNVector3(x: toScale, y: toScale, z: toScale))
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
animation.autoreverses = true
animation.repeatCount = .infinity
@ -433,30 +442,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 +568,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 {

View File

@ -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
}

View File

@ -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)
}