Swiftgram/submodules/PremiumUI/Sources/PremiumLimitScreen.swift
2023-12-29 04:50:22 +04:00

2146 lines
102 KiB
Swift

import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramCore
import SwiftSignalKit
import AccountContext
import TelegramPresentationData
import PresentationDataUtils
import ComponentFlow
import ViewControllerComponent
import SheetComponent
import MultilineTextComponent
import BundleIconComponent
import SolidRoundedButtonComponent
import Markdown
import BalancedTextComponent
import ConfettiEffect
import AvatarNode
import TextFormat
import RoundedRectWithTailPath
func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? {
return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(backgroundColor.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
context.setLineWidth(2.0)
context.setLineCap(.round)
context.setStrokeColor(foregroundColor.cgColor)
context.move(to: CGPoint(x: 10.0, y: 10.0))
context.addLine(to: CGPoint(x: 20.0, y: 20.0))
context.strokePath()
context.move(to: CGPoint(x: 20.0, y: 10.0))
context.addLine(to: CGPoint(x: 10.0, y: 20.0))
context.strokePath()
})
}
public class PremiumLimitDisplayComponent: Component {
private let inactiveColor: UIColor
private let activeColors: [UIColor]
private let inactiveTitle: String
private let inactiveValue: String
private let inactiveTitleColor: UIColor
private let activeTitle: String
private let activeValue: String
private let activeTitleColor: UIColor
private let badgeIconName: String?
private let badgeText: String?
private let badgePosition: CGFloat
private let badgeGraphPosition: CGFloat
private let invertProgress: Bool
private let isPremiumDisabled: Bool
public init(
inactiveColor: UIColor,
activeColors: [UIColor],
inactiveTitle: String,
inactiveValue: String,
inactiveTitleColor: UIColor,
activeTitle: String,
activeValue: String,
activeTitleColor: UIColor,
badgeIconName: String?,
badgeText: String?,
badgePosition: CGFloat,
badgeGraphPosition: CGFloat,
invertProgress: Bool = false,
isPremiumDisabled: Bool
) {
self.inactiveColor = inactiveColor
self.activeColors = activeColors
self.inactiveTitle = inactiveTitle
self.inactiveValue = inactiveValue
self.inactiveTitleColor = inactiveTitleColor
self.activeTitle = activeTitle
self.activeValue = activeValue
self.activeTitleColor = activeTitleColor
self.badgeIconName = badgeIconName
self.badgeText = badgeText
self.badgePosition = badgePosition
self.badgeGraphPosition = badgeGraphPosition
self.invertProgress = invertProgress
self.isPremiumDisabled = isPremiumDisabled
}
public static func ==(lhs: PremiumLimitDisplayComponent, rhs: PremiumLimitDisplayComponent) -> Bool {
if lhs.inactiveColor != rhs.inactiveColor {
return false
}
if lhs.activeColors != rhs.activeColors {
return false
}
if lhs.inactiveTitle != rhs.inactiveTitle {
return false
}
if lhs.inactiveValue != rhs.inactiveValue {
return false
}
if lhs.inactiveTitleColor != rhs.inactiveTitleColor {
return false
}
if lhs.activeTitle != rhs.activeTitle {
return false
}
if lhs.activeValue != rhs.activeValue {
return false
}
if lhs.activeTitleColor != rhs.activeTitleColor {
return false
}
if lhs.badgeIconName != rhs.badgeIconName {
return false
}
if lhs.badgeText != rhs.badgeText {
return false
}
if lhs.badgePosition != rhs.badgePosition {
return false
}
if lhs.badgeGraphPosition != rhs.badgeGraphPosition {
return false
}
if lhs.invertProgress != rhs.invertProgress {
return false
}
if lhs.isPremiumDisabled != rhs.isPremiumDisabled {
return false
}
return true
}
public final class View: UIView {
private var component: PremiumLimitDisplayComponent?
private let container: UIView
private let inactiveBackground: SimpleLayer
private let inactiveTitleLabel = ComponentView<Empty>()
private let inactiveValueLabel = ComponentView<Empty>()
private let innerLeftTitleLabel = ComponentView<Empty>()
private let innerRightTitleLabel = ComponentView<Empty>()
private let activeContainer: UIView
private let activeBackground: SimpleLayer
private let activeTitleLabel = ComponentView<Empty>()
private let activeValueLabel = ComponentView<Empty>()
private let badgeView: UIView
private let badgeMaskView: UIView
private let badgeShapeLayer = CAShapeLayer()
private let badgeForeground: SimpleLayer
private let badgeIcon: UIImageView
private let badgeLabel: BadgeLabelView
private let badgeLabelMaskView = UIImageView()
private var badgeTailPosition: CGFloat = 0.0
private var badgeShapeArguments: (Double, Double, CGSize, CGFloat, CGFloat)?
private let hapticFeedback = HapticFeedback()
override init(frame: CGRect) {
self.container = UIView()
self.container.clipsToBounds = true
self.container.layer.cornerRadius = 6.0
self.inactiveBackground = SimpleLayer()
self.activeContainer = UIView()
self.activeContainer.clipsToBounds = true
self.activeBackground = SimpleLayer()
self.activeBackground.anchorPoint = CGPoint()
self.badgeView = UIView()
self.badgeView.alpha = 0.0
self.badgeShapeLayer.fillColor = UIColor.white.cgColor
self.badgeShapeLayer.transform = CATransform3DMakeScale(1.0, -1.0, 1.0)
self.badgeMaskView = UIView()
self.badgeMaskView.layer.addSublayer(self.badgeShapeLayer)
self.badgeView.mask = self.badgeMaskView
self.badgeForeground = SimpleLayer()
self.badgeIcon = UIImageView()
self.badgeIcon.contentMode = .center
self.badgeLabel = BadgeLabelView()
let _ = self.badgeLabel.update(value: "0", transition: .immediate)
self.badgeLabel.mask = self.badgeLabelMaskView
super.init(frame: frame)
self.addSubview(self.container)
self.container.layer.addSublayer(self.inactiveBackground)
self.container.addSubview(self.activeContainer)
self.activeContainer.layer.addSublayer(self.activeBackground)
self.addSubview(self.badgeView)
self.badgeView.layer.addSublayer(self.badgeForeground)
self.badgeView.addSubview(self.badgeIcon)
self.badgeView.addSubview(self.badgeLabel)
self.badgeLabelMaskView.contentMode = .scaleToFill
self.badgeLabelMaskView.image = generateImage(CGSize(width: 2.0, height: 36.0), rotatedContext: { size, context in
let bounds = CGRect(origin: .zero, size: size)
context.clear(bounds)
let colorsArray: [CGColor] = [
UIColor(rgb: 0xffffff, alpha: 0.0).cgColor,
UIColor(rgb: 0xffffff).cgColor,
UIColor(rgb: 0xffffff).cgColor,
UIColor(rgb: 0xffffff, alpha: 0.0).cgColor,
]
var locations: [CGFloat] = [0.0, 0.24, 0.76, 1.0]
let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray as CFArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
})
self.isUserInteractionEnabled = false
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.badgeShapeAnimator?.invalidate()
}
private var didPlayAppearanceAnimation = false
func playAppearanceAnimation(component: PremiumLimitDisplayComponent, badgeFullSize: CGSize, from: CGFloat? = nil) {
if from == nil {
self.badgeView.layer.animateScale(from: 0.1, to: 1.0, duration: 0.4, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
}
let rotationAngle: CGFloat
if badgeFullSize.width > 100.0 {
rotationAngle = 0.2
} else {
rotationAngle = 0.26
}
let positionAnimation = CABasicAnimation(keyPath: "position.x")
positionAnimation.fromValue = NSValue(cgPoint: CGPoint(x: from ?? 0.0, y: 0.0))
positionAnimation.toValue = NSValue(cgPoint: self.badgeView.center)
positionAnimation.duration = 0.5
positionAnimation.fillMode = .forwards
positionAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
self.badgeView.layer.add(positionAnimation, forKey: "appearance1")
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotateAnimation.fromValue = 0.0 as NSNumber
rotateAnimation.toValue = -rotationAngle as NSNumber
rotateAnimation.duration = 0.15
rotateAnimation.fillMode = .forwards
rotateAnimation.timingFunction = CAMediaTimingFunction(name: .easeOut)
rotateAnimation.isRemovedOnCompletion = false
self.badgeView.layer.add(rotateAnimation, forKey: "appearance2")
Queue.mainQueue().after(0.5, {
let bounceAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
bounceAnimation.fromValue = -rotationAngle as NSNumber
bounceAnimation.toValue = 0.04 as NSNumber
bounceAnimation.duration = 0.2
bounceAnimation.fillMode = .forwards
bounceAnimation.timingFunction = CAMediaTimingFunction(name: .easeOut)
bounceAnimation.isRemovedOnCompletion = false
self.badgeView.layer.add(bounceAnimation, forKey: "appearance3")
self.badgeView.layer.removeAnimation(forKey: "appearance2")
if !self.badgeView.isHidden {
self.hapticFeedback.impact(.light)
}
Queue.mainQueue().after(0.2) {
let returnAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
returnAnimation.fromValue = 0.04 as NSNumber
returnAnimation.toValue = 0.0 as NSNumber
returnAnimation.duration = 0.15
returnAnimation.fillMode = .forwards
returnAnimation.timingFunction = CAMediaTimingFunction(name: .easeIn)
self.badgeView.layer.add(returnAnimation, forKey: "appearance4")
self.badgeView.layer.removeAnimation(forKey: "appearance3")
}
})
if from == nil {
self.badgeView.alpha = 1.0
self.badgeView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
}
if let badgeText = component.badgeText {
let transition: Transition = .easeInOut(duration: from != nil ? 0.3 : 0.5)
var frameTransition = transition
if from == nil {
frameTransition = frameTransition.withAnimation(.none)
}
let badgeLabelSize = self.badgeLabel.update(value: badgeText, transition: transition)
frameTransition.setFrame(view: self.badgeLabel, frame: CGRect(origin: CGPoint(x: 14.0 + floorToScreenPixels((badgeFullSize.width - badgeLabelSize.width) / 2.0), y: 5.0), size: badgeLabelSize))
}
}
var previousAvailableSize: CGSize?
func update(component: PremiumLimitDisplayComponent, availableSize: CGSize, transition: Transition) -> CGSize {
self.component = component
self.inactiveBackground.backgroundColor = component.inactiveColor.cgColor
self.activeBackground.backgroundColor = component.activeColors.last?.cgColor
let size = CGSize(width: availableSize.width, height: 120.0)
self.badgeIcon.image = component.badgeIconName.flatMap { UIImage(bundleImageName: $0)?.withRenderingMode(.alwaysTemplate) }
self.badgeIcon.tintColor = component.activeTitleColor
self.badgeView.isHidden = self.badgeIcon.image == nil
let lineHeight: CGFloat = 30.0
let containerFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - lineHeight), size: CGSize(width: size.width, height: lineHeight))
self.container.frame = containerFrame
let activityPosition: CGFloat = floor(containerFrame.width * component.badgeGraphPosition)
let activeWidth: CGFloat = containerFrame.width - activityPosition
let leftTextColor: UIColor
let rightTextColor: UIColor
if component.invertProgress {
leftTextColor = component.inactiveTitleColor
rightTextColor = component.inactiveTitleColor
} else {
leftTextColor = component.inactiveTitleColor
rightTextColor = component.activeTitleColor
}
if !component.isPremiumDisabled {
if component.invertProgress {
let innerLeftTitleSize = self.innerLeftTitleLabel.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(
NSAttributedString(
string: component.inactiveTitle,
font: Font.semibold(15.0),
textColor: component.activeTitleColor
)
)
)
),
environment: {},
containerSize: availableSize
)
if let view = self.innerLeftTitleLabel.view {
if view.superview == nil {
self.activeContainer.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: 12.0, y: floorToScreenPixels((lineHeight - innerLeftTitleSize.height) / 2.0)), size: innerLeftTitleSize)
}
let innerRightTitleSize = self.innerRightTitleLabel.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(
NSAttributedString(
string: component.activeValue,
font: Font.semibold(15.0),
textColor: component.activeTitleColor
)
)
)
),
environment: {},
containerSize: availableSize
)
if let view = self.innerRightTitleLabel.view {
if view.superview == nil {
self.activeContainer.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: containerFrame.width - 12.0 - innerRightTitleSize.width, y: floorToScreenPixels((lineHeight - innerRightTitleSize.height) / 2.0)), size: innerRightTitleSize)
}
}
let inactiveTitleSize = self.inactiveTitleLabel.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(
NSAttributedString(
string: component.inactiveTitle,
font: Font.semibold(15.0),
textColor: leftTextColor
)
)
)
),
environment: {},
containerSize: availableSize
)
if let view = self.inactiveTitleLabel.view {
if view.superview == nil {
self.container.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: 12.0, y: floorToScreenPixels((lineHeight - inactiveTitleSize.height) / 2.0)), size: inactiveTitleSize)
}
let inactiveValueSize = self.inactiveValueLabel.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(
NSAttributedString(
string: component.inactiveValue,
font: Font.semibold(15.0),
textColor: leftTextColor
)
)
)
),
environment: {},
containerSize: availableSize
)
if let view = self.inactiveValueLabel.view {
if view.superview == nil {
self.container.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: activityPosition - 12.0 - inactiveValueSize.width, y: floorToScreenPixels((lineHeight - inactiveValueSize.height) / 2.0)), size: inactiveValueSize)
}
let activeTitleSize = self.activeTitleLabel.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(
NSAttributedString(
string: component.activeTitle,
font: Font.semibold(15.0),
textColor: rightTextColor
)
)
)
),
environment: {},
containerSize: availableSize
)
if let view = self.activeTitleLabel.view {
if view.superview == nil {
self.container.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: activityPosition + 12.0, y: floorToScreenPixels((lineHeight - activeTitleSize.height) / 2.0)), size: activeTitleSize)
}
let activeValueSize = self.activeValueLabel.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(
NSAttributedString(
string: component.activeValue,
font: Font.semibold(15.0),
textColor: rightTextColor
)
)
)
),
environment: {},
containerSize: availableSize
)
if let view = self.activeValueLabel.view {
if view.superview == nil {
self.container.addSubview(view)
if component.invertProgress {
self.container.bringSubviewToFront(self.activeContainer)
}
}
view.frame = CGRect(origin: CGPoint(x: containerFrame.width - 12.0 - activeValueSize.width, y: floorToScreenPixels((lineHeight - activeValueSize.height) / 2.0)), size: activeValueSize)
}
}
var progressTransition: Transition = .immediate
if !transition.animation.isImmediate {
progressTransition = .easeInOut(duration: 0.5)
}
if !component.isPremiumDisabled {
if component.invertProgress {
progressTransition.setFrame(layer: self.inactiveBackground, frame: CGRect(origin: CGPoint(x: activityPosition, y: 0.0), size: CGSize(width: size.width - activityPosition, height: lineHeight)))
progressTransition.setFrame(view: self.activeContainer, frame: CGRect(origin: .zero, size: CGSize(width: activityPosition, height: lineHeight)))
progressTransition.setBounds(layer: self.activeBackground, bounds: CGRect(origin: .zero, size: CGSize(width: containerFrame.width * 1.35, height: lineHeight)))
} else {
progressTransition.setFrame(layer: self.inactiveBackground, frame: CGRect(origin: .zero, size: CGSize(width: activityPosition, height: lineHeight)))
progressTransition.setFrame(view: self.activeContainer, frame: CGRect(origin: CGPoint(x: activityPosition, y: 0.0), size: CGSize(width: activeWidth, height: lineHeight)))
progressTransition.setFrame(layer: self.activeBackground, frame: CGRect(origin: CGPoint(x: -activityPosition, y: 0.0), size: CGSize(width: containerFrame.width * 1.35, height: lineHeight)))
}
if self.activeBackground.animation(forKey: "movement") == nil {
self.activeBackground.position = CGPoint(x: -self.activeContainer.frame.width * 0.35, y: lineHeight / 2.0)
}
}
let countWidth: CGFloat
if let badgeText = component.badgeText {
switch badgeText.count {
case 1:
countWidth = 20.0
case 2:
countWidth = 35.0
case 3:
countWidth = 51.0
case 4:
countWidth = 60.0
case 5:
countWidth = 74.0
case 6:
countWidth = 88.0
default:
countWidth = 51.0
}
} else {
countWidth = 51.0
}
let badgeWidth: CGFloat = countWidth + 54.0
let badgeSize = CGSize(width: badgeWidth, height: 48.0)
let badgeFullSize = CGSize(width: badgeWidth, height: 48.0 + 12.0)
self.badgeMaskView.frame = CGRect(origin: .zero, size: badgeFullSize)
self.badgeShapeLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -4.0), size: badgeFullSize)
self.badgeView.bounds = CGRect(origin: .zero, size: badgeFullSize)
let currentBadgeX = self.badgeView.center.x
var badgePosition = component.badgePosition
if component.isPremiumDisabled {
badgePosition = 0.5
}
if badgePosition > 1.0 - 0.15 {
progressTransition.setAnchorPoint(layer: self.badgeView.layer, anchorPoint: CGPoint(x: 1.0, y: 1.0))
if component.isPremiumDisabled || progressTransition.animation.isImmediate {
self.badgeShapeLayer.path = generateRoundedRectWithTailPath(rectSize: badgeSize, tailPosition: component.isPremiumDisabled ? nil : 1.0).cgPath
} else {
self.badgeShapeArguments = (CACurrentMediaTime(), 0.5, badgeSize, self.badgeTailPosition, 1.0)
self.animateBadgeTailPositionChange()
}
self.badgeTailPosition = 1.0
if let _ = self.badgeView.layer.animation(forKey: "appearance1") {
} else {
self.badgeView.center = CGPoint(x: 3.0 + (size.width - 6.0) * badgePosition + 3.0, y: 82.0)
}
} else if badgePosition < 0.15 {
progressTransition.setAnchorPoint(layer: self.badgeView.layer, anchorPoint: CGPoint(x: 0.0, y: 1.0))
if component.isPremiumDisabled || progressTransition.animation.isImmediate {
self.badgeShapeLayer.path = generateRoundedRectWithTailPath(rectSize: badgeSize, tailPosition: component.isPremiumDisabled ? nil : 0.0).cgPath
} else {
self.badgeShapeArguments = (CACurrentMediaTime(), 0.5, badgeSize, self.badgeTailPosition, 0.0)
self.animateBadgeTailPositionChange()
}
self.badgeTailPosition = 0.0
if let _ = self.badgeView.layer.animation(forKey: "appearance1") {
} else {
self.badgeView.center = CGPoint(x: (size.width - 6.0) * badgePosition, y: 82.0)
}
} else {
progressTransition.setAnchorPoint(layer: self.badgeView.layer, anchorPoint: CGPoint(x: 0.5, y: 1.0))
if component.isPremiumDisabled || progressTransition.animation.isImmediate {
self.badgeShapeLayer.path = generateRoundedRectWithTailPath(rectSize: badgeSize, tailPosition: component.isPremiumDisabled ? nil : 0.5).cgPath
} else {
self.badgeShapeArguments = (CACurrentMediaTime(), 0.5, badgeSize, self.badgeTailPosition, 0.5)
self.animateBadgeTailPositionChange()
}
self.badgeTailPosition = 0.5
if let _ = self.badgeView.layer.animation(forKey: "appearance1") {
} else {
self.badgeView.center = CGPoint(x: size.width * badgePosition, y: 82.0)
}
}
self.badgeForeground.bounds = CGRect(origin: CGPoint(), size: CGSize(width: badgeFullSize.width * 3.0, height: badgeFullSize.height))
if self.badgeForeground.animation(forKey: "movement") == nil {
self.badgeForeground.position = CGPoint(x: badgeSize.width * 3.0 / 2.0 - self.badgeForeground.frame.width * 0.35, y: badgeFullSize.height / 2.0)
}
self.badgeIcon.frame = CGRect(x: 10.0, y: 9.0, width: 30.0, height: 30.0)
self.badgeLabelMaskView.frame = CGRect(x: 0.0, y: 0.0, width: 100.0, height: 36.0)
if component.isPremiumDisabled {
if !self.didPlayAppearanceAnimation {
self.didPlayAppearanceAnimation = true
self.badgeView.alpha = 1.0
if let badgeText = component.badgeText {
let badgeLabelSize = self.badgeLabel.update(value: badgeText, transition: .immediate)
transition.setFrame(view: self.badgeLabel, frame: CGRect(origin: CGPoint(x: 14.0 + floorToScreenPixels((badgeFullSize.width - badgeLabelSize.width) / 2.0), y: 5.0), size: badgeLabelSize))
}
}
} else if !self.didPlayAppearanceAnimation || !transition.animation.isImmediate {
self.didPlayAppearanceAnimation = true
if transition.animation.isImmediate {
if component.badgePosition < 0.1 {
self.badgeView.alpha = 1.0
if let badgeText = component.badgeText {
let badgeLabelSize = self.badgeLabel.update(value: badgeText, transition: .immediate)
transition.setFrame(view: self.badgeLabel, frame: CGRect(origin: CGPoint(x: 14.0 + floorToScreenPixels((badgeFullSize.width - badgeLabelSize.width) / 2.0), y: 5.0), size: badgeLabelSize))
}
} else {
self.playAppearanceAnimation(component: component, badgeFullSize: badgeFullSize)
}
} else {
self.playAppearanceAnimation(component: component, badgeFullSize: badgeFullSize, from: currentBadgeX)
}
}
if self.previousAvailableSize != availableSize {
self.previousAvailableSize = availableSize
var locations: [CGFloat] = []
let delta = 1.0 / CGFloat(component.activeColors.count - 1)
for i in 0 ..< component.activeColors.count {
locations.append(delta * CGFloat(i))
}
let gradient = generateGradientImage(size: CGSize(width: 200.0, height: 60.0), colors: component.activeColors, locations: locations, direction: .horizontal)
self.badgeForeground.contentsGravity = .resizeAspectFill
self.badgeForeground.contents = gradient?.cgImage
self.activeBackground.contentsGravity = .resizeAspectFill
self.activeBackground.contents = gradient?.cgImage
self.setupGradientAnimations()
}
return size
}
private var badgeShapeAnimator: ConstantDisplayLinkAnimator?
private func animateBadgeTailPositionChange() {
if self.badgeShapeAnimator == nil {
self.badgeShapeAnimator = ConstantDisplayLinkAnimator(update: { [weak self] in
self?.animateBadgeTailPositionChange()
})
self.badgeShapeAnimator?.isPaused = true
}
if let (startTime, duration, badgeSize, initial, target) = self.badgeShapeArguments {
self.badgeShapeAnimator?.isPaused = false
let t = CGFloat(max(0.0, min(1.0, (CACurrentMediaTime() - startTime) / duration)))
let value = initial + (target - initial) * t
self.badgeShapeLayer.path = generateRoundedRectWithTailPath(rectSize: badgeSize, tailPosition: value).cgPath
if t >= 1.0 {
self.badgeShapeArguments = nil
self.badgeShapeAnimator?.isPaused = true
self.badgeShapeAnimator?.invalidate()
self.badgeShapeAnimator = nil
}
} else {
self.badgeShapeAnimator?.isPaused = true
self.badgeShapeAnimator?.invalidate()
self.badgeShapeAnimator = nil
}
}
private func setupGradientAnimations() {
guard let _ = self.component else {
return
}
if let _ = self.badgeForeground.animation(forKey: "movement") {
} else {
CATransaction.begin()
let badgeOffset = (self.badgeForeground.frame.width - self.badgeView.bounds.width) / 2.0
let badgePreviousValue = self.badgeForeground.position.x
var badgeNewValue: CGFloat = badgeOffset
if badgeOffset - badgePreviousValue < self.badgeForeground.frame.width * 0.25 {
badgeNewValue -= self.badgeForeground.frame.width * 0.35
}
self.badgeForeground.position = CGPoint(x: badgeNewValue, y: self.badgeForeground.bounds.size.height / 2.0)
let lineOffset = 0.0
let linePreviousValue = self.activeBackground.position.x
var lineNewValue: CGFloat = lineOffset
if linePreviousValue < 0.0 {
lineNewValue = 0.0
} else {
lineNewValue = -self.activeContainer.bounds.width * 0.35
}
lineNewValue -= self.activeContainer.frame.minX
self.activeBackground.position = CGPoint(x: lineNewValue, y: 0.0)
let badgeAnimation = CABasicAnimation(keyPath: "position.x")
badgeAnimation.duration = 4.5
badgeAnimation.fromValue = badgePreviousValue
badgeAnimation.toValue = badgeNewValue
badgeAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
CATransaction.setCompletionBlock { [weak self] in
self?.setupGradientAnimations()
}
self.badgeForeground.add(badgeAnimation, forKey: "movement")
let lineAnimation = CABasicAnimation(keyPath: "position.x")
lineAnimation.duration = 4.5
lineAnimation.fromValue = linePreviousValue
lineAnimation.toValue = lineNewValue
lineAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
self.activeBackground.add(lineAnimation, forKey: "movement")
CATransaction.commit()
}
}
}
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, transition: transition)
}
}
private final class LimitSheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let subject: PremiumLimitScreen.Subject
let count: Int32
let cancel: () -> Void
let action: () -> Bool
let dismiss: () -> Void
let openPeer: (EnginePeer) -> Void
let openStats: (() -> Void)?
let openGift: (() -> Void)?
init(context: AccountContext, subject: PremiumLimitScreen.Subject, count: Int32, cancel: @escaping () -> Void, action: @escaping () -> Bool, dismiss: @escaping () -> Void, openPeer: @escaping (EnginePeer) -> Void, openStats: (() -> Void)?, openGift: (() -> Void)?) {
self.context = context
self.subject = subject
self.count = count
self.cancel = cancel
self.action = action
self.dismiss = dismiss
self.openPeer = openPeer
self.openStats = openStats
self.openGift = openGift
}
static func ==(lhs: LimitSheetContent, rhs: LimitSheetContent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.subject != rhs.subject {
return false
}
if lhs.count != rhs.count {
return false
}
return true
}
final class State: ComponentState {
private let context: AccountContext
private var disposable: Disposable?
var initialized = false
var limits: EngineConfiguration.UserLimits
var premiumLimits: EngineConfiguration.UserLimits
var isPremium = false
var myBoostCount: Int32 = 0
var cachedCloseImage: (UIImage, PresentationTheme)?
var cachedChevronImage: (UIImage, PresentationTheme)?
init(context: AccountContext, subject: PremiumLimitScreen.Subject) {
self.context = context
self.limits = EngineConfiguration.UserLimits.defaultValue
self.premiumLimits = EngineConfiguration.UserLimits.defaultValue
super.init()
self.disposable = (context.engine.data.get(
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false),
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true),
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)
) |> deliverOnMainQueue).start(next: { [weak self] result in
if let strongSelf = self {
let (limits, premiumLimits, accountPeer) = result
strongSelf.initialized = true
strongSelf.limits = limits
strongSelf.premiumLimits = premiumLimits
strongSelf.isPremium = accountPeer?.isPremium ?? false
strongSelf.updated(transition: .immediate)
}
})
}
deinit {
self.disposable?.dispose()
}
}
func makeState() -> State {
return State(context: self.context, subject: self.subject)
}
static var body: Body {
let closeButton = Child(Button.self)
let title = Child(MultilineTextComponent.self)
let text = Child(BalancedTextComponent.self)
let alternateText = Child(List<Empty>.self)
let limit = Child(PremiumLimitDisplayComponent.self)
let linkButton = Child(SolidRoundedButtonComponent.self)
let button = Child(SolidRoundedButtonComponent.self)
let peerShortcut = Child(Button.self)
let statsButton = Child(Button.self)
let orLeftLine = Child(Rectangle.self)
let orRightLine = Child(Rectangle.self)
let orText = Child(MultilineTextComponent.self)
let giftText = Child(BalancedTextComponent.self)
return { context in
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
let component = context.component
let theme = environment.theme
let strings = environment.strings
let state = context.state
let subject = component.subject
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 })
let isPremiumDisabled: Bool
if case .storiesChannelBoost = subject {
isPremiumDisabled = false
} else {
isPremiumDisabled = premiumConfiguration.isPremiumDisabled
}
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
let textSideInset: CGFloat = 32.0 + environment.safeInsets.left
let closeImage: UIImage
if let (image, theme) = state.cachedCloseImage, theme === environment.theme {
closeImage = image
} else {
closeImage = generateCloseButtonImage(backgroundColor: UIColor(rgb: 0x808084, alpha: 0.1), foregroundColor: theme.actionSheet.inputClearButtonColor)!
state.cachedCloseImage = (closeImage, theme)
}
let closeButton = closeButton.update(
component: Button(
content: AnyComponent(Image(image: closeImage)),
action: { [weak component] in
component?.dismiss()
component?.cancel()
}
),
availableSize: CGSize(width: 30.0, height: 30.0),
transition: .immediate
)
context.add(closeButton
.position(CGPoint(x: context.availableSize.width - environment.safeInsets.left - closeButton.size.width, y: 28.0))
)
var boostUpdated = false
var peerShortcutChild: _UpdatedChildComponent?
var useAlternateText = false
var alternateTitle = ""
var alternateBadge: String?
var titleText = strings.Premium_LimitReached
var actionButtonText: String?
var actionButtonHasGloss = true
var gradientedActionButton = true
var buttonAnimationName: String? = "premium_x2"
var buttonIconName: String?
let iconName: String
var badgeText: String
var string: String
let defaultValue: String
var defaultTitle = strings.Premium_Free
var premiumTitle = strings.Premium_Premium
let premiumValue: String
let badgePosition: CGFloat
let badgeGraphPosition: CGFloat
var invertProgress = false
switch subject {
case .folders:
let limit = state.limits.maxFoldersCount
let premiumLimit = state.premiumLimits.maxFoldersCount
iconName = "Premium/Folder"
badgeText = "\(component.count)"
string = component.count >= premiumLimit ? strings.Premium_MaxFoldersCountFinalText("\(premiumLimit)").string : strings.Premium_MaxFoldersCountText("\(limit)", "\(premiumLimit)").string
defaultValue = component.count > limit ? "\(limit)" : ""
premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)"
if component.count >= premiumLimit {
badgeGraphPosition = max(0.15, CGFloat(limit) / CGFloat(premiumLimit))
} else {
badgeGraphPosition = max(0.15, CGFloat(component.count) / CGFloat(premiumLimit))
}
badgePosition = max(0.15, CGFloat(component.count) / CGFloat(premiumLimit))
if !state.isPremium && badgePosition > 0.5 {
string = strings.Premium_MaxFoldersCountText("\(limit)", "\(premiumLimit)").string
}
if isPremiumDisabled {
badgeText = "\(limit)"
string = strings.Premium_MaxFoldersCountNoPremiumText("\(limit)").string
}
case .chatsPerFolder:
let limit = state.limits.maxFolderChatsCount
let premiumLimit = state.premiumLimits.maxFolderChatsCount
iconName = "Premium/Chat"
badgeText = "\(component.count)"
string = component.count >= premiumLimit ? strings.Premium_MaxChatsInFolderFinalText("\(premiumLimit)").string : strings.Premium_MaxChatsInFolderText("\(limit)", "\(premiumLimit)").string
defaultValue = component.count > limit ? "\(limit)" : ""
premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)"
badgePosition = CGFloat(component.count) / CGFloat(premiumLimit)
badgeGraphPosition = badgePosition
if isPremiumDisabled {
badgeText = "\(limit)"
string = strings.Premium_MaxChatsInFolderNoPremiumText("\(limit)").string
}
case .channels:
let limit = state.limits.maxChannelsCount
let premiumLimit = state.premiumLimits.maxChannelsCount
iconName = "Premium/Chat"
badgeText = "\(component.count)"
string = component.count >= premiumLimit ? strings.Premium_MaxChannelsFinalText("\(premiumLimit)").string : strings.Premium_MaxChannelsText("\(limit)", "\(premiumLimit)").string
defaultValue = component.count > limit ? "\(limit)" : ""
premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)"
if component.count >= premiumLimit {
badgeGraphPosition = max(0.15, CGFloat(limit) / CGFloat(premiumLimit))
} else {
badgeGraphPosition = max(0.15, CGFloat(component.count) / CGFloat(premiumLimit))
}
badgePosition = max(0.15, CGFloat(component.count) / CGFloat(premiumLimit))
if isPremiumDisabled {
badgeText = "\(limit)"
string = strings.Premium_MaxChannelsNoPremiumText("\(limit)").string
}
case .linksPerSharedFolder:
/*let count: Int32 = 5 + Int32("".count)// component.count
let limit: Int32 = 5 + Int32("".count)//state.limits.maxSharedFolderInviteLinks
let premiumLimit: Int32 = 100 + Int32("".count)//state.premiumLimits.maxSharedFolderInviteLinks*/
let count: Int32 = component.count
let limit: Int32 = state.limits.maxSharedFolderInviteLinks
let premiumLimit: Int32 = state.premiumLimits.maxSharedFolderInviteLinks
iconName = "Premium/Link"
badgeText = "\(count)"
string = count >= premiumLimit ? strings.Premium_MaxSharedFolderLinksFinalText("\(premiumLimit)").string : strings.Premium_MaxSharedFolderLinksText("\(limit)", "\(premiumLimit)").string
defaultValue = count > limit ? "\(limit)" : ""
premiumValue = count >= premiumLimit ? "" : "\(premiumLimit)"
if count >= premiumLimit {
badgeGraphPosition = max(0.15, CGFloat(limit) / CGFloat(premiumLimit))
} else {
badgeGraphPosition = max(0.15, CGFloat(count) / CGFloat(premiumLimit))
}
badgePosition = max(0.15, CGFloat(count) / CGFloat(premiumLimit))
if isPremiumDisabled {
badgeText = "\(limit)"
string = strings.Premium_MaxSharedFolderLinksNoPremiumText("\(limit)").string
}
buttonAnimationName = nil
case .membershipInSharedFolders:
let limit = state.limits.maxSharedFolderJoin
let premiumLimit = state.premiumLimits.maxSharedFolderJoin
iconName = "Premium/Folder"
badgeText = "\(component.count)"
string = component.count >= premiumLimit ? strings.Premium_MaxSharedFolderMembershipFinalText("\(premiumLimit)").string : strings.Premium_MaxSharedFolderMembershipText("\(limit)", "\(premiumLimit)").string
defaultValue = component.count > limit ? "\(limit)" : ""
premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)"
if component.count >= premiumLimit {
badgeGraphPosition = max(0.15, CGFloat(limit) / CGFloat(premiumLimit))
} else {
badgeGraphPosition = max(0.15, CGFloat(component.count) / CGFloat(premiumLimit))
}
badgePosition = max(0.15, CGFloat(component.count) / CGFloat(premiumLimit))
if isPremiumDisabled {
badgeText = "\(limit)"
string = strings.Premium_MaxSharedFolderMembershipNoPremiumText("\(limit)").string
}
buttonAnimationName = nil
case .pins:
let limit = state.limits.maxPinnedChatCount
let premiumLimit = state.premiumLimits.maxPinnedChatCount
iconName = "Premium/Pin"
badgeText = "\(component.count)"
string = component.count >= premiumLimit ? strings.Premium_MaxPinsFinalText("\(premiumLimit)").string : strings.Premium_MaxPinsText("\(limit)", "\(premiumLimit)").string
defaultValue = component.count > limit ? "\(limit)" : ""
premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)"
badgePosition = CGFloat(component.count) / CGFloat(premiumLimit)
badgeGraphPosition = badgePosition
if isPremiumDisabled {
badgeText = "\(limit)"
string = strings.Premium_MaxPinsNoPremiumText("\(limit)").string
}
case .pinnedSavedPeers:
//TODO:localize
let limit = state.limits.maxPinnedSavedChatCount
let premiumLimit = state.premiumLimits.maxPinnedSavedChatCount
iconName = "Premium/Pin"
badgeText = "\(component.count)"
string = component.count >= premiumLimit ? strings.Premium_MaxPinsFinalText("\(premiumLimit)").string : strings.Premium_MaxPinsText("\(limit)", "\(premiumLimit)").string
defaultValue = component.count > limit ? "\(limit)" : ""
premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)"
badgePosition = max(0.15, min(0.85, CGFloat(component.count) / CGFloat(premiumLimit)))
badgeGraphPosition = badgePosition
buttonAnimationName = nil
if isPremiumDisabled {
badgeText = "\(limit)"
string = strings.Premium_MaxPinsNoPremiumText("\(limit)").string
}
case .files:
let limit = Int64(state.limits.maxUploadFileParts) * 512 * 1024 + 1024 * 1024 * 100
let premiumLimit = Int64(state.premiumLimits.maxUploadFileParts) * 512 * 1024 + 1024 * 1024 * 100
iconName = "Premium/File"
badgeText = dataSizeString(component.count == 4 ? premiumLimit : limit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator))
string = component.count == 4 ? strings.Premium_MaxFileSizeFinalText(dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator))).string : strings.Premium_MaxFileSizeText(dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator))).string
defaultValue = component.count == 4 ? dataSizeString(limit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator)) : ""
premiumValue = component.count != 4 ? dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator)) : ""
badgePosition = component.count == 4 ? 1.0 : 0.5
badgeGraphPosition = 0.5
titleText = strings.Premium_FileTooLarge
if isPremiumDisabled {
badgeText = dataSizeString(limit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator))
string = strings.Premium_MaxFileSizeNoPremiumText(dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator))).string
}
case .accounts:
let limit = 3
let premiumLimit = limit + 1
iconName = "Premium/Account"
badgeText = "\(component.count)"
string = component.count >= premiumLimit ? strings.Premium_MaxAccountsFinalText("\(premiumLimit)").string : strings.Premium_MaxAccountsText("\(limit)").string
defaultValue = component.count > limit ? "\(limit)" : ""
premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)"
if component.count == limit {
badgePosition = 0.5
} else {
badgePosition = min(1.0, CGFloat(component.count) / CGFloat(premiumLimit))
}
badgeGraphPosition = 0.5
buttonAnimationName = "premium_addone"
if isPremiumDisabled {
badgeText = "\(limit)"
string = strings.Premium_MaxAccountsNoPremiumText("\(limit)").string
}
case .expiringStories:
let limit = state.limits.maxExpiringStoriesCount
let premiumLimit = state.premiumLimits.maxExpiringStoriesCount
iconName = "Premium/Stories"
badgeText = "\(limit)"
string = component.count >= premiumLimit ? strings.Premium_MaxExpiringStoriesFinalText("\(premiumLimit)").string : strings.Premium_MaxExpiringStoriesText("\(limit)", "\(premiumLimit)").string
defaultValue = ""
premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)"
badgePosition = max(0.32, CGFloat(component.count) / CGFloat(premiumLimit))
badgeGraphPosition = badgePosition
if isPremiumDisabled {
badgeText = "\(limit)"
string = strings.Premium_MaxExpiringStoriesNoPremiumText("\(limit)").string
}
buttonAnimationName = nil
case .storiesWeekly:
let limit = state.limits.maxStoriesWeeklyCount
let premiumLimit = state.premiumLimits.maxStoriesWeeklyCount
iconName = "Premium/Stories"
badgeText = "\(limit)"
string = component.count >= premiumLimit ? strings.Premium_MaxStoriesWeeklyFinalText("\(premiumLimit)").string : strings.Premium_MaxStoriesWeeklyText("\(limit)", "\(premiumLimit)").string
defaultValue = ""
premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)"
badgePosition = max(0.32, CGFloat(component.count) / CGFloat(premiumLimit))
badgeGraphPosition = badgePosition
if isPremiumDisabled {
badgeText = "\(limit)"
string = strings.Premium_MaxStoriesWeeklyNoPremiumText("\(limit)").string
}
buttonAnimationName = nil
case .storiesMonthly:
let limit = state.limits.maxStoriesMonthlyCount
let premiumLimit = state.premiumLimits.maxStoriesMonthlyCount
iconName = "Premium/Stories"
badgeText = "\(limit)"
string = component.count >= premiumLimit ? strings.Premium_MaxStoriesMonthlyFinalText("\(premiumLimit)").string : strings.Premium_MaxStoriesMonthlyText("\(limit)", "\(premiumLimit)").string
defaultValue = ""
premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)"
badgePosition = max(0.32, CGFloat(component.count) / CGFloat(premiumLimit))
badgeGraphPosition = badgePosition
if isPremiumDisabled {
badgeText = "\(limit)"
string = strings.Premium_MaxStoriesMonthlyNoPremiumText("\(limit)").string
}
buttonAnimationName = nil
case let .storiesChannelBoost(peer, boostSubject, isCurrent, level, currentLevelBoosts, nextLevelBoosts, link, myBoostCount, canBoostAgain):
if link == nil, !isCurrent, state.initialized {
peerShortcutChild = peerShortcut.update(
component: Button(
content: AnyComponent(
PeerShortcutComponent(
context: component.context,
theme: environment.theme,
peer: peer,
badge: myBoostCount > 0 ? "\(myBoostCount)" : nil
)
),
action: {
component.dismiss()
Queue.mainQueue().after(0.35) {
component.openPeer(peer)
}
}
),
availableSize: CGSize(width: context.availableSize.width - 32.0, height: context.availableSize.height),
transition: .immediate
)
}
if link != nil {
gradientedActionButton = false
}
if let _ = link, let openStats = component.openStats {
let _ = openStats
let statsButton = statsButton.update(
component: Button(
content: AnyComponent(
BundleIconComponent(
name: "Premium/Stats",
tintColor: environment.theme.list.itemAccentColor
)
),
action: {
component.dismiss()
Queue.mainQueue().after(0.35) {
openStats()
}
}
).minSize(CGSize(width: 44.0, height: 44.0)),
availableSize: context.availableSize,
transition: .immediate
)
context.add(statsButton
.position(CGPoint(x: 31.0, y: 28.0))
)
}
if myBoostCount > 0 && state.myBoostCount != myBoostCount {
state.myBoostCount = myBoostCount
boostUpdated = true
}
useAlternateText = myBoostCount > 0
iconName = "Premium/Boost"
badgeText = "\(component.count)"
var remaining: Int32?
if let nextLevelBoosts {
remaining = nextLevelBoosts - component.count
}
if let _ = link {
if let remaining {
let storiesString = strings.ChannelBoost_StoriesPerDay(level + 1)
let valueString = strings.ChannelBoost_MoreBoosts(remaining)
switch boostSubject {
case .stories:
if level == 0 {
titleText = strings.ChannelBoost_EnableStories
string = strings.ChannelBoost_EnableStoriesText(valueString).string
} else {
titleText = strings.ChannelBoost_IncreaseLimit
string = strings.ChannelBoost_IncreaseLimitText(valueString, storiesString).string
}
case let .nameColors(colors):
titleText = strings.ChannelBoost_EnableColors
let colorLevel = requiredBoostSubjectLevel(subject: .nameColors(colors: colors), context: component.context, configuration: premiumConfiguration)
string = strings.ChannelBoost_EnableColorsLevelText("\(colorLevel)").string
case let .channelReactions(reactionCount):
titleText = strings.ChannelBoost_CustomReactions
string = strings.ChannelBoost_CustomReactionsText("\(reactionCount)", "\(reactionCount)").string
}
} else {
let storiesString = strings.ChannelBoost_StoriesPerDay(level)
titleText = strings.ChannelBoost_MaxLevelReached
string = strings.ChannelBoost_MaxLevelReachedTextAuthor("\(level)", storiesString).string
}
actionButtonText = strings.ChannelBoost_CopyLink
buttonIconName = "Premium/CopyLink"
actionButtonHasGloss = false
} else {
let storiesString = strings.ChannelBoost_StoriesPerDay(level + 1)
if let remaining {
let boostsString = strings.ChannelBoost_MoreBoosts(remaining)
if level == 0 {
titleText = isCurrent ? strings.ChannelBoost_EnableStoriesForChannel : strings.ChannelBoost_EnableStoriesForOtherChannel
string = strings.ChannelBoost_EnableStoriesForChannelText(peer.compactDisplayTitle, boostsString).string
} else {
titleText = strings.ChannelBoost_HelpUpgradeChannel
string = strings.ChannelBoost_HelpUpgradeChannelText(peer.compactDisplayTitle, boostsString, storiesString).string
}
actionButtonText = strings.ChannelBoost_BoostChannel
buttonIconName = "Premium/BoostChannel"
} else {
titleText = strings.ChannelBoost_MaxLevelReached
string = strings.ChannelBoost_BoostedChannelReachedLevel("\(level)", storiesString).string
actionButtonText = strings.Common_OK
buttonIconName = nil
actionButtonHasGloss = false
}
}
buttonAnimationName = nil
defaultTitle = strings.ChannelBoost_Level("\(level)").string
defaultValue = ""
premiumValue = strings.ChannelBoost_Level("\(level + 1)").string
premiumTitle = ""
if myBoostCount > 0 {
alternateTitle = isCurrent ? strings.ChannelBoost_YouBoostedChannelText(peer.compactDisplayTitle).string : strings.ChannelBoost_YouBoostedOtherChannelText
if myBoostCount > 1 {
alternateBadge = "X\(myBoostCount)"
}
let storiesString = strings.ChannelBoost_StoriesPerDay(level + 1)
if let _ = remaining, canBoostAgain {
actionButtonText = strings.ChannelBoost_BoostAgain
} else {
buttonIconName = nil
actionButtonText = environment.strings.Common_OK
actionButtonHasGloss = false
}
if let remaining {
let boostsString = strings.ChannelBoost_MoreBoosts(remaining)
if level == 0 {
if remaining == 0 {
string = strings.ChannelBoost_EnabledStoriesForChannelText
} else {
string = strings.ChannelBoost_EnableStoriesMoreRequired(boostsString).string
}
}
else {
if remaining == 0 {
string = strings.ChannelBoost_BoostedChannelReachedLevel("\(level + 1)", storiesString).string
} else {
string = strings.ChannelBoost_BoostedChannelMoreRequired(boostsString, storiesString).string
}
}
} else {
string = strings.ChannelBoost_BoostedChannelReachedLevel("\(level + 1)", storiesString).string
}
}
let progress: CGFloat
if let nextLevelBoosts {
progress = CGFloat(component.count - currentLevelBoosts) / CGFloat(nextLevelBoosts - currentLevelBoosts)
} else {
progress = 1.0
}
badgePosition = progress
badgeGraphPosition = progress
invertProgress = true
}
var reachedMaximumLimit = badgePosition >= 1.0
if case .folders = subject, !state.isPremium {
reachedMaximumLimit = false
}
if case .storiesChannelBoost = component.subject {
reachedMaximumLimit = false
}
let contentSize: CGSize
if state.initialized {
let title = title.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(
string: titleText,
font: Font.semibold(17.0),
textColor: theme.actionSheet.primaryTextColor,
paragraphAlignment: .center
)),
horizontalAlignment: .center,
maximumNumberOfLines: 1
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
let textFont = Font.regular(15.0)
let boldTextFont = Font.semibold(15.0)
let textColor = theme.actionSheet.primaryTextColor
let linkColor = theme.actionSheet.controlAccentColor
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
})
var textChild: _UpdatedChildComponent?
var alternateTextChild: _UpdatedChildComponent?
if useAlternateText {
alternateTextChild = alternateText.update(
component: List(
[
AnyComponentWithIdentity(
id: "title",
component: AnyComponent(
BoostedTitleContent(text: NSAttributedString(string: alternateTitle, font: Font.semibold(15.0), textColor: textColor), badge: alternateBadge)
)
),
AnyComponentWithIdentity(
id: "text",
component: AnyComponent(
BalancedTextComponent(
text: .markdown(text: string, attributes: markdownAttributes),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.1
)
)
)
],
centerAlignment: true
),
availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height),
transition: .immediate
)
// alternateTextChild = alternateText.update(
// component: BalancedTextComponent(
// text: .markdown(text: string, attributes: markdownAttributes),
// horizontalAlignment: .center,
// maximumNumberOfLines: 0,
// lineSpacing: 0.1
// ),
// availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height),
// transition: .immediate
// )
} else {
textChild = text.update(
component: BalancedTextComponent(
text: .markdown(text: string, attributes: markdownAttributes),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.1
),
availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height),
transition: .immediate
)
}
var topOffset: CGFloat = 0.0
if let peerShortcutChild {
context.add(peerShortcutChild
.position(CGPoint(x: context.availableSize.width / 2.0, y: 64.0))
)
topOffset += 38.0
}
let gradientColors: [UIColor]
var buttonGradientColors: [UIColor]
if isPremiumDisabled {
gradientColors = [
UIColor(rgb: 0x007afe),
UIColor(rgb: 0x5494ff)
]
} else {
gradientColors = [
UIColor(rgb: 0x0077ff),
UIColor(rgb: 0x6b93ff),
UIColor(rgb: 0x8878ff),
UIColor(rgb: 0xe46ace)
]
}
if gradientedActionButton {
buttonGradientColors = gradientColors
} else {
buttonGradientColors = [
UIColor(rgb: 0x007afe),
UIColor(rgb: 0x5494ff)
]
}
var limitTransition: Transition = .immediate
if boostUpdated {
limitTransition = .easeInOut(duration: 0.35)
}
let isIncreaseButton = !reachedMaximumLimit && !isPremiumDisabled
let button = button.update(
component: SolidRoundedButtonComponent(
title: actionButtonText ?? (isIncreaseButton ? strings.Premium_IncreaseLimit : strings.Common_OK),
theme: SolidRoundedButtonComponent.Theme(
backgroundColor: .black,
backgroundColors: buttonGradientColors,
foregroundColor: .white
),
font: .bold,
fontSize: 17.0,
height: 50.0,
cornerRadius: 10.0,
gloss: isIncreaseButton && actionButtonHasGloss,
iconName: buttonIconName,
animationName: isIncreaseButton ? buttonAnimationName : nil,
iconPosition: buttonIconName != nil ? .left : .right,
action: {
if isIncreaseButton {
if component.action() {
component.dismiss()
}
} else {
component.dismiss()
}
}
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0),
transition: context.transition
)
var buttonOffset: CGFloat = 0.0
var textOffset: CGFloat = 184.0 + topOffset
if case let .storiesChannelBoost(_, _, _, _, _, _, link, _, _) = component.subject {
if let link {
let linkButton = linkButton.update(
component: SolidRoundedButtonComponent(
title: link.replacingOccurrences(of: "https://", with: ""),
theme: SolidRoundedButtonComponent.Theme(
backgroundColor: theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.3),
backgroundColors: [],
foregroundColor: environment.theme.list.itemPrimaryTextColor
),
font: .regular,
fontSize: 17.0,
height: 50.0,
cornerRadius: 10.0,
action: {
if component.action() {
component.dismiss()
}
}
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0),
transition: context.transition
)
buttonOffset += 66.0
let linkFrame = CGRect(origin: CGPoint(x: sideInset, y: textOffset + (textChild?.size ?? .zero).height + 24.0), size: linkButton.size)
context.add(linkButton
.position(CGPoint(x: linkFrame.midX, y: linkFrame.midY))
)
}
}
if isPremiumDisabled {
textOffset -= 68.0
}
context.add(title
.position(CGPoint(x: context.availableSize.width / 2.0, y: 28.0))
)
var textSize: CGSize
if let textChild {
textSize = textChild.size
textOffset += textSize.height / 2.0
context.add(textChild
.position(CGPoint(x: context.availableSize.width / 2.0, y: textOffset))
.appear(Transition.Appear({ _, view, transition in
transition.animatePosition(view: view, from: CGPoint(x: 0.0, y: 64.0), to: .zero, additive: true)
transition.animateAlpha(view: view, from: 0.0, to: 1.0)
}))
.disappear(Transition.Disappear({ view, transition, completion in
view.superview?.sendSubviewToBack(view)
transition.animatePosition(view: view, from: .zero, to: CGPoint(x: 0.0, y: -64.0), additive: true)
transition.setAlpha(view: view, alpha: 0.0, completion: { _ in
completion()
})
}))
)
} else if let alternateTextChild {
textSize = alternateTextChild.size
textOffset += textSize.height / 2.0
context.add(alternateTextChild
.position(CGPoint(x: context.availableSize.width / 2.0, y: textOffset))
.appear(Transition.Appear({ _, view, transition in
transition.animatePosition(view: view, from: CGPoint(x: 0.0, y: 64.0), to: .zero, additive: true)
transition.animateAlpha(view: view, from: 0.0, to: 1.0)
}))
.disappear(Transition.Disappear({ view, transition, completion in
transition.animatePosition(view: view, from: .zero, to: CGPoint(x: 0.0, y: -64.0), additive: true)
transition.setAlpha(view: view, alpha: 0.0, completion: { _ in
completion()
})
}))
)
} else {
textSize = .zero
}
let limit = limit.update(
component: PremiumLimitDisplayComponent(
inactiveColor: theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.3),
activeColors: gradientColors,
inactiveTitle: defaultTitle,
inactiveValue: defaultValue,
inactiveTitleColor: theme.list.itemPrimaryTextColor,
activeTitle: premiumTitle,
activeValue: premiumValue,
activeTitleColor: .white,
badgeIconName: iconName,
badgeText: badgeText,
badgePosition: badgePosition,
badgeGraphPosition: badgeGraphPosition,
invertProgress: invertProgress,
isPremiumDisabled: isPremiumDisabled
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height),
transition: limitTransition
)
context.add(limit
.position(CGPoint(x: context.availableSize.width / 2.0, y: limit.size.height / 2.0 + 44.0 + topOffset))
)
let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: textOffset + ceil(textSize.height / 2.0) + buttonOffset + 24.0), size: button.size)
context.add(button
.position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY))
)
var additionalContentHeight: CGFloat = 0.0
if case let .storiesChannelBoost(_, _, _, _, _, _, link, _, _) = component.subject, link != nil, let openGift = component.openGift {
let orText = orText.update(
component: MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.ChannelBoost_Or, font: Font.regular(15.0), textColor: textColor.withAlphaComponent(0.8), paragraphAlignment: .center))),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height),
transition: .immediate
)
context.add(orText
.position(CGPoint(x: context.availableSize.width / 2.0, y: buttonFrame.maxY + 27.0))
)
let orLeftLine = orLeftLine.update(
component: Rectangle(color: environment.theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.3)),
availableSize: CGSize(width: 90.0, height: 1.0 - UIScreenPixel),
transition: .immediate
)
context.add(orLeftLine
.position(CGPoint(x: context.availableSize.width / 2.0 - orText.size.width / 2.0 - 11.0 - 45.0, y: buttonFrame.maxY + 27.0))
)
let orRightLine = orRightLine.update(
component: Rectangle(color: environment.theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.3)),
availableSize: CGSize(width: 90.0, height: 1.0 - UIScreenPixel),
transition: .immediate
)
context.add(orRightLine
.position(CGPoint(x: context.availableSize.width / 2.0 + orText.size.width / 2.0 + 11.0 + 45.0, y: buttonFrame.maxY + 27.0))
)
if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== environment.theme {
state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: linkColor)!, environment.theme)
}
let giftString = environment.strings.Premium_BoostByGiftDescription2
let giftAttributedString = parseMarkdownIntoAttributedString(giftString, attributes: markdownAttributes).mutableCopy() as! NSMutableAttributedString
if let range = giftAttributedString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 {
giftAttributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: giftAttributedString.string))
}
let giftText = giftText.update(
component: BalancedTextComponent(
text: .plain(giftAttributedString),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.1,
highlightColor: linkColor.withAlphaComponent(0.2),
highlightAction: { _ in
return nil
},
tapAction: { _, _ in
openGift()
}
),
availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height),
transition: .immediate
)
context.add(giftText
.position(CGPoint(x: context.availableSize.width / 2.0, y: buttonFrame.maxY + 50.0 + giftText.size.height / 2.0))
)
additionalContentHeight += giftText.size.height + 50.0
}
contentSize = CGSize(width: context.availableSize.width, height: buttonFrame.maxY + additionalContentHeight + 5.0 + environment.safeInsets.bottom)
} else {
var height: CGFloat = 351.0
if isPremiumDisabled {
height -= 78.0
}
if case let .storiesChannelBoost(_, _, isCurrent, _, _, _, link, _, _) = component.subject {
if link != nil {
height += 66.0
if let _ = component.openGift {
height += 100.0
}
} else {
if isCurrent {
height -= 53.0
} else {
height -= 53.0 - 32.0
}
}
}
contentSize = CGSize(width: context.availableSize.width, height: height + environment.safeInsets.bottom)
}
return contentSize
}
}
}
private final class LimitSheetComponent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let subject: PremiumLimitScreen.Subject
let count: Int32
let cancel: () -> Void
let action: () -> Bool
let openPeer: (EnginePeer) -> Void
let openStats: (() -> Void)?
let openGift: (() -> Void)?
init(context: AccountContext, subject: PremiumLimitScreen.Subject, count: Int32, cancel: @escaping () -> Void, action: @escaping () -> Bool, openPeer: @escaping (EnginePeer) -> Void, openStats: (() -> Void)?, openGift: (() -> Void)?) {
self.context = context
self.subject = subject
self.count = count
self.cancel = cancel
self.action = action
self.openPeer = openPeer
self.openStats = openStats
self.openGift = openGift
}
static func ==(lhs: LimitSheetComponent, rhs: LimitSheetComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.subject != rhs.subject {
return false
}
if lhs.count != rhs.count {
return false
}
return true
}
static var body: Body {
let sheet = Child(SheetComponent<EnvironmentType>.self)
let animateOut = StoredActionSlot(Action<Void>.self)
let sheetExternalState = SheetComponent<EnvironmentType>.ExternalState()
return { context in
let environment = context.environment[EnvironmentType.self]
let controller = environment.controller
let sheet = sheet.update(
component: SheetComponent<EnvironmentType>(
content: AnyComponent<EnvironmentType>(LimitSheetContent(
context: context.component.context,
subject: context.component.subject,
count: context.component.count,
cancel: context.component.cancel,
action: context.component.action,
dismiss: {
animateOut.invoke(Action { _ in
if let controller = controller() {
controller.dismiss(completion: nil)
}
})
},
openPeer: context.component.openPeer,
openStats: context.component.openStats,
openGift: context.component.openGift
)),
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
followContentSizeChanges: true,
externalState: sheetExternalState,
animateOut: animateOut
),
environment: {
environment
SheetComponentEnvironment(
isDisplaying: environment.value.isVisible,
isCentered: environment.metrics.widthClass == .regular,
hasInputHeight: !environment.inputHeight.isZero,
regularMetricsSize: CGSize(width: 430.0, height: 900.0),
dismiss: { animated in
if animated {
animateOut.invoke(Action { _ in
if let controller = controller() {
controller.dismiss(completion: nil)
}
})
} else {
if let controller = controller() {
controller.dismiss(completion: nil)
}
}
}
)
},
availableSize: context.availableSize,
transition: context.transition
)
context.add(sheet
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
)
if let controller = controller(), !controller.automaticallyControlPresentationContextLayout {
let layout = ContainerViewLayout(
size: context.availableSize,
metrics: environment.metrics,
deviceMetrics: environment.deviceMetrics,
intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: max(environment.safeInsets.bottom, sheetExternalState.contentHeight), right: 0.0),
safeInsets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: 0.0, right: environment.safeInsets.right),
additionalInsets: .zero,
statusBarHeight: environment.statusBarHeight,
inputHeight: nil,
inputHeightIsInteractivellyChanging: false,
inVoiceOver: false
)
controller.presentationContext.containerLayoutUpdated(layout, transition: context.transition.containedViewLayoutTransition)
}
return context.availableSize
}
}
}
public class PremiumLimitScreen: ViewControllerComponentContainer {
public enum Subject: Equatable {
case folders
case chatsPerFolder
case pins
case pinnedSavedPeers
case files
case accounts
case linksPerSharedFolder
case membershipInSharedFolders
case channels
case expiringStories
case storiesWeekly
case storiesMonthly
public enum BoostSubject: Equatable {
case stories
case nameColors(colors: PeerNameColor)
case channelReactions(reactionCount: Int)
}
case storiesChannelBoost(peer: EnginePeer, boostSubject: BoostSubject, isCurrent: Bool, level: Int32, currentLevelBoosts: Int32, nextLevelBoosts: Int32?, link: String?, myBoostCount: Int32, canBoostAgain: Bool)
}
private let context: AccountContext
private var action: (() -> Bool)?
private let openPeer: (EnginePeer) -> Void
public var disposed: () -> Void = {}
private let hapticFeedback = HapticFeedback()
public init(context: AccountContext, subject: PremiumLimitScreen.Subject, count: Int32, forceDark: Bool = false, cancel: @escaping () -> Void = {}, action: @escaping () -> Bool, openPeer: @escaping (EnginePeer) -> Void = { _ in }, openStats: (() -> Void)? = nil, openGift: (() -> Void)? = nil) {
self.context = context
self.openPeer = openPeer
var actionImpl: (() -> Bool)?
super.init(context: context, component: LimitSheetComponent(context: context, subject: subject, count: count, cancel: {}, action: {
return actionImpl?() ?? true
}, openPeer: openPeer, openStats: openStats, openGift: openGift), navigationBarAppearance: .none, statusBarStyle: .ignore, theme: forceDark ? .dark : .default)
self.navigationPresentation = .flatModal
if case .storiesChannelBoost = subject {
self.automaticallyControlPresentationContextLayout = false
}
self.wasDismissed = cancel
actionImpl = { [weak self] in
if action() {
self?.wasDismissed = nil
return true
} else {
return false
}
}
self.action = actionImpl
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.disposed()
}
public override func viewDidLoad() {
super.viewDidLoad()
self.view.disablesInteractiveModalDismiss = true
}
public func dismissAnimated() {
if let view = self.node.hostView.findTaggedView(tag: SheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? SheetComponent<ViewControllerComponentContainer.Environment>.View {
view.dismissAnimated()
}
}
public func updateSubject(_ subject: Subject, count: Int32) {
let component = LimitSheetComponent(
context: self.context,
subject: subject,
count: count,
cancel: {},
action: { [weak self] in
return self?.action?() ?? true
},
openPeer: self.openPeer,
openStats: nil,
openGift: nil
)
self.updateComponent(component: AnyComponent(component), transition: .easeInOut(duration: 0.2))
self.animateSuccess()
}
public func animateSuccess() {
self.hapticFeedback.impact()
self.view.addSubview(ConfettiView(frame: self.view.bounds))
}
}
private final class PeerShortcutComponent: Component {
let context: AccountContext
let theme: PresentationTheme
let peer: EnginePeer
let badge: String?
init(context: AccountContext, theme: PresentationTheme, peer: EnginePeer, badge: String?) {
self.context = context
self.theme = theme
self.peer = peer
self.badge = badge
}
static func ==(lhs: PeerShortcutComponent, rhs: PeerShortcutComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.peer != rhs.peer {
return false
}
if lhs.badge != rhs.badge {
return false
}
return true
}
final class View: UIView {
private let backgroundView = UIView()
private let avatarNode: AvatarNode
private let text = ComponentView<Empty>()
private var component: PeerShortcutComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 18.0))
super.init(frame: frame)
self.backgroundView.clipsToBounds = true
self.backgroundView.layer.cornerRadius = 16.0
self.addSubview(self.backgroundView)
self.addSubnode(self.avatarNode)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: PeerShortcutComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.component = component
self.state = state
self.backgroundView.backgroundColor = component.theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.3)
self.avatarNode.frame = CGRect(origin: CGPoint(x: 1.0, y: 1.0), size: CGSize(width: 30.0, height: 30.0))
self.avatarNode.setPeer(
context: component.context,
theme: component.context.sharedContext.currentPresentationData.with({ $0 }).theme,
peer: component.peer,
synchronousLoad: true
)
let textSize = self.text.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: component.peer.compactDisplayTitle, font: Font.medium(15.0), textColor: component.theme.list.itemPrimaryTextColor, paragraphAlignment: .left))
)
),
environment: {},
containerSize: CGSize(width: availableSize.width - 50.0, height: availableSize.height)
)
let size = CGSize(width: 30.0 + textSize.width + 20.0, height: 32.0)
if let view = self.text.view {
if view.superview == nil {
self.addSubview(view)
}
let textFrame = CGRect(origin: CGPoint(x: 38.0, y: floorToScreenPixels((size.height - textSize.height) / 2.0)), size: textSize)
view.frame = textFrame
}
self.backgroundView.frame = CGRect(origin: .zero, size: size)
return size
}
}
func makeView() -> View {
return View(frame: CGRect())
}
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)
}
}
public final class BoostIconComponent: Component {
let hasIcon: Bool
let text: String
public init(hasIcon: Bool, text: String) {
self.hasIcon = hasIcon
self.text = text
}
public static func ==(lhs: BoostIconComponent, rhs: BoostIconComponent) -> Bool {
if lhs.hasIcon != rhs.hasIcon {
return false
}
if lhs.text != rhs.text {
return false
}
return true
}
public final class View: UIView {
private let badgeBackground = UIView()
private let badgeIcon: UIImageView
private let badgeText = ComponentView<Empty>()
private var component: BoostIconComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.badgeIcon = UIImageView(image: UIImage(bundleImageName: "Premium/BoostButtonIcon"))
super.init(frame: frame)
self.badgeBackground.clipsToBounds = true
self.badgeBackground.backgroundColor = UIColor(rgb: 0x9671ff)
self.addSubview(self.badgeBackground)
self.addSubview(self.badgeIcon)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: BoostIconComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.component = component
self.state = state
let textSize = self.badgeText.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: component.text, font: Font.with(size: 10.0, design: .round, weight: .bold), textColor: .white, paragraphAlignment: .left))
)
),
environment: {},
containerSize: availableSize
)
let spacing: CGFloat = 2.0
var totalWidth = textSize.width
var iconSize = CGSize()
if let icon = self.badgeIcon.image, component.hasIcon {
iconSize = CGSize(width: icon.size.width * 0.9, height: icon.size.height * 0.9)
totalWidth += spacing + icon.size.width
}
let size = CGSize(width: totalWidth + 8.0, height: 19.0)
let iconFrame = CGRect(x: floorToScreenPixels((size.width - totalWidth) / 2.0 + 1.0), y: 4.0 + UIScreenPixel, width: iconSize.width, height: iconSize.height)
self.badgeIcon.frame = iconFrame
let textFrame = CGRect(origin: CGPoint(x: component.hasIcon ? iconFrame.maxX + spacing : 5.0, y: 4.0), size: textSize)
if let view = self.badgeText.view {
if view.superview == nil {
self.addSubview(view)
}
view.frame = textFrame
}
self.badgeBackground.frame = CGRect(origin: .zero, size: size)
self.badgeBackground.layer.cornerRadius = size.height / 2.0
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)
}
}
final class BoostedTitleContent: CombinedComponent {
let text: NSAttributedString
let badge: String?
init(
text: NSAttributedString,
badge: String?
) {
self.text = text
self.badge = badge
}
static func ==(lhs: BoostedTitleContent, rhs: BoostedTitleContent) -> Bool {
if lhs.text != rhs.text {
return false
}
if lhs.badge != rhs.badge {
return false
}
return true
}
static var body: Body {
let text = Child(MultilineTextComponent.self)
let badge = Child(BoostIconComponent.self)
return { context in
let component = context.component
let height: CGFloat = 24.0
var totalWidth: CGFloat = 0.0
let text = text.update(
component: MultilineTextComponent(
text: .plain(component.text),
horizontalAlignment: .center
),
availableSize: CGSize(width: context.availableSize.width - 40.0, height: context.availableSize.height),
transition: .immediate
)
totalWidth += text.size.width
context.add(text
.position(CGPoint(x: text.size.width / 2.0, y: height / 2.0))
)
if let badgeText = component.badge {
let badge = badge.update(
component: BoostIconComponent(hasIcon: false, text: badgeText),
availableSize: CGSize(width: 24.0, height: 24.0),
transition: .immediate
)
totalWidth += badge.size.width + 4.0
context.add(badge
.position(CGPoint(x: totalWidth - badge.size.width / 2.0, y: height / 2.0))
)
}
return CGSize(width: totalWidth, height: height)
}
}
}