2025-11-12 17:47:34 +04:00

3059 lines
146 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import TelegramPresentationData
import ChatPresentationInterfaceState
import ComponentFlow
import AccountContext
import ViewControllerComponent
import TelegramCore
import SwiftSignalKit
import Display
import MultilineTextComponent
import ButtonComponent
import PlainButtonComponent
import Markdown
import SliderComponent
import RoundedRectWithTailPath
import AvatarNode
import BundleIconComponent
import TextFormat
import CheckComponent
import ContextUI
import StarsBalanceOverlayComponent
import StoryLiveChatMessageComponent
import TelegramStringFormatting
import GlassBarButtonComponent
import AnimatedTextComponent
import BotPaymentsUI
import UndoUI
import GiftItemComponent
import LottieComponent
private final class BadgeComponent: Component {
let theme: PresentationTheme
let prefix: String?
let title: String
let subtitle: String?
let subtitleOnTop: Bool
let color: UIColor
init(
theme: PresentationTheme,
prefix: String?,
title: String,
subtitle: String?,
subtitleOnTop: Bool,
color: UIColor
) {
self.theme = theme
self.prefix = prefix
self.title = title
self.subtitle = subtitle
self.subtitleOnTop = subtitleOnTop
self.color = color
}
static func ==(lhs: BadgeComponent, rhs: BadgeComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.prefix != rhs.prefix {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.subtitle != rhs.subtitle {
return false
}
if lhs.subtitleOnTop != rhs.subtitleOnTop {
return false
}
if lhs.color != rhs.color {
return false
}
return true
}
final class View: UIView {
private let badgeView: UIView
private let badgeMaskView: UIView
private let badgeShapeView: UIImageView
private let badgeShapeAnimation = ComponentView<Empty>()
private let badgeForeground: SimpleLayer
let badgeIcon: UIImageView
private let badgeLabel: BadgeLabelView
private let badgeLabelMaskView = UIImageView()
private let prefix = ComponentView<Empty>()
private let subtitle = ComponentView<Empty>()
private var badgeTailPosition: CGFloat = 0.0
private var badgeShapeArguments: (Double, Double, CGSize, CGFloat, CGFloat)?
private var component: BadgeComponent?
private var isUpdating: Bool = false
private var previousAvailableSize: CGSize?
override init(frame: CGRect) {
self.badgeView = UIView()
self.badgeView.alpha = 0.0
self.badgeView.layer.anchorPoint = CGPoint(x: 0.5, y: 1.0)
self.badgeShapeView = UIImageView()
self.badgeMaskView = UIView()
self.badgeMaskView.addSubview(self.badgeShapeView)
self.badgeView.mask = self.badgeMaskView
self.badgeForeground = SimpleLayer()
self.badgeForeground.anchorPoint = CGPoint()
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.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) {
preconditionFailure()
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.badgeView.frame.contains(point) {
return self
} else {
return nil
}
}
func update(component: BadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
if self.component == nil {
self.badgeIcon.image = UIImage(bundleImageName: "Premium/SendStarsStarSliderIcon")?.withRenderingMode(.alwaysTemplate)
}
let previousComponent = self.component
self.component = component
self.badgeIcon.tintColor = .white
self.badgeLabel.color = .white
let badgeLabelSize = self.badgeLabel.update(value: component.title, transition: .easeInOut(duration: 0.12))
let countWidth: CGFloat = badgeLabelSize.width + 3.0
var badgeWidth: CGFloat = countWidth + 54.0
var badgeOffset: CGPoint = .zero
if let prefix = component.prefix {
let prefixSize = self.prefix.update(
transition: .immediate,
component: AnyComponent(Text(text: prefix, font: Font.with(size: 24.0, design: .round, weight: .semibold, traits: []), color: .white)),
environment: {},
containerSize: availableSize
)
if let prefixView = self.prefix.view {
if prefixView.superview == nil {
self.badgeView.addSubview(prefixView)
}
prefixView.frame = CGRect(origin: CGPoint(x: 44.0, y: 9.0 - UIScreenPixel), size: prefixSize)
prefixView.alpha = 1.0
}
badgeWidth += prefixSize.width
badgeOffset.x += prefixSize.width - 6.0
} else if let prefixView = self.prefix.view {
prefixView.alpha = 0.0
}
if let subtitle = component.subtitle {
let subtitleSize = self.subtitle.update(
transition: .immediate,
component: AnyComponent(Text(text: subtitle, font: Font.regular(11.0), color: UIColor.white)),
environment: {},
containerSize: availableSize
)
if let subtitleView = self.subtitle.view {
if subtitleView.superview == nil {
self.badgeView.addSubview(subtitleView)
}
subtitleView.frame = CGRect(origin: CGPoint(x: 44.0, y: 28.0), size: subtitleSize)
subtitleView.alpha = 1.0
}
badgeOffset.y -= 6.0 + UIScreenPixel
} else if let subtitleView = self.subtitle.view {
subtitleView.alpha = 0.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.badgeView.bounds = CGRect(origin: .zero, size: badgeFullSize)
self.badgeView.center = CGPoint(x: badgeSize.width / 2.0, y: badgeSize.height)
self.badgeForeground.bounds = CGRect(origin: CGPoint(), size: CGSize(width: 600.0, height: badgeFullSize.height + 10.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)
self.badgeView.alpha = 1.0
let size = badgeSize
transition.setFrame(view: self.badgeLabel, frame: CGRect(origin: CGPoint(x: badgeOffset.x + 14.0 + floorToScreenPixels((badgeFullSize.width - badgeLabelSize.width) / 2.0), y: 5.0 + badgeOffset.y), size: badgeLabelSize))
if self.previousAvailableSize != availableSize || previousComponent?.color != component.color {
self.previousAvailableSize = availableSize
let activeColors: [UIColor] = [
component.color,
component.color
]
var locations: [CGFloat] = []
let delta = 1.0 / CGFloat(activeColors.count - 1)
for i in 0 ..< activeColors.count {
locations.append(delta * CGFloat(i))
}
let gradient = generateGradientImage(size: CGSize(width: 200.0, height: 60.0), colors: activeColors, locations: locations, direction: .horizontal)
self.badgeForeground.contentsGravity = .resizeAspectFill
self.badgeForeground.contents = gradient?.cgImage
self.setupGradientAnimations()
}
return size
}
func adjustTail(size: CGSize, tailOffset: CGFloat, transition: ComponentTransition) {
if self.badgeShapeView.image == nil {
self.badgeShapeView.image = generateStretchableFilledCircleImage(diameter: 48.0, color: UIColor.white)
}
self.badgeShapeView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: 48.0))
let badgeShapeSize = CGSize(width: 78, height: 60)
let _ = self.badgeShapeAnimation.update(
transition: .immediate,
component: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(name: "badge_with_tail"),
color: .white,
placeholderColor: nil,
startingPosition: .begin,
size: badgeShapeSize,
renderingScale: UIScreenScale,
loop: false,
playOnce: nil
)),
environment: {},
containerSize: badgeShapeSize
)
if let badgeShapeAnimationView = self.badgeShapeAnimation.view as? LottieComponent.View {
if badgeShapeAnimationView.superview == nil {
badgeShapeAnimationView.layer.anchorPoint = CGPoint()
self.badgeMaskView.addSubview(badgeShapeAnimationView)
}
var shapeFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: badgeShapeSize)
let badgeShapeWidth = badgeShapeSize.width
let midFrame = 359 / 2
if tailOffset < badgeShapeWidth * 0.5 {
let frameIndex = Int(floor(CGFloat(midFrame) * tailOffset / (badgeShapeWidth * 0.5)))
badgeShapeAnimationView.setFrameIndex(index: frameIndex)
} else if tailOffset >= size.width - badgeShapeWidth * 0.5 {
let endOffset = tailOffset - (size.width - badgeShapeWidth * 0.5)
let frameIndex = midFrame + Int(floor(CGFloat(359 - midFrame) * endOffset / (badgeShapeWidth * 0.5)))
badgeShapeAnimationView.setFrameIndex(index: frameIndex)
shapeFrame.origin.x = size.width - badgeShapeWidth
} else {
badgeShapeAnimationView.setFrameIndex(index: midFrame)
shapeFrame.origin.x = tailOffset - badgeShapeWidth * 0.5
}
badgeShapeAnimationView.center = shapeFrame.origin
badgeShapeAnimationView.bounds = CGRect(origin: CGPoint(), size: shapeFrame.size)
}
}
func updateBadgeAngle(angle: CGFloat) {
let transition: ContainedViewLayoutTransition = .immediate
transition.updateTransformRotation(view: self.badgeView, angle: angle)
}
private func setupGradientAnimations() {
guard let _ = self.component else {
return
}
if let _ = self.badgeForeground.animation(forKey: "movement") {
} else {
CATransaction.begin()
let badgePreviousValue = self.badgeForeground.position.x
let badgeNewValue: CGFloat
if self.badgeForeground.position.x == -300.0 {
badgeNewValue = 0.0
} else {
badgeNewValue = -300.0
}
self.badgeForeground.position = CGPoint(x: badgeNewValue, 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")
CATransaction.commit()
}
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private final class PeerPlaceComponent: Component {
let theme: PresentationTheme
let color: UIColor
let place: Int32
let groupingSeparator: String
init(
theme: PresentationTheme,
color: UIColor,
place: Int32,
groupingSeparator: String
) {
self.theme = theme
self.color = color
self.place = place
self.groupingSeparator = groupingSeparator
}
static func ==(lhs: PeerPlaceComponent, rhs: PeerPlaceComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.color != rhs.color {
return false
}
if lhs.place != rhs.place {
return false
}
if lhs.groupingSeparator != rhs.groupingSeparator {
return false
}
return true
}
final class View: UIView {
private var background = UIImageView()
private let label = ComponentView<Empty>()
private var component: PeerPlaceComponent?
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(self.background)
}
required init(coder: NSCoder) {
preconditionFailure()
}
func update(component: PeerPlaceComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
let textColor: UIColor
let backgroundColors: [UIColor]?
if component.place < 4 {
textColor = .white
switch component.place {
case 1:
backgroundColors = [UIColor(rgb: 0xffa901), UIColor(rgb: 0xffcd3b)]
case 2:
backgroundColors = [UIColor(rgb: 0x999999), UIColor(rgb: 0xbbbbbb)]
case 3:
backgroundColors = [UIColor(rgb: 0xcb692e), UIColor(rgb: 0xdc9a59)]
default:
backgroundColors = nil
}
} else {
textColor = component.color
backgroundColors = nil
}
let backgroundSize = CGSize(width: 24.0, height: 24.0)
if let backgroundColors {
let colors: NSArray = Array(backgroundColors.map { $0.cgColor }) as NSArray
self.background.image = generateGradientFilledCircleImage(
diameter: backgroundSize.width,
colors: colors,
direction: .vertical
)
self.background.isHidden = false
} else {
self.background.isHidden = true
}
let backgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - backgroundSize.width) * 0.5), y: floorToScreenPixels((availableSize.height - backgroundSize.height) * 0.5)), size: backgroundSize)
self.background.frame = backgroundFrame
let labelSize = self.label.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: presentationStringsFormattedNumber(component.place, component.groupingSeparator), font: Font.regular(17.0), textColor: textColor)))),
environment: {},
containerSize: CGSize(width: 40.0, height: 40.0)
)
let labelFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - labelSize.width) * 0.5), y: floorToScreenPixels((availableSize.height - labelSize.height) * 0.5)), size: labelSize)
if let labelView = self.label.view {
if labelView.superview == nil {
self.addSubview(labelView)
}
labelView.frame = labelFrame
}
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private final class PeerComponent: Component {
enum Status {
case winning
case outbid
}
let context: AccountContext
let theme: PresentationTheme
let groupingSeparator: String
let peer: EnginePeer
let place: Int32
let amount: Int64
let status: Status?
let isLast: Bool
init(
context: AccountContext,
theme: PresentationTheme,
groupingSeparator: String,
peer: EnginePeer,
place: Int32,
amount: Int64,
status: Status? = nil,
isLast: Bool
) {
self.context = context
self.theme = theme
self.groupingSeparator = groupingSeparator
self.peer = peer
self.place = place
self.amount = amount
self.status = status
self.isLast = isLast
}
static func ==(lhs: PeerComponent, rhs: PeerComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.peer != rhs.peer {
return false
}
if lhs.place != rhs.place {
return false
}
if lhs.amount != rhs.amount {
return false
}
if lhs.status != rhs.status {
return false
}
if lhs.isLast != rhs.isLast {
return false
}
return true
}
final class View: UIView {
private var avatarNode: AvatarNode?
private let place = ComponentView<Empty>()
private let title = ComponentView<Empty>()
private let amount = ComponentView<Empty>()
private let amountStar = UIImageView()
private let separator = SimpleLayer()
private var component: PeerComponent?
override init(frame: CGRect) {
super.init(frame: frame)
self.layer.addSublayer(self.separator)
self.amountStar.image = UIImage(bundleImageName: "Premium/Stars/StarSmall")
self.addSubview(self.amountStar)
}
required init(coder: NSCoder) {
preconditionFailure()
}
func update(component: PeerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
let size = CGSize(width: availableSize.width, height: 52.0)
var color = component.theme.list.itemSecondaryTextColor
switch component.status {
case .winning:
color = component.theme.list.itemDisclosureActions.constructive.fillColor
case .outbid:
color = component.theme.list.itemDestructiveColor
default:
break
}
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let placeSize = self.place.update(
transition: .immediate,
component: AnyComponent(PeerPlaceComponent(theme: component.theme, color: color, place: component.place, groupingSeparator: presentationData.dateTimeFormat.groupingSeparator)),
environment: {},
containerSize: CGSize(width: 40.0, height: 40.0)
)
let placeFrame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((size.height - placeSize.height) / 2.0)), size: placeSize)
if let placeView = self.place.view {
if placeView.superview == nil {
self.addSubview(placeView)
}
placeView.frame = placeFrame
}
let avatarNode: AvatarNode
if let current = self.avatarNode {
avatarNode = current
} else {
avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 16.0))
self.avatarNode = avatarNode
self.addSubview(avatarNode.view)
}
let avatarSize = CGSize(width: 40.0, height: 40.0)
let avatarFrame = CGRect(origin: CGPoint(x: 51.0, y: floorToScreenPixels((size.height - avatarSize.height) / 2.0)), size: avatarSize)
avatarNode.frame = avatarFrame
avatarNode.setPeer(context: component.context, theme: component.theme, peer: component.peer, synchronousLoad: true)
avatarNode.updateSize(size: avatarFrame.size)
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.peer.compactDisplayTitle, font: Font.regular(17.0), textColor: component.theme.list.itemPrimaryTextColor))
)),
environment: {},
containerSize: CGSize(width: availableSize.width - 120.0 - 110.0, height: 100.0)
)
let titleFrame = CGRect(origin: CGPoint(x: 110.0, y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
self.addSubview(titleView)
}
titleView.frame = titleFrame
}
let amountSize = self.amount.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: presentationStringsFormattedNumber(Int32(clamping: component.amount), component.groupingSeparator), font: Font.with(size: 15.0, traits: .monospacedNumbers), textColor: component.theme.list.itemSecondaryTextColor))
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 100.0)
)
let amountFrame = CGRect(origin: CGPoint(x: availableSize.width - amountSize.width, y: floorToScreenPixels((size.height - amountSize.height) / 2.0)), size: amountSize)
if let amountView = self.amount.view {
if amountView.superview == nil {
self.addSubview(amountView)
}
amountView.frame = amountFrame
}
if let icon = self.amountStar.image {
self.amountStar.frame = CGRect(origin: CGPoint(x: amountFrame.minX - icon.size.width, y: floorToScreenPixels((size.height - icon.size.height) / 2.0) - UIScreenPixel), size: icon.size)
}
self.separator.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor
self.separator.frame = CGRect(origin: CGPoint(x: 110.0, y: size.height), size: CGSize(width: size.width - 110.0, height: 1.0 - UIScreenPixel))
transition.setAlpha(layer: self.separator, alpha: component.isLast ? 0.0 : 1.0)
return size
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private final class SliderBackgroundComponent: Component {
let theme: PresentationTheme
let strings: PresentationStrings
let value: CGFloat
let topCutoff: CGFloat?
let giftsPerRound: Int32
let color: UIColor
init(
theme: PresentationTheme,
strings: PresentationStrings,
value: CGFloat,
topCutoff: CGFloat?,
giftsPerRound: Int32,
color: UIColor
) {
self.theme = theme
self.strings = strings
self.value = value
self.topCutoff = topCutoff
self.giftsPerRound = giftsPerRound
self.color = color
}
static func ==(lhs: SliderBackgroundComponent, rhs: SliderBackgroundComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.value != rhs.value {
return false
}
if lhs.topCutoff != rhs.topCutoff {
return false
}
if lhs.giftsPerRound != rhs.giftsPerRound {
return false
}
if lhs.color != rhs.color {
return false
}
return true
}
private enum TopTextOverflowState {
case left
case center
case right
func animates(from: TopTextOverflowState) -> Bool {
switch self {
case .left:
return false
case .center:
switch from {
case .left:
return false
case .center:
return false
case .right:
return true
}
case .right:
switch from {
case .left:
return false
case .center:
return true
case .right:
return false
}
}
}
}
final class View: UIView {
private let sliderBackground = UIView()
private let sliderForeground = UIView()
private let sliderStars = SliderStarsView()
private let topForegroundLine = SimpleLayer()
private let topBackgroundLine = SimpleLayer()
private let topForegroundText = ComponentView<Empty>()
private let topBackgroundText = ComponentView<Empty>()
private var topTextOverflowState: TopTextOverflowState?
override init(frame: CGRect) {
super.init(frame: frame)
self.sliderBackground.clipsToBounds = true
self.sliderForeground.clipsToBounds = true
self.sliderForeground.addSubview(self.sliderStars)
self.addSubview(self.sliderBackground)
self.addSubview(self.sliderForeground)
self.sliderBackground.layer.addSublayer(self.topBackgroundLine)
self.sliderForeground.layer.addSublayer(self.topForegroundLine)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: SliderBackgroundComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.sliderBackground.backgroundColor = component.theme.list.itemPrimaryTextColor.withMultipliedAlpha(component.theme.overallDarkAppearance ? 0.2 : 0.07)
self.sliderForeground.backgroundColor = component.color
self.topForegroundLine.backgroundColor = component.theme.list.plainBackgroundColor.cgColor
self.topBackgroundLine.backgroundColor = component.theme.list.plainBackgroundColor.cgColor
transition.setFrame(view: self.sliderBackground, frame: CGRect(origin: CGPoint(), size: availableSize))
let sliderMinWidth = availableSize.height
let sliderAreaWidth: CGFloat = availableSize.width - sliderMinWidth
let sliderForegroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: sliderMinWidth + floorToScreenPixels(sliderAreaWidth * component.value), height: availableSize.height))
transition.setFrame(view: self.sliderForeground, frame: sliderForegroundFrame)
self.sliderBackground.layer.cornerRadius = availableSize.height * 0.5
self.sliderForeground.layer.cornerRadius = availableSize.height * 0.5
self.sliderStars.frame = CGRect(origin: .zero, size: availableSize)
self.sliderStars.update(size: availableSize, value: component.value)
self.sliderForeground.isHidden = sliderForegroundFrame.width <= sliderMinWidth
let topCutoff = component.topCutoff ?? 0.0
let topX = floorToScreenPixels(sliderAreaWidth * topCutoff)
let topLineAvoidDistance = 6.0
let knobWidth: CGFloat = 30.0
let topLineClosestEdge = min(abs(sliderForegroundFrame.maxX - topX), abs(sliderForegroundFrame.maxX - knobWidth - topX))
var topLineOverlayFactor = topLineClosestEdge / topLineAvoidDistance
topLineOverlayFactor = max(0.0, min(1.0, topLineOverlayFactor))
if sliderForegroundFrame.maxX - knobWidth <= topX && sliderForegroundFrame.maxX >= topX {
topLineOverlayFactor = 0.0
}
let topLineHeight: CGFloat = availableSize.height
let topLineAlpha: CGFloat = topLineOverlayFactor * topLineOverlayFactor
let topLineFrameTransition = transition
let topLineAlphaTransition = transition
/*if transition.userData(GiftAuctionBidScreenComponent.IsAdjustingAmountHint.self) != nil {
topLineFrameTransition = .easeInOut(duration: 0.12)
topLineAlphaTransition = .easeInOut(duration: 0.12)
}*/
let topLineFrame = CGRect(origin: CGPoint(x: topX, y: (availableSize.height - topLineHeight) * 0.5), size: CGSize(width: 1.0, height: topLineHeight))
topLineFrameTransition.setFrame(layer: self.topForegroundLine, frame: topLineFrame)
topLineAlphaTransition.setAlpha(layer: self.topForegroundLine, alpha: topLineAlpha)
topLineFrameTransition.setFrame(layer: self.topBackgroundLine, frame: topLineFrame)
topLineAlphaTransition.setAlpha(layer: self.topBackgroundLine, alpha: topLineAlpha)
let topTextSize = self.topForegroundText.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.strings.Gift_AuctionBid_Top("\(component.giftsPerRound)").string, font: Font.semibold(15.0), textColor: UIColor(white: 1.0, alpha: 0.4)))
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 100.0)
)
let _ = self.topBackgroundText.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.strings.Gift_AuctionBid_Top("\(component.giftsPerRound)").string, font: Font.semibold(15.0), textColor: component.theme.overallDarkAppearance ? UIColor(white: 1.0, alpha: 0.22) : UIColor(white: 0.0, alpha: 0.2)))
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 100.0)
)
var topTextFrame = CGRect(origin: CGPoint(x: topLineFrame.maxX + 6.0, y: floor((availableSize.height - topTextSize.height) * 0.5)), size: topTextSize)
let topTextFrameTransition = transition
let topTextLeftInset: CGFloat = 4.0
var topTextOverflowWidth: CGFloat = 0.0
let topTextOverflowState: TopTextOverflowState
if sliderForegroundFrame.maxX < topTextFrame.minX - topTextLeftInset {
topTextOverflowState = .left
} else if sliderForegroundFrame.maxX >= topTextFrame.minX - topTextLeftInset && sliderForegroundFrame.maxX - knobWidth < topTextFrame.maxX + topTextLeftInset {
topTextOverflowWidth = sliderForegroundFrame.maxX - (topTextFrame.minX - topTextLeftInset)
topTextOverflowState = .center
} else {
topTextOverflowState = .right
}
topTextFrame.origin.x += topTextOverflowWidth
if let topForegroundTextView = self.topForegroundText.view, let topBackgroundTextView = self.topBackgroundText.view {
if topForegroundTextView.superview == nil {
topBackgroundTextView.layer.anchorPoint = CGPoint()
self.sliderBackground.addSubview(topBackgroundTextView)
topForegroundTextView.layer.anchorPoint = CGPoint()
self.sliderForeground.addSubview(topForegroundTextView)
}
var animateTopTextAdditionalX: CGFloat = 0.0
if transition.userData(GiftAuctionBidScreenComponent.IsAdjustingAmountHint.self) != nil {
if let previousState = self.topTextOverflowState, previousState != topTextOverflowState, topTextOverflowState.animates(from: previousState) {
animateTopTextAdditionalX = topForegroundTextView.center.x - topTextFrame.origin.x
}
}
topTextFrameTransition.setPosition(view: topForegroundTextView, position: topTextFrame.origin)
topTextFrameTransition.setPosition(view: topBackgroundTextView, position: topTextFrame.origin)
topForegroundTextView.bounds = CGRect(origin: CGPoint(), size: topTextFrame.size)
topBackgroundTextView.bounds = CGRect(origin: CGPoint(), size: topTextFrame.size)
if animateTopTextAdditionalX != 0.0 {
topForegroundTextView.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: animateTopTextAdditionalX, y: 0.0)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.3, damping: 100.0, additive: true)
topBackgroundTextView.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: animateTopTextAdditionalX, y: 0.0)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.3, damping: 100.0, additive: true)
}
topForegroundTextView.isHidden = component.topCutoff == nil || topLineFrame.maxX + topTextSize.width + 20.0 > availableSize.width
topBackgroundTextView.isHidden = topForegroundTextView.isHidden
self.topBackgroundLine.isHidden = topX < 10.0
self.topForegroundLine.isHidden = self.topBackgroundLine.isHidden
}
self.topTextOverflowState = topTextOverflowState
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private final class GiftAuctionBidScreenComponent: Component {
final class IsAdjustingAmountHint {
}
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let gift: StarGift
let auctionContext: GiftAuctionContext
init(
context: AccountContext,
gift: StarGift,
auctionContext: GiftAuctionContext
) {
self.context = context
self.gift = gift
self.auctionContext = auctionContext
}
static func ==(lhs: GiftAuctionBidScreenComponent, rhs: GiftAuctionBidScreenComponent) -> Bool {
return true
}
private struct ItemLayout: Equatable {
var containerSize: CGSize
var containerInset: CGFloat
var containerCornerRadius: CGFloat
var bottomInset: CGFloat
var topInset: CGFloat
init(containerSize: CGSize, containerInset: CGFloat, containerCornerRadius: CGFloat, bottomInset: CGFloat, topInset: CGFloat) {
self.containerSize = containerSize
self.containerInset = containerInset
self.containerCornerRadius = containerCornerRadius
self.bottomInset = bottomInset
self.topInset = topInset
}
}
private final class ScrollView: UIScrollView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return super.hitTest(point, with: event)
}
}
private struct Amount: Equatable {
private let sliderSteps: [Int]
private let minRealValue: Int
let minAllowedRealValue: Int
let maxRealValue: Int
let maxSliderValue: Int
private let isLogarithmic: Bool
private(set) var realValue: Int
private(set) var sliderValue: Int
private static func makeSliderSteps(minRealValue: Int, maxRealValue: Int, isLogarithmic: Bool) -> [Int] {
if isLogarithmic {
var sliderSteps: [Int] = [1, 10, 50, 100, 500, 1_000, 2_000, 5_000, 7_500, 10_000, 20_000, 30_000]
sliderSteps.removeAll(where: { $0 <= minRealValue })
sliderSteps.insert(minRealValue, at: 0)
sliderSteps.removeAll(where: { $0 >= maxRealValue })
sliderSteps.append(maxRealValue)
return sliderSteps
} else {
return [minRealValue, maxRealValue]
}
}
private static func remapValueToSlider(realValue: Int, minAllowedRealValue: Int, maxSliderValue: Int, steps: [Int]) -> Int {
guard realValue >= steps.first!, realValue <= steps.last! else { return 0 }
let realValue = max(minAllowedRealValue, realValue)
for i in 0 ..< steps.count - 1 {
if realValue >= steps[i] && realValue <= steps[i + 1] {
let range = steps[i + 1] - steps[i]
let relativeValue = realValue - steps[i]
let stepFraction = Float(relativeValue) / Float(range)
return Int(Float(i) * Float(maxSliderValue) / Float(steps.count - 1)) + Int(stepFraction * Float(maxSliderValue) / Float(steps.count - 1))
}
}
return maxSliderValue // Return max slider position if value equals the last step
}
private static func remapSliderToValue(sliderValue: Int, minAllowedRealValue: Int, maxSliderValue: Int, steps: [Int]) -> Int {
guard sliderValue >= 0, sliderValue <= maxSliderValue else { return steps.first! }
let stepIndex = Int(Float(sliderValue) / Float(maxSliderValue) * Float(steps.count - 1))
let fraction = Float(sliderValue) / Float(maxSliderValue) * Float(steps.count - 1) - Float(stepIndex)
if stepIndex >= steps.count - 1 {
return steps.last!
} else {
let range = steps[stepIndex + 1] - steps[stepIndex]
return max(minAllowedRealValue, steps[stepIndex] + Int(fraction * Float(range)))
}
}
init(realValue: Int, minRealValue: Int, minAllowedRealValue: Int, maxRealValue: Int, maxSliderValue: Int, isLogarithmic: Bool) {
self.sliderSteps = Amount.makeSliderSteps(minRealValue: minRealValue, maxRealValue: maxRealValue, isLogarithmic: isLogarithmic)
self.minRealValue = minRealValue
self.minAllowedRealValue = minAllowedRealValue
self.maxRealValue = maxRealValue
self.maxSliderValue = maxSliderValue
self.isLogarithmic = isLogarithmic
self.realValue = realValue
self.sliderValue = Amount.remapValueToSlider(realValue: self.realValue, minAllowedRealValue: self.minAllowedRealValue, maxSliderValue: self.maxSliderValue, steps: self.sliderSteps)
}
init(sliderValue: Int, minRealValue: Int, minAllowedRealValue: Int, maxRealValue: Int, maxSliderValue: Int, isLogarithmic: Bool) {
self.sliderSteps = Amount.makeSliderSteps(minRealValue: minRealValue, maxRealValue: maxRealValue, isLogarithmic: isLogarithmic)
self.minRealValue = minRealValue
self.minAllowedRealValue = minAllowedRealValue
self.maxRealValue = maxRealValue
self.maxSliderValue = maxSliderValue
self.isLogarithmic = isLogarithmic
self.sliderValue = sliderValue
self.realValue = Amount.remapSliderToValue(sliderValue: self.sliderValue, minAllowedRealValue: self.minAllowedRealValue, maxSliderValue: self.maxSliderValue, steps: self.sliderSteps)
}
func withRealValue(_ realValue: Int) -> Amount {
return Amount(realValue: realValue, minRealValue: self.minRealValue, minAllowedRealValue: self.minAllowedRealValue, maxRealValue: self.maxRealValue, maxSliderValue: self.maxSliderValue, isLogarithmic: self.isLogarithmic)
}
func withSliderValue(_ sliderValue: Int) -> Amount {
return Amount(sliderValue: sliderValue, minRealValue: self.minRealValue, minAllowedRealValue: self.minAllowedRealValue, maxRealValue: self.maxRealValue, maxSliderValue: self.maxSliderValue, isLogarithmic: self.isLogarithmic)
}
func withMinAllowedRealValue(_ minAllowedRealValue: Int) -> Amount {
return Amount(realValue: self.realValue, minRealValue: self.minRealValue, minAllowedRealValue: minAllowedRealValue, maxRealValue: self.maxRealValue, maxSliderValue: self.maxSliderValue, isLogarithmic: self.isLogarithmic)
}
}
final class View: UIView, UIScrollViewDelegate {
private let dimView: UIView
private let containerView: UIView
private let backgroundLayer: SimpleLayer
private let navigationBarContainer: SparseContainerView
private let scrollView: ScrollView
private let scrollContentClippingView: SparseContainerView
private let scrollContentView: UIView
private let hierarchyTrackingNode: HierarchyTrackingNode
private var balanceOverlay = ComponentView<Empty>()
private let backgroundHandleView: UIImageView
private let closeButton = ComponentView<Empty>()
private let moreButton = ComponentView<Empty>()
private let moreButtonPlayOnce = ActionSlot<Void>()
private let title = ComponentView<Empty>()
private let badgeStars = BadgeStarsView()
private let sliderBackground = ComponentView<Empty>()
private let slider = ComponentView<Empty>()
private let sliderPlus = ComponentView<Empty>()
private let badge = ComponentView<Empty>()
private var liveStreamPerks: [ComponentView<Empty>] = []
private var liveStreamMessagePreview: ComponentView<Empty>?
private var myPeerTitle: ComponentView<Empty>?
private var myPeerItem: ComponentView<Empty>?
private var topPeersTitle: ComponentView<Empty>?
private var topPeerItems: [EnginePeer.Id: ComponentView<Empty>] = [:]
private var giftAuctionState: GiftAuctionContext.State?
private var giftAuctionDisposable: Disposable?
private var giftAuctionTimer: SwiftSignalKit.Timer?
private var peersMap: [EnginePeer.Id: EnginePeer] = [:]
private let actionButton = ComponentView<Empty>()
private var ignoreScrolling: Bool = false
private var component: GiftAuctionBidScreenComponent?
private weak var state: EmptyComponentState?
private var isUpdating: Bool = false
private var environment: ViewControllerComponentContainer.Environment?
private var itemLayout: ItemLayout?
private var balance: StarsAmount?
private var amount: Amount = Amount(realValue: 1, minRealValue: 1, minAllowedRealValue: 1, maxRealValue: 1000, maxSliderValue: 1000, isLogarithmic: true)
private var didChangeAmount: Bool = false
private var cachedStarImage: (UIImage, PresentationTheme)?
private var balanceDisposable: Disposable?
private var badgePhysicsLink: SharedDisplayLinkDriver.Link?
override init(frame: CGRect) {
self.dimView = UIView()
self.containerView = UIView()
self.containerView.clipsToBounds = true
self.containerView.layer.cornerRadius = 40.0
self.containerView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
self.backgroundLayer = SimpleLayer()
self.backgroundLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
self.backgroundLayer.cornerRadius = 40.0
self.backgroundHandleView = UIImageView()
self.navigationBarContainer = SparseContainerView()
self.scrollView = ScrollView()
self.scrollContentClippingView = SparseContainerView()
self.scrollContentClippingView.clipsToBounds = true
self.scrollContentView = UIView()
self.hierarchyTrackingNode = HierarchyTrackingNode()
super.init(frame: frame)
self.addSubview(self.dimView)
self.addSubview(self.containerView)
self.containerView.layer.addSublayer(self.backgroundLayer)
self.scrollView.delaysContentTouches = true
self.scrollView.canCancelContentTouches = true
self.scrollView.clipsToBounds = false
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.scrollView.contentInsetAdjustmentBehavior = .never
}
if #available(iOS 13.0, *) {
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
}
self.scrollView.showsVerticalScrollIndicator = false
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.alwaysBounceHorizontal = false
self.scrollView.alwaysBounceVertical = true
self.scrollView.scrollsToTop = false
self.scrollView.delegate = self
self.scrollView.clipsToBounds = true
self.containerView.addSubview(self.scrollContentClippingView)
self.scrollContentClippingView.addSubview(self.scrollView)
self.scrollView.addSubview(self.scrollContentView)
self.containerView.addSubview(self.navigationBarContainer)
self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
self.containerView.addSubnode(self.hierarchyTrackingNode)
self.hierarchyTrackingNode.updated = { [weak self] value in
guard let self else {
return
}
if value {
if self.badgePhysicsLink == nil {
let badgePhysicsLink = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] _ in
guard let self else {
return
}
self.updateBadgePhysics()
})
self.badgePhysicsLink = badgePhysicsLink
}
} else {
if let badgePhysicsLink = self.badgePhysicsLink {
self.badgePhysicsLink = nil
badgePhysicsLink.invalidate()
}
}
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.balanceDisposable?.dispose()
self.giftAuctionDisposable?.dispose()
self.giftAuctionTimer?.invalidate()
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !self.ignoreScrolling {
self.updateScrolling(transition: .immediate)
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if !self.bounds.contains(point) {
return nil
}
if let balanceView = self.balanceOverlay.view, let result = balanceView.hitTest(self.convert(point, to: balanceView), with: event) {
return result
}
if !self.backgroundLayer.frame.contains(point) {
return self.dimView
}
if let result = self.navigationBarContainer.hitTest(self.convert(point, to: self.navigationBarContainer), with: event) {
return result
}
if let badgeView = self.badge.view, badgeView.hitTest(self.convert(point, to: badgeView), with: event) != nil {
if let sliderView = self.slider.view as? SliderComponent.View, let hitTestTarget = sliderView.hitTestTarget {
return hitTestTarget
}
}
let result = super.hitTest(point, with: event)
return result
}
@objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
guard let environment = self.environment, let controller = environment.controller() else {
return
}
controller.dismiss()
}
}
private func updateScrolling(transition: ComponentTransition) {
guard let itemLayout = self.itemLayout else {
return
}
var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset
topOffset = max(0.0, topOffset)
transition.setTransform(layer: self.backgroundLayer, transform: CATransform3DMakeTranslation(0.0, topOffset + itemLayout.containerInset, 0.0))
transition.setPosition(view: self.navigationBarContainer, position: CGPoint(x: 0.0, y: topOffset + itemLayout.containerInset))
var topOffsetFraction = self.scrollView.bounds.minY / 100.0
topOffsetFraction = max(0.0, min(1.0, topOffsetFraction))
let minScale: CGFloat = (itemLayout.containerSize.width - 6.0 * 2.0) / itemLayout.containerSize.width
let minScaledTranslation: CGFloat = (itemLayout.containerSize.height - itemLayout.containerSize.height * minScale) * 0.5 - 6.0
let minScaledCornerRadius: CGFloat = itemLayout.containerCornerRadius
let scale = minScale * (1.0 - topOffsetFraction) + 1.0 * topOffsetFraction
let scaledTranslation = minScaledTranslation * (1.0 - topOffsetFraction)
let scaledCornerRadius = minScaledCornerRadius * (1.0 - topOffsetFraction) + itemLayout.containerCornerRadius * topOffsetFraction
var containerTransform = CATransform3DIdentity
containerTransform = CATransform3DTranslate(containerTransform, 0.0, scaledTranslation, 0.0)
containerTransform = CATransform3DScale(containerTransform, scale, scale, scale)
transition.setTransform(view: self.containerView, transform: containerTransform)
transition.setCornerRadius(layer: self.containerView.layer, cornerRadius: scaledCornerRadius)
}
func animateIn() {
self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY
self.scrollContentClippingView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.backgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.navigationBarContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
if let actionButtonView = self.actionButton.view {
actionButtonView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
}
}
func animateOut(completion: @escaping () -> Void) {
let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY
self.dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
self.scrollContentClippingView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in
completion()
})
self.backgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
self.navigationBarContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
if let actionButtonView = self.actionButton.view {
actionButtonView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
}
if let view = self.balanceOverlay.view {
view.layer.animateScale(from: 1.0, to: 0.8, duration: 0.4, removeOnCompletion: false)
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
}
}
private var previousSliderValue: Float = 0.0
private var previousTimestamp: Double?
private var badgeAngularSpeed: CGFloat = 0.0
private var badgeAngle: CGFloat = 0.0
private var previousBadgeX: CGFloat?
private var previousPhysicsTimestamp: Double?
private func updateBadgePhysics() {
let timestamp = CACurrentMediaTime()
let deltaTime: CGFloat
if let previousPhysicsTimestamp = self.previousPhysicsTimestamp {
deltaTime = CGFloat(min(1.0 / 60.0, timestamp - previousPhysicsTimestamp))
} else {
deltaTime = CGFloat(1.0 / 60.0)
}
self.previousPhysicsTimestamp = timestamp
guard let badgeView = self.badge.view as? BadgeComponent.View else {
return
}
let badgeX = badgeView.center.x
let horizontalVelocity: CGFloat
if let previousBadgeX = self.previousBadgeX {
horizontalVelocity = (badgeX - previousBadgeX) / deltaTime
} else {
horizontalVelocity = 0.0
}
self.previousBadgeX = badgeX
let testSpringFriction: CGFloat = 9.0
let testSpringConstant: CGFloat = 243.0
let frictionConstant: CGFloat = testSpringFriction
let springConstant: CGFloat = testSpringConstant
let time: CGFloat = deltaTime
var badgeAngle = self.badgeAngle
badgeAngle -= horizontalVelocity * 0.0001
if abs(badgeAngle) > 0.22 {
badgeAngle = badgeAngle < 0.0 ? -0.22 : 0.22
}
// friction force = velocity * friction constant
let frictionForce = self.badgeAngularSpeed * frictionConstant
// spring force = (target point - current position) * spring constant
let springForce = -badgeAngle * springConstant
// force = spring force - friction force
let force = springForce - frictionForce
// velocity = current velocity + force * time / mass
self.badgeAngularSpeed = self.badgeAngularSpeed + force * time
// position = current position + velocity * time
badgeAngle = badgeAngle + self.badgeAngularSpeed * time
badgeAngle = badgeAngle.isNaN ? 0.0 : badgeAngle
let epsilon: CGFloat = 0.01
if abs(badgeAngle) < epsilon && abs(self.badgeAngularSpeed) < epsilon {
badgeAngle = 0.0
self.badgeAngularSpeed = 0.0
}
if abs(badgeAngle) > 0.22 {
badgeAngle = badgeAngle < 0.0 ? -0.22 : 0.22
}
if self.badgeAngle != badgeAngle {
self.badgeAngle = badgeAngle
badgeView.updateBadgeAngle(angle: self.badgeAngle)
}
}
private var isLoading = false
private func placeBid() {
guard let component = self.component, case let .generic(gift) = component.gift, let controller = self.environment?.controller() else {
return
}
var isUpdate = false
let value = Int64(self.amount.realValue)
if let myBidAmount = self.giftAuctionState?.myState.bidAmount {
isUpdate = true
if value == myBidAmount {
controller.dismiss()
return
}
}
let giftsPerRounds = gift.auctionGiftsPerRound ?? 50
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
if let myBidAmount = self.giftAuctionState?.myState.bidAmount, let myMinBidAmount = self.giftAuctionState?.myState.minBidAmount, value < myMinBidAmount {
HapticFeedback().error()
controller.present(
UndoOverlayController(
presentationData: presentationData,
content: .info(
title: nil,
text: presentationData.strings.Gift_AuctionBid_AddMoreStars(presentationData.strings.Gift_AuctionBid_AddMoreStars_Stars(Int32(clamping: myMinBidAmount - myBidAmount))).string,
timeout: nil,
customUndoText: nil
),
position: .bottom,
action: { _ in return true }
),
in: .current
)
return
}
self.isLoading = true
self.state?.updated()
let source: BotPaymentInvoiceSource
if let state = self.giftAuctionState, state.myState.bidAmount != nil {
source = .starGiftAuctionUpdateBid(
giftId: gift.id,
bidAmount: value
)
} else {
source = .starGiftAuctionBid(
hideName: false,
peerId: component.context.account.peerId,
giftId: gift.id,
bidAmount: value,
text: nil,
entities: nil
)
}
let signal = BotCheckoutController.InputData.fetch(context: component.context, source: source)
|> `catch` { error -> Signal<BotCheckoutController.InputData, SendBotPaymentFormError> in
switch error {
case .disallowedStarGifts:
return .fail(.disallowedStarGift)
case .starGiftsUserLimit:
return .fail(.starGiftUserLimit)
default:
return .fail(.generic)
}
}
|> mapToSignal { inputData -> Signal<SendBotPaymentResult, SendBotPaymentFormError> in
return component.context.engine.payments.sendStarsPaymentForm(formId: inputData.form.id, source: source)
}
|> deliverOnMainQueue
let _ = signal.start(next: { [weak self, weak controller] result in
guard let self, let component = self.component else {
return
}
self.isLoading = false
self.state?.updated()
self.amount = self.amount.withMinAllowedRealValue(Int(value))
let title = isUpdate ? presentationData.strings.Gift_AuctionBid_Increased_Title : presentationData.strings.Gift_AuctionBid_Placed_Title
let text = isUpdate ? presentationData.strings.Gift_AuctionBid_Increased_Text("\(giftsPerRounds)").string : presentationData.strings.Gift_AuctionBid_Placed_Text("\(giftsPerRounds)").string
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
controller?.present(
UndoOverlayController(
presentationData: presentationData,
content: .actionSucceeded(
title: title,
text: text,
cancel: nil,
destructive: false
),
position: .bottom,
action: { _ in return true }
),
in: .current
)
component.context.starsContext?.load(force: true)
})
}
private func openPeer(_ peer: EnginePeer, dismiss: Bool = true) {
guard let component = self.component, let controller = self.environment?.controller() as? GiftAuctionBidScreen, let navigationController = controller.navigationController as? NavigationController else {
return
}
let context = component.context
let action = {
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(
navigationController: navigationController,
chatController: nil,
context: context,
chatLocation: .peer(peer),
subject: nil,
botStart: nil,
updateTextInputState: nil,
keepStack: .always,
useExisting: true,
purposefulAction: nil,
scrollToEndIfExists: false,
activateMessageSearch: nil,
animated: true
))
}
if dismiss {
controller.dismiss()
Queue.mainQueue().after(0.4, {
action()
})
} else {
action()
}
}
func share() {
guard let component = self.component, let controller = self.environment?.controller() else {
return
}
let context = component.context
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var link = ""
if case let .generic(gift) = component.auctionContext.gift, let slug = gift.auctionSlug {
link = "https://t.me/auction/\(slug)"
}
let shareController = context.sharedContext.makeShareController(
context: context,
subject: .url(link),
forceExternal: false,
shareStory: nil,
enqueued: { [weak self, weak controller] peerIds, _ in
guard let self else {
return
}
let _ = (context.engine.data.get(
EngineDataList(
peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init)
)
)
|> deliverOnMainQueue).startStandalone(next: { [weak self, weak controller] peerList in
guard let self else {
return
}
let peers = peerList.compactMap { $0 }
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let text: String
var savedMessages = false
if peerIds.count == 1, let peerId = peerIds.first, peerId == context.account.peerId {
text = presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One
savedMessages = true
} else {
if peers.count == 1, let peer = peers.first {
var peerName = peer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
peerName = peerName.replacingOccurrences(of: "**", with: "")
text = presentationData.strings.Conversation_ForwardTooltip_Chat_One(peerName).string
} else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last {
var firstPeerName = firstPeer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
firstPeerName = firstPeerName.replacingOccurrences(of: "**", with: "")
var secondPeerName = secondPeer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
secondPeerName = secondPeerName.replacingOccurrences(of: "**", with: "")
text = presentationData.strings.Conversation_ForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string
} else if let peer = peers.first {
var peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
peerName = peerName.replacingOccurrences(of: "**", with: "")
text = presentationData.strings.Conversation_ForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string
} else {
text = ""
}
}
controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: false, action: { [weak self, weak controller] action in
if let self, savedMessages, action == .info {
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> deliverOnMainQueue).start(next: { [weak self, weak controller] peer in
guard let peer else {
return
}
self?.openPeer(peer)
Queue.mainQueue().after(0.6) {
controller?.dismiss(animated: false, completion: nil)
}
})
}
return false
}, additionalView: nil), in: .current)
})
},
actionCompleted: { [weak controller] in
controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current)
}
)
controller.present(shareController, in: .window(.root))
}
func morePressed(view: UIView, gesture: ContextGesture?) {
guard let component = self.component, let controller = self.environment?.controller() else {
return
}
let context = component.context
let gift = component.auctionContext.gift
let auctionContext = component.auctionContext
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var link = ""
if case let .generic(gift) = gift, let slug = gift.auctionSlug {
link = "https://t.me/auction/\(slug)"
}
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_Auction_Context_About, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Info"), color: theme.contextMenu.primaryColor) }, action: { [weak controller] c, f in
f(.default)
let infoController = context.sharedContext.makeGiftAuctionInfoScreen(context: context, auctionContext: auctionContext, completion: nil)
controller?.push(infoController)
})))
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_Auction_Context_CopyLink, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor) }, action: { [weak controller] c, f in
f(.default)
UIPasteboard.general.string = link
controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current)
})))
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_Auction_Context_Share, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, f in
f(.default)
self?.share()
})))
let contextController = ContextController(presentationData: presentationData, source: .reference(GiftViewContextReferenceContentSource(controller: controller, sourceView: view)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
controller.presentInGlobalOverlay(contextController)
}
func update(component: GiftAuctionBidScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
let environment = environment[ViewControllerComponentContainer.Environment.self].value
let themeUpdated = self.environment?.theme !== environment.theme
let resetScrolling = self.scrollView.bounds.width != availableSize.width
let fillingSize: CGFloat
if case .regular = environment.metrics.widthClass {
fillingSize = min(availableSize.width, 414.0) - environment.safeInsets.left * 2.0
} else {
fillingSize = min(availableSize.width, 428.0) - environment.safeInsets.left * 2.0
}
let sideInset: CGFloat = floor((availableSize.width - fillingSize) * 0.5) + 24.0
let context = component.context
let balanceSize = self.balanceOverlay.update(
transition: .immediate,
component: AnyComponent(
StarsBalanceOverlayComponent(
context: component.context,
peerId: component.context.account.peerId,
theme: environment.theme,
currency: .stars,
action: { [weak self] in
guard let self, let starsContext = context.starsContext, let navigationController = self.environment?.controller()?.navigationController as? NavigationController else {
return
}
self.environment?.controller()?.dismiss()
let _ = (context.engine.payments.starsTopUpOptions()
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { options in
let controller = context.sharedContext.makeStarsPurchaseScreen(
context: context,
starsContext: starsContext,
options: options,
purpose: .generic,
targetPeerId: nil,
customTheme: environment.theme,
completion: { _ in }
)
navigationController.pushViewController(controller)
})
}
)
),
environment: {},
containerSize: availableSize
)
if let view = self.balanceOverlay.view {
if view.superview == nil {
self.addSubview(view)
view.layer.animatePosition(from: CGPoint(x: 0.0, y: -64.0), to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
view.layer.animateSpring(from: 0.8 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5, initialVelocity: 0.0, removeOnCompletion: true, additive: false, completion: nil)
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
view.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - balanceSize.width) / 2.0), y: environment.statusBarHeight + 5.0), size: balanceSize)
}
if self.component == nil {
if let starsContext = component.context.starsContext {
self.balanceDisposable = (starsContext.state
|> deliverOnMainQueue).startStrict(next: { [weak self] state in
guard let self else {
return
}
if let state {
if self.balance != state.balance {
self.balance = state.balance
self.state?.updated(transition: .immediate)
}
}
})
}
let context = component.context
self.giftAuctionDisposable = (component.auctionContext.state
|> deliverOnMainQueue).start(next: { [weak self] state in
guard let self else {
return
}
let isFirstTime = self.giftAuctionState == nil
self.giftAuctionState = state
var peerIds: [EnginePeer.Id] = []
if case let .ongoing(_, _, _, _, _, topBidders, _, _, _, _) = state?.auctionState {
for bidder in topBidders {
if self.peersMap[bidder] == nil {
peerIds.append(bidder)
}
}
}
var transition = ComponentTransition.spring(duration: 0.4)
if isFirstTime {
peerIds.append(context.account.peerId)
var minBidAmount: Int64 = 100
if case let .ongoing(_, _, _, auctionMinBidAmount, _, _, _, _, _, _) = state?.auctionState {
minBidAmount = auctionMinBidAmount
}
var currentValue = max(Int(minBidAmount), 100)
if let myBidAmount = state?.myState.bidAmount {
currentValue = Int(myBidAmount)
}
var minAllowedRealValue: Int64 = minBidAmount
if let myBidAmount = state?.myState.bidAmount {
minAllowedRealValue = myBidAmount
}
self.amount = Amount(realValue: currentValue, minRealValue: Int(minBidAmount), minAllowedRealValue: Int(minAllowedRealValue), maxRealValue: 30000, maxSliderValue: 999, isLogarithmic: true)
transition = .immediate
}
if !peerIds.isEmpty {
let _ = (context.engine.data.get(EngineDataMap(
peerIds.map { peerId -> TelegramEngine.EngineData.Item.Peer.Peer in
return TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)
}
))
|> deliverOnMainQueue).startStandalone(next: { [weak self] peers in
guard let self else {
return
}
var peersMap: [EnginePeer.Id: EnginePeer] = self.peersMap
for (peerId, maybePeer) in peers {
if let peer = maybePeer {
peersMap[peerId] = peer
}
}
self.peersMap = peersMap
self.state?.updated(transition: transition)
})
}
self.state?.updated(transition: transition)
})
self.giftAuctionTimer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in
self?.state?.updated()
}, queue: Queue.mainQueue())
self.giftAuctionTimer?.start()
}
self.component = component
self.state = state
self.environment = environment
if themeUpdated {
self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
self.backgroundLayer.backgroundColor = environment.theme.actionSheet.opaqueItemBackgroundColor.cgColor
}
transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize))
var contentHeight: CGFloat = 0.0
let sliderInset: CGFloat = sideInset + 8.0
let sliderSize = self.slider.update(
transition: transition,
component: AnyComponent(SliderComponent(
content: .discrete(SliderComponent.Discrete(
valueCount: self.amount.maxSliderValue + 1,
value: self.amount.sliderValue,
markPositions: false,
valueUpdated: { [weak self] value in
guard let self else {
return
}
let maxAmount: Int = 1000
self.amount = self.amount.withSliderValue(value)
self.didChangeAmount = true
self.state?.updated(transition: ComponentTransition(animation: .none).withUserData(IsAdjustingAmountHint()))
let sliderValue = Float(value) / Float(maxAmount)
let currentTimestamp = CACurrentMediaTime()
if let previousTimestamp {
let deltaTime = currentTimestamp - previousTimestamp
let delta = sliderValue - self.previousSliderValue
let deltaValue = abs(sliderValue - self.previousSliderValue)
let speed = deltaValue / Float(deltaTime)
let newSpeed = max(0, min(65.0, speed * 70.0))
if newSpeed < 0.01 && deltaValue < 0.001 {
} else {
self.badgeStars.update(speed: newSpeed, delta: delta)
}
}
self.previousSliderValue = sliderValue
self.previousTimestamp = currentTimestamp
}
)),
trackBackgroundColor: .clear,
trackForegroundColor: .clear,
knobSize: 26.0,
knobColor: .white,
isTrackingUpdated: { [weak self] isTracking in
guard let self else {
return
}
if !isTracking {
self.previousTimestamp = nil
self.badgeStars.update(speed: 0.0)
}
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sliderInset * 2.0, height: 30.0)
)
let sliderPlusSize = self.sliderPlus.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: "+", font: Font.with(size: 26.0, design: .round, weight: .regular), textColor: environment.theme.list.itemSecondaryTextColor.withAlphaComponent(0.5))))
),
environment: {},
containerSize: availableSize
)
contentHeight += 148.0
let sliderFrame = CGRect(origin: CGPoint(x: sliderInset, y: contentHeight), size: sliderSize)
let sliderBackgroundFrame = CGRect(origin: CGPoint(x: sliderFrame.minX - 8.0, y: sliderFrame.minY + 7.0), size: CGSize(width: sliderFrame.width + 16.0, height: sliderFrame.height - 14.0))
let sliderPlusFrame = CGRect(origin: CGPoint(x: sliderBackgroundFrame.maxX - sliderPlusSize.width - 6.0, y: sliderBackgroundFrame.minY - 3.0 + UIScreenPixel), size: sliderPlusSize)
let progressFraction: CGFloat = CGFloat(self.amount.sliderValue) / CGFloat(self.amount.maxSliderValue)
var sliderColor: UIColor = UIColor(rgb: 0xFFB10D)
let liveStreamParams = LiveChatMessageParams(appConfig: component.context.currentAppConfiguration.with({ $0 }))
let color = GroupCallMessagesContext.getStarAmountParamMapping(params: liveStreamParams, value: Int64(self.amount.realValue)).color ?? GroupCallMessagesContext.Message.Color(rawValue: 0x985FDC)
sliderColor = StoryLiveChatMessageComponent.getMessageColor(color: color)
var giftsPerRound: Int32 = 50
if case let .generic(gift) = self.giftAuctionState?.gift, let giftsPerRoundValue = gift.auctionGiftsPerRound {
giftsPerRound = giftsPerRoundValue
}
var topCutoff: CGFloat?
if let giftAuctionState = self.giftAuctionState, case let .ongoing(_, _, _, _, bidLevels, _, _, _, _, _) = giftAuctionState.auctionState {
for bidLevel in bidLevels {
if bidLevel.position == giftsPerRound - 1 {
topCutoff = CGFloat(bidLevel.amount) / CGFloat(self.amount.maxRealValue)
break
}
}
}
let _ = self.sliderBackground.update(
transition: transition,
component: AnyComponent(SliderBackgroundComponent(
theme: environment.theme,
strings: environment.strings,
value: progressFraction,
topCutoff: topCutoff,
giftsPerRound: giftsPerRound,
color: sliderColor
)),
environment: {},
containerSize: sliderBackgroundFrame.size
)
if let sliderView = self.slider.view, let sliderBackgroundView = self.sliderBackground.view, let sliderPlusView = self.sliderPlus.view {
if sliderView.superview == nil {
self.scrollContentView.addSubview(self.badgeStars)
self.scrollContentView.addSubview(sliderBackgroundView)
self.scrollContentView.addSubview(sliderView)
self.scrollContentView.addSubview(sliderPlusView)
sliderPlusView.isUserInteractionEnabled = false
}
transition.setFrame(view: sliderView, frame: sliderFrame)
transition.setFrame(view: sliderBackgroundView, frame: sliderBackgroundFrame)
transition.setFrame(view: sliderPlusView, frame: sliderPlusFrame)
var subtitle: String?
let badgeValue: String = "\(self.amount.realValue)"
var subtitleOnTop = false
// if self.amount.sliderValue == self.amount.maxSliderValue {
// badgeValue = "Custom"
// } else
if let myBidAmount = self.giftAuctionState?.myState.bidAmount {
if self.amount.realValue > myBidAmount {
subtitle = "+\(self.amount.realValue - Int(myBidAmount))"
subtitleOnTop = true
} else if myBidAmount == self.amount.realValue {
subtitle = environment.strings.Gift_AuctionBid_YourBid
}
}
let badgeSize = self.badge.update(
transition: transition,
component: AnyComponent(BadgeComponent(
theme: environment.theme,
prefix: nil,
title: badgeValue,
subtitle: subtitle,
subtitleOnTop: subtitleOnTop,
color: sliderColor
)),
environment: {},
containerSize: CGSize(width: 200.0, height: 200.0)
)
let sliderMinWidth = sliderBackgroundFrame.height
let sliderAreaWidth: CGFloat = sliderBackgroundFrame.width - sliderMinWidth
let sliderForegroundFrame = CGRect(origin: sliderBackgroundFrame.origin, size: CGSize(width: sliderMinWidth + floorToScreenPixels(sliderAreaWidth * progressFraction), height: sliderBackgroundFrame.height))
var badgeFrame = CGRect()
if let badgeView = self.badge.view as? BadgeComponent.View {
if badgeView.superview == nil {
self.scrollContentView.insertSubview(badgeView, belowSubview: self.badgeStars)
}
let apparentBadgeSize = badgeSize
let badgeOriginX = sliderBackgroundFrame.minX + sliderForegroundFrame.width - 15.0
badgeFrame = CGRect(origin: CGPoint(x: badgeOriginX - apparentBadgeSize.width * 0.5, y: sliderForegroundFrame.minY - 9.0 - badgeSize.height), size: apparentBadgeSize)
let badgeSideInset: CGFloat = 23.0
let badgeOverflowWidth: CGFloat
if badgeFrame.minX < badgeSideInset {
badgeOverflowWidth = badgeSideInset - badgeFrame.minX
} else if badgeFrame.minX + badgeFrame.width > availableSize.width - badgeSideInset {
badgeOverflowWidth = availableSize.width - badgeSideInset - badgeFrame.width - badgeFrame.minX
} else {
badgeOverflowWidth = 0.0
}
badgeFrame.origin.x += badgeOverflowWidth
let badgeTailOffset = badgeOriginX - badgeFrame.minX
let badgePosition = CGPoint(x: badgeFrame.minX + badgeTailOffset, y: badgeFrame.maxY)
badgeView.center = badgePosition
badgeView.bounds = CGRect(origin: CGPoint(), size: badgeFrame.size)
transition.setAnchorPoint(layer: badgeView.layer, anchorPoint: CGPoint(x: max(0.0, min(1.0, badgeTailOffset / badgeFrame.width)), y: 1.0))
badgeView.adjustTail(size: apparentBadgeSize, tailOffset: badgeTailOffset, transition: transition)
}
let starsRect = CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: sliderForegroundFrame.midY))
self.badgeStars.frame = starsRect
self.badgeStars.update(size: starsRect.size, color: sliderColor, emitterPosition: CGPoint(x: badgeFrame.minX, y: badgeFrame.midY - 64.0))
}
var perks: [([AnimatedTextComponent.Item], String)] = []
var minBidAnimatedItems: [AnimatedTextComponent.Item] = []
var untilNextDropAnimatedItems: [AnimatedTextComponent.Item] = []
var dropsLeftAnimatedItems: [AnimatedTextComponent.Item] = []
if let auctionState = self.giftAuctionState?.auctionState {
if case let .ongoing(_, _, _, minBidAmount, _, _, nextDropDate, dropsLeft, _, _) = auctionState {
var minBidAmount = minBidAmount
if let myMinBidAmmount = self.giftAuctionState?.myState.minBidAmount {
minBidAmount = myMinBidAmmount
}
let minBidString = "# \(presentationStringsFormattedNumber(Int32(clamping: minBidAmount), environment.dateTimeFormat.groupingSeparator))"
if let hashIndex = minBidString.firstIndex(of: "#") {
var prefix = String(minBidString[..<hashIndex])
if !prefix.isEmpty {
prefix.removeLast()
minBidAnimatedItems.append(
AnimatedTextComponent.Item(
id: AnyHashable(minBidAnimatedItems.count),
content: .text(prefix)
)
)
}
minBidAnimatedItems.append(
AnimatedTextComponent.Item(
id: AnyHashable(minBidAnimatedItems.count),
content: .icon("Premium/Stars/StarMedium", tint: false, offset: CGPoint(x: 1.0, y: 2.0 - UIScreenPixel))
)
)
let suffixStart = minBidString.index(after: hashIndex)
let suffix = minBidString[suffixStart...]
var i = suffix.startIndex
while i < suffix.endIndex {
if suffix[i].isNumber {
var j = i
while j < suffix.endIndex, suffix[j].isNumber {
j = suffix.index(after: j)
}
let string = suffix[i..<j]
if let value = Int(string) {
minBidAnimatedItems.append(
AnimatedTextComponent.Item(
id: AnyHashable(minBidAnimatedItems.count),
content: .number(value, minDigits: string.count)
)
)
}
i = j
} else {
var j = i
while j < suffix.endIndex, !suffix[j].isNumber {
j = suffix.index(after: j)
}
let textRun = String(suffix[i..<j])
if !textRun.isEmpty {
minBidAnimatedItems.append(
AnimatedTextComponent.Item(
id: AnyHashable(minBidAnimatedItems.count),
content: .text(textRun)
)
)
}
i = j
}
}
} else {
minBidAnimatedItems.append(AnimatedTextComponent.Item(id: "static", content: .text(minBidString)))
}
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
let dropTimeout = max(0, nextDropDate - currentTime)
let minutes = Int(dropTimeout / 60)
let seconds = Int(dropTimeout % 60)
untilNextDropAnimatedItems.append(AnimatedTextComponent.Item(id: "m", content: .number(minutes, minDigits: 2)))
untilNextDropAnimatedItems.append(AnimatedTextComponent.Item(id: "colon", content: .text(":")))
untilNextDropAnimatedItems.append(AnimatedTextComponent.Item(id: "s", content: .number(seconds, minDigits: 2)))
dropsLeftAnimatedItems = [AnimatedTextComponent.Item(id: "drops", content: .number(Int(dropsLeft), minDigits: 1))]
}
}
perks.append((
minBidAnimatedItems,
environment.strings.Gift_AuctionBid_MinimumBid
))
perks.append((
untilNextDropAnimatedItems,
environment.strings.Gift_AuctionBid_UntilNext
))
perks.append((
dropsLeftAnimatedItems,
environment.strings.Gift_AuctionBid_Left
))
contentHeight += 54.0
let perkHeight: CGFloat = 60.0
let perkSpacing: CGFloat = 10.0
let perkWidth: CGFloat = floor((fillingSize - sideInset * 2.0 - perkSpacing * CGFloat(perks.count - 1)) / CGFloat(perks.count))
for i in 0 ..< perks.count {
var perkFrame = CGRect(origin: CGPoint(x: sideInset + CGFloat(i) * (perkWidth + perkSpacing), y: contentHeight), size: CGSize(width: perkWidth, height: perkHeight))
if i == perks.count - 1 {
perkFrame.size.width = max(0.0, availableSize.width - sideInset - perkFrame.minX)
}
let perkView: ComponentView<Empty>
if self.liveStreamPerks.count > i {
perkView = self.liveStreamPerks[i]
} else {
perkView = ComponentView()
self.liveStreamPerks.append(perkView)
}
let perk = perks[i]
let _ = perkView.update(
transition: transition,
component: AnyComponent(AuctionStatComponent(
context: component.context,
gift: i == perks.count - 1 ? component.auctionContext.gift : nil,
title: perk.0,
subtitle: perk.1,
theme: environment.theme
)),
environment: {},
containerSize: perkFrame.size
)
if let perkComponentView = perkView.view {
if perkComponentView.superview == nil {
self.scrollContentView.addSubview(perkComponentView)
}
transition.setFrame(view: perkComponentView, frame: perkFrame)
}
}
contentHeight += perkHeight
contentHeight += 24.0
if self.backgroundHandleView.image == nil {
self.backgroundHandleView.image = generateStretchableFilledCircleImage(diameter: 5.0, color: .white)?.withRenderingMode(.alwaysTemplate)
}
self.backgroundHandleView.tintColor = environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(environment.theme.overallDarkAppearance ? 0.2 : 0.07)
let backgroundHandleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - 36.0) * 0.5), y: 5.0), size: CGSize(width: 36.0, height: 5.0))
if self.backgroundHandleView.superview == nil {
self.navigationBarContainer.addSubview(self.backgroundHandleView)
}
transition.setFrame(view: self.backgroundHandleView, frame: backgroundHandleFrame)
let closeButtonSize = self.closeButton.update(
transition: .immediate,
component: AnyComponent(GlassBarButtonComponent(
size: CGSize(width: 40.0, height: 40.0),
backgroundColor: environment.theme.rootController.navigationBar.glassBarButtonBackgroundColor,
isDark: environment.theme.overallDarkAppearance,
state: .generic,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: environment.theme.rootController.navigationBar.glassBarButtonForegroundColor
)
)),
action: { [weak self] _ in
guard let self else {
return
}
self.environment?.controller()?.dismiss()
}
)),
environment: {},
containerSize: CGSize(width: 40.0, height: 40.0)
)
let closeButtonFrame = CGRect(origin: CGPoint(x: 16.0, y: 16.0), size: closeButtonSize)
if let closeButtonView = self.closeButton.view {
if closeButtonView.superview == nil {
self.navigationBarContainer.addSubview(closeButtonView)
}
transition.setFrame(view: closeButtonView, frame: closeButtonFrame)
}
let moreButtonSize = self.moreButton.update(
transition: .immediate,
component: AnyComponent(GlassBarButtonComponent(
size: CGSize(width: 40.0, height: 40.0),
backgroundColor: environment.theme.rootController.navigationBar.glassBarButtonBackgroundColor,
isDark: environment.theme.overallDarkAppearance,
state: .generic,
component: AnyComponentWithIdentity(id: "info", component: AnyComponent(
LottieComponent(
content: LottieComponent.AppBundleContent(
name: "anim_morewide"
),
color: environment.theme.rootController.navigationBar.glassBarButtonForegroundColor,
size: CGSize(width: 34.0, height: 34.0),
playOnce: self.moreButtonPlayOnce
)
)),
action: { [weak self] view in
guard let self else {
return
}
self.morePressed(view: view, gesture: nil)
self.moreButtonPlayOnce.invoke(Void())
}
)),
environment: {},
containerSize: CGSize(width: 40.0, height: 40.0)
)
let infoButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - 16.0 - moreButtonSize.width, y: 16.0), size: moreButtonSize)
if let infoButtonView = self.moreButton.view {
if infoButtonView.superview == nil {
self.navigationBarContainer.addSubview(infoButtonView)
}
transition.setFrame(view: infoButtonView, frame: infoButtonFrame)
}
let containerInset: CGFloat = environment.statusBarHeight + 10.0
var initialContentHeight = contentHeight
let clippingY: CGFloat
let title = self.title
let actionButton = self.actionButton
let titleSize = title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: environment.strings.Gift_AuctionBid_Title, font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor))
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
)
let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: 26.0), size: titleSize)
if let titleView = title.view {
if titleView.superview == nil {
self.navigationBarContainer.addSubview(titleView)
}
transition.setFrame(view: titleView, frame: titleFrame)
}
var myBidTitleComponent: AnyComponent<Empty>?
var myBidComponent: AnyComponent<Empty>?
var topBidsTitleComponent: AnyComponent<Empty>?
var topBidsComponents: [(EnginePeer.Id, AnyComponent<Empty>)] = []
if let giftAuctionState = self.giftAuctionState, case let .ongoing(_, _, _, _, bidLevels, topBidders, _, _, _, _) = giftAuctionState.auctionState {
if var myBidAmount = giftAuctionState.myState.bidAmount, let myBidDate = giftAuctionState.myState.bidDate, let peer = self.peersMap[component.context.account.peerId] {
var place: Int32 = 1
var isBiddingUp = false
if self.amount.realValue > myBidAmount {
myBidAmount = Int64(self.amount.realValue)
isBiddingUp = true
}
for level in bidLevels {
if myBidAmount < level.amount || (myBidAmount == level.amount && myBidDate > level.date) {
place = level.position + 1
}
}
var bidTitle: String
var bidTitleColor: UIColor
var bidStatus: PeerComponent.Status?
if isBiddingUp {
bidTitleColor = environment.theme.list.itemSecondaryTextColor
bidTitle = environment.strings.Gift_AuctionBid_BidPreview
} else if case let .generic(gift) = giftAuctionState.gift, let auctionGiftsPerRound = gift.auctionGiftsPerRound, place > auctionGiftsPerRound {
bidTitle = environment.strings.Gift_AuctionBid_Outbid
bidTitleColor = environment.theme.list.itemDestructiveColor
bidStatus = .outbid
} else {
bidTitle = environment.strings.Gift_AuctionBid_Winning
bidTitleColor = environment.theme.list.itemDisclosureActions.constructive.fillColor
bidStatus = .winning
}
myBidTitleComponent = AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: bidTitle.uppercased(), font: Font.medium(13.0), textColor: bidTitleColor))))
myBidComponent = AnyComponent(PeerComponent(context: component.context, theme: environment.theme, groupingSeparator: environment.dateTimeFormat.groupingSeparator, peer: peer, place: place, amount: myBidAmount, status: bidStatus, isLast: true))
}
var i: Int32 = 1
for bidder in topBidders {
if let peer = self.peersMap[bidder] {
var bid: Int64 = 0
for level in bidLevels {
if level.position == i {
bid = level.amount
break
}
}
topBidsComponents.append((bidder, AnyComponent(PeerComponent(context: component.context, theme: environment.theme, groupingSeparator: environment.dateTimeFormat.groupingSeparator, peer: peer, place: i, amount: bid, isLast: i == topBidders.count))))
}
i += 1
}
if !topBidsComponents.isEmpty {
topBidsTitleComponent = AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Gift_AuctionBid_TopWinners.uppercased(), font: Font.medium(13.0), textColor: environment.theme.list.itemSecondaryTextColor))))
}
}
if let myBidTitleComponent, let myBidComponent {
let myPeerTitle: ComponentView<Empty>
let myPeerItem: ComponentView<Empty>
if let currentTitle = self.myPeerTitle, let currentItem = self.myPeerItem {
myPeerTitle = currentTitle
myPeerItem = currentItem
} else {
myPeerTitle = ComponentView()
self.myPeerTitle = myPeerTitle
myPeerItem = ComponentView()
self.myPeerItem = myPeerItem
}
let myPeerTitleSize = myPeerTitle.update(
transition: .immediate,
component: myBidTitleComponent,
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height)
)
let myPeerTitleFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: myPeerTitleSize)
if let myPeerTitleView = myPeerTitle.view {
if myPeerTitleView.superview == nil {
self.scrollContentView.addSubview(myPeerTitleView)
}
myPeerTitleView.frame = myPeerTitleFrame
}
contentHeight += myPeerTitleSize.height
contentHeight += 7.0
let myPeerItemSize = myPeerItem.update(
transition: .immediate,
component: myBidComponent,
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height)
)
let myPeerItemFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: myPeerItemSize)
if let myPeerItemView = myPeerItem.view {
if myPeerItemView.superview == nil {
self.scrollContentView.addSubview(myPeerItemView)
}
myPeerItemView.frame = myPeerItemFrame
}
contentHeight += myPeerItemSize.height
contentHeight += 8.0
} else if let myPeerTitle = self.myPeerTitle, let myPeerItem = self.myPeerItem {
self.myPeerTitle = nil
self.myPeerItem = nil
if let myPeerTitleView = myPeerTitle.view, let myPeerItemView = myPeerItem.view {
transition.setAlpha(view: myPeerTitleView, alpha: 0.0, completion: { _ in
myPeerTitleView.removeFromSuperview()
})
transition.setAlpha(view: myPeerItemView, alpha: 0.0, completion: { _ in
myPeerItemView.removeFromSuperview()
})
}
}
if let topBidsTitleComponent {
let topPeersTitle: ComponentView<Empty>
if let currentTitle = self.topPeersTitle {
topPeersTitle = currentTitle
} else {
topPeersTitle = ComponentView()
self.topPeersTitle = topPeersTitle
}
let topPeersTitleSize = topPeersTitle.update(
transition: .immediate,
component: topBidsTitleComponent,
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height)
)
let topPeersTitleFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: topPeersTitleSize)
if let topPeersTitleView = topPeersTitle.view {
if topPeersTitleView.superview == nil {
self.scrollContentView.addSubview(topPeersTitleView)
}
topPeersTitleView.frame = topPeersTitleFrame
}
contentHeight += topPeersTitleSize.height
contentHeight += 7.0
var validKeys: Set<EnginePeer.Id> = Set()
for (peerId, topBidItemComponent) in topBidsComponents {
validKeys.insert(peerId)
let topPeerItem: ComponentView<Empty>
if let current = self.topPeerItems[peerId] {
topPeerItem = current
} else {
topPeerItem = ComponentView()
self.topPeerItems[peerId] = topPeerItem
}
let topPeerItemSize = topPeerItem.update(
transition: .immediate,
component: topBidItemComponent,
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height)
)
let topPeerItemFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: topPeerItemSize)
if let topPeerItemView = topPeerItem.view {
if topPeerItemView.superview == nil {
self.scrollContentView.addSubview(topPeerItemView)
}
topPeerItemView.frame = topPeerItemFrame
}
contentHeight += topPeerItemSize.height
}
var removeKeys: [EnginePeer.Id] = []
for (peerId, item) in self.topPeerItems {
if !validKeys.contains(peerId) {
removeKeys.append(peerId)
if let itemView = item.view {
transition.setAlpha(view: itemView, alpha: 0.0, completion: { _ in
itemView.removeFromSuperview()
})
}
}
}
for id in removeKeys {
self.topPeerItems.removeValue(forKey: id)
}
contentHeight += 16.0
} else if let topPeersTitle = self.topPeersTitle {
self.topPeersTitle = nil
if let topPeersTitleView = topPeersTitle.view {
transition.setAlpha(view: topPeersTitleView, alpha: 0.0, completion: { _ in
topPeersTitleView.removeFromSuperview()
})
}
for (_, item) in self.topPeerItems {
if let itemView = item.view {
transition.setAlpha(view: itemView, alpha: 0.0, completion: { _ in
itemView.removeFromSuperview()
})
}
}
self.topPeerItems = [:]
}
initialContentHeight = contentHeight
if self.cachedStarImage == nil || self.cachedStarImage?.1 !== environment.theme {
self.cachedStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: .white)!, environment.theme)
}
var formattedAmount = presentationStringsFormattedNumber(Int32(clamping: self.amount.realValue), environment.dateTimeFormat.groupingSeparator)
let buttonString: String
let buttonId: String
if let myBidAmount = self.giftAuctionState?.myState.bidAmount {
if myBidAmount == self.amount.realValue {
buttonString = environment.strings.Common_OK
buttonId = "ok"
} else {
formattedAmount = presentationStringsFormattedNumber(Int32(clamping: self.amount.realValue - Int(myBidAmount)), environment.dateTimeFormat.groupingSeparator)
buttonString = environment.strings.Gift_AuctionBid_AddToBid(" # \(formattedAmount)").string
buttonId = "add"
}
} else {
buttonString = environment.strings.Gift_AuctionBid_PlaceBid(" # \(formattedAmount)").string
buttonId = "bid"
}
let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.with(size: 17.0, weight: .semibold, traits: .monospacedNumbers), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)
if let range = buttonAttributedString.string.range(of: "#"), let starImage = self.cachedStarImage?.0 {
buttonAttributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: buttonAttributedString.string))
buttonAttributedString.addAttribute(.foregroundColor, value: environment.theme.list.itemCheckColors.foregroundColor, range: NSRange(range, in: buttonAttributedString.string))
buttonAttributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: buttonAttributedString.string))
buttonAttributedString.addAttribute(.kern, value: 2.0, range: NSRange(range, in: buttonAttributedString.string))
}
let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 54.0, sideInset: 32.0)
let actionButtonSize = actionButton.update(
transition: transition,
component: AnyComponent(ButtonComponent(
background: ButtonComponent.Background(
style: .glass,
color: environment.theme.list.itemCheckColors.fillColor,
foreground: environment.theme.list.itemCheckColors.foregroundColor,
pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9),
cornerRadius: 54.0 * 0.5
),
content: AnyComponentWithIdentity(
id: AnyHashable(buttonId),
component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString)))
),
isEnabled: true,
displaysProgress: self.isLoading,
action: { [weak self] in
guard let self, let component = self.component else {
return
}
guard let balance = self.balance else {
return
}
if balance < StarsAmount(value: Int64(self.amount.realValue), nanos: 0) {
let _ = (component.context.engine.payments.starsTopUpOptions()
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { [weak self] options in
guard let self, let component = self.component else {
return
}
guard let starsContext = component.context.starsContext else {
return
}
let purchasePurpose: StarsPurchasePurpose = .generic
let purchaseScreen = component.context.sharedContext.makeStarsPurchaseScreen(context: component.context, starsContext: starsContext, options: options, purpose: purchasePurpose, targetPeerId: nil, customTheme: environment.theme, completion: { result in
let _ = result
//TODO:release
})
self.environment?.controller()?.push(purchaseScreen)
self.environment?.controller()?.dismiss()
})
return
}
self.placeBid()
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width - buttonInsets.left - buttonInsets.right, height: 54.0)
)
var bottomPanelHeight = 13.0 + buttonInsets.bottom + actionButtonSize.height
let actionButtonFrame = CGRect(origin: CGPoint(x: buttonInsets.left, y: availableSize.height - buttonInsets.bottom - actionButtonSize.height), size: actionButtonSize)
bottomPanelHeight -= 1.0
if let actionButtonView = actionButton.view {
if actionButtonView.superview == nil {
self.containerView.addSubview(actionButtonView)
}
transition.setFrame(view: actionButtonView, frame: actionButtonFrame)
}
contentHeight += bottomPanelHeight
initialContentHeight += bottomPanelHeight
clippingY = actionButtonFrame.maxY + 24.0
let topInset: CGFloat = max(0.0, availableSize.height - containerInset - initialContentHeight)
let scrollContentHeight = max(topInset + contentHeight + containerInset, availableSize.height - containerInset)
self.scrollContentClippingView.layer.cornerRadius = 10.0
self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, containerCornerRadius: environment.deviceMetrics.screenCornerRadius, bottomInset: environment.safeInsets.bottom, topInset: topInset)
transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + containerInset), size: CGSize(width: availableSize.width, height: contentHeight)))
transition.setPosition(layer: self.backgroundLayer, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0))
transition.setBounds(layer: self.backgroundLayer, bounds: CGRect(origin: CGPoint(), size: CGSize(width: fillingSize, height: availableSize.height)))
let scrollClippingFrame = CGRect(origin: CGPoint(x: 0.0, y: containerInset), size: CGSize(width: availableSize.width, height: clippingY - containerInset))
transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center)
transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size))
self.ignoreScrolling = true
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height)))
let contentSize = CGSize(width: availableSize.width, height: scrollContentHeight)
if contentSize != self.scrollView.contentSize {
self.scrollView.contentSize = contentSize
}
if resetScrolling {
self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: availableSize)
}
self.ignoreScrolling = false
self.updateScrolling(transition: transition)
transition.setPosition(view: self.containerView, position: CGRect(origin: CGPoint(), size: availableSize).center)
transition.setBounds(view: self.containerView, bounds: CGRect(origin: CGPoint(), size: availableSize))
if let controller = environment.controller(), !controller.automaticallyControlPresentationContextLayout {
let bottomInset: CGFloat = contentHeight - 12.0
let layout = ContainerViewLayout(
size: availableSize,
metrics: environment.metrics,
deviceMetrics: environment.deviceMetrics,
intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomInset, right: 0.0),
safeInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0),
additionalInsets: .zero,
statusBarHeight: environment.statusBarHeight,
inputHeight: nil,
inputHeightIsInteractivellyChanging: false,
inVoiceOver: false
)
controller.presentationContext.containerLayoutUpdated(layout, transition: transition.containedViewLayoutTransition)
}
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public class GiftAuctionBidScreen: ViewControllerComponentContainer {
private let context: AccountContext
private var didPlayAppearAnimation: Bool = false
private var isDismissed: Bool = false
public init(context: AccountContext, auctionContext: GiftAuctionContext) {
self.context = context
super.init(context: context, component: GiftAuctionBidScreenComponent(
context: context,
gift: auctionContext.gift,
auctionContext: auctionContext
), navigationBarAppearance: .none, theme: .default)
self.statusBar.statusBarStyle = .Ignore
self.navigationPresentation = .flatModal
self.blocksBackgroundWhenInOverlay = true
self.automaticallyControlPresentationContextLayout = false
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.view.disablesInteractiveModalDismiss = true
if !self.didPlayAppearAnimation {
self.didPlayAppearAnimation = true
if let componentView = self.node.hostView.componentView as? GiftAuctionBidScreenComponent.View {
componentView.animateIn()
}
}
}
override public func dismiss(completion: (() -> Void)? = nil) {
if !self.isDismissed {
self.isDismissed = true
if let componentView = self.node.hostView.componentView as? GiftAuctionBidScreenComponent.View {
componentView.animateOut(completion: { [weak self] in
completion?()
self?.dismiss(animated: false)
})
} else {
self.dismiss(animated: false)
}
}
}
}
private final class BadgeStarsView: UIView {
private let staticEmitterLayer = CAEmitterLayer()
private let dynamicEmitterLayer = CAEmitterLayer()
private var currentColor: UIColor?
override init(frame: CGRect) {
super.init(frame: frame)
self.layer.addSublayer(self.staticEmitterLayer)
self.layer.addSublayer(self.dynamicEmitterLayer)
}
required init(coder: NSCoder) {
preconditionFailure()
}
private func setupEmitter() {
guard let currentColor = self.currentColor else {
return
}
let color = currentColor
self.staticEmitterLayer.emitterShape = .circle
self.staticEmitterLayer.emitterSize = CGSize(width: 10.0, height: 5.0)
self.staticEmitterLayer.emitterMode = .outline
self.layer.addSublayer(self.staticEmitterLayer)
self.dynamicEmitterLayer.birthRate = 0.0
self.dynamicEmitterLayer.emitterShape = .circle
self.dynamicEmitterLayer.emitterSize = CGSize(width: 10.0, height: 55.0)
self.dynamicEmitterLayer.emitterMode = .surface
self.layer.addSublayer(self.dynamicEmitterLayer)
let staticEmitter = CAEmitterCell()
staticEmitter.name = "emitter"
staticEmitter.contents = UIImage(bundleImageName: "Premium/Stars/Particle")?.cgImage
staticEmitter.birthRate = 20.0
staticEmitter.lifetime = 2.7
staticEmitter.velocity = 30.0
staticEmitter.velocityRange = 3
staticEmitter.scale = 0.15
staticEmitter.scaleRange = 0.08
staticEmitter.emissionRange = .pi * 2.0
staticEmitter.setValue(3.0, forKey: "mass")
staticEmitter.setValue(2.0, forKey: "massRange")
let dynamicEmitter = CAEmitterCell()
dynamicEmitter.name = "emitter"
dynamicEmitter.contents = UIImage(bundleImageName: "Premium/Stars/Particle")?.cgImage
dynamicEmitter.birthRate = 0.0
dynamicEmitter.lifetime = 2.7
dynamicEmitter.velocity = 30.0
dynamicEmitter.velocityRange = 3
dynamicEmitter.scale = 0.15
dynamicEmitter.scaleRange = 0.08
dynamicEmitter.emissionRange = .pi / 3.0
dynamicEmitter.setValue(3.0, forKey: "mass")
dynamicEmitter.setValue(2.0, forKey: "massRange")
let staticColors: [Any] = [
UIColor.white.withAlphaComponent(0.0).cgColor,
UIColor.white.withAlphaComponent(0.35).cgColor,
color.cgColor,
color.cgColor,
color.withAlphaComponent(0.0).cgColor
]
let staticColorBehavior = CAEmitterCell.createEmitterBehavior(type: "colorOverLife")
staticColorBehavior.setValue(staticColors, forKey: "colors")
staticEmitter.setValue([staticColorBehavior], forKey: "emitterBehaviors")
let dynamicColors: [Any] = [
UIColor.white.withAlphaComponent(0.35).cgColor,
color.withAlphaComponent(0.85).cgColor,
color.cgColor,
color.cgColor,
color.withAlphaComponent(0.0).cgColor
]
let dynamicColorBehavior = CAEmitterCell.createEmitterBehavior(type: "colorOverLife")
dynamicColorBehavior.setValue(dynamicColors, forKey: "colors")
dynamicEmitter.setValue([dynamicColorBehavior], forKey: "emitterBehaviors")
let attractor = CAEmitterCell.createEmitterBehavior(type: "simpleAttractor")
attractor.setValue("attractor", forKey: "name")
attractor.setValue(20, forKey: "falloff")
attractor.setValue(35, forKey: "radius")
self.staticEmitterLayer.setValue([attractor], forKey: "emitterBehaviors")
self.staticEmitterLayer.setValue(4.0, forKeyPath: "emitterBehaviors.attractor.stiffness")
self.staticEmitterLayer.setValue(false, forKeyPath: "emitterBehaviors.attractor.enabled")
self.staticEmitterLayer.emitterCells = [staticEmitter]
self.dynamicEmitterLayer.emitterCells = [dynamicEmitter]
}
func update(speed: Float, delta: Float? = nil) {
if speed > 0.0 {
if self.dynamicEmitterLayer.birthRate.isZero {
self.dynamicEmitterLayer.beginTime = CACurrentMediaTime()
}
self.dynamicEmitterLayer.setValue(Float(20.0 + speed * 1.4), forKeyPath: "emitterCells.emitter.birthRate")
self.dynamicEmitterLayer.setValue(2.7 - min(1.1, 1.5 * speed / 120.0), forKeyPath: "emitterCells.emitter.lifetime")
self.dynamicEmitterLayer.setValue(30.0 + CGFloat(speed / 80.0), forKeyPath: "emitterCells.emitter.velocity")
if let delta, speed > 15.0 {
self.dynamicEmitterLayer.setValue(delta > 0 ? .pi : 0, forKeyPath: "emitterCells.emitter.emissionLongitude")
self.dynamicEmitterLayer.setValue(.pi / 2.0, forKeyPath: "emitterCells.emitter.emissionRange")
} else {
self.dynamicEmitterLayer.setValue(0.0, forKeyPath: "emitterCells.emitter.emissionLongitude")
self.dynamicEmitterLayer.setValue(.pi * 2.0, forKeyPath: "emitterCells.emitter.emissionRange")
}
self.staticEmitterLayer.setValue(true, forKeyPath: "emitterBehaviors.attractor.enabled")
self.dynamicEmitterLayer.birthRate = 1.0
self.staticEmitterLayer.birthRate = 0.0
} else {
self.dynamicEmitterLayer.birthRate = 0.0
if let staticEmitter = self.staticEmitterLayer.emitterCells?.first {
staticEmitter.beginTime = CACurrentMediaTime()
}
self.staticEmitterLayer.birthRate = 1.0
self.staticEmitterLayer.setValue(false, forKeyPath: "emitterBehaviors.attractor.enabled")
}
}
func update(size: CGSize, color: UIColor, emitterPosition: CGPoint) {
if self.staticEmitterLayer.emitterCells == nil {
self.currentColor = color
self.setupEmitter()
} else if self.currentColor != color {
self.currentColor = color
let staticColors: [Any] = [
UIColor.white.withAlphaComponent(0.0).cgColor,
UIColor.white.withAlphaComponent(0.35).cgColor,
color.cgColor,
color.cgColor,
color.withAlphaComponent(0.0).cgColor
]
let staticColorBehavior = CAEmitterCell.createEmitterBehavior(type: "colorOverLife")
staticColorBehavior.setValue(staticColors, forKey: "colors")
let dynamicColors: [Any] = [
UIColor.white.withAlphaComponent(0.35).cgColor,
color.withAlphaComponent(0.85).cgColor,
color.cgColor,
color.cgColor,
color.withAlphaComponent(0.0).cgColor
]
let dynamicColorBehavior = CAEmitterCell.createEmitterBehavior(type: "colorOverLife")
dynamicColorBehavior.setValue(dynamicColors, forKey: "colors")
for cell in self.staticEmitterLayer.emitterCells ?? [] {
cell.setValue([staticColorBehavior], forKey: "emitterBehaviors")
}
for cell in self.dynamicEmitterLayer.emitterCells ?? [] {
cell.setValue([dynamicColorBehavior], forKey: "emitterBehaviors")
}
}
self.staticEmitterLayer.frame = CGRect(origin: .zero, size: size)
self.staticEmitterLayer.emitterPosition = emitterPosition
self.dynamicEmitterLayer.frame = CGRect(origin: .zero, size: size)
self.dynamicEmitterLayer.emitterPosition = emitterPosition
self.staticEmitterLayer.setValue(emitterPosition, forKeyPath: "emitterBehaviors.attractor.position")
}
}
private final class SliderStarsView: UIView {
private let emitterLayer = CAEmitterLayer()
override init(frame: CGRect) {
super.init(frame: frame)
self.layer.addSublayer(self.emitterLayer)
}
required init(coder: NSCoder) {
preconditionFailure()
}
private func setupEmitter() {
self.emitterLayer.emitterShape = .rectangle
self.emitterLayer.emitterMode = .surface
self.layer.addSublayer(self.emitterLayer)
let emitter = CAEmitterCell()
emitter.name = "emitter"
emitter.contents = UIImage(bundleImageName: "Premium/Stars/Particle")?.cgImage
emitter.birthRate = 20.0
emitter.lifetime = 2.0
emitter.velocity = 15.0
emitter.velocityRange = 10
emitter.scale = 0.15
emitter.scaleRange = 0.08
emitter.emissionRange = .pi / 4.0
emitter.setValue(3.0, forKey: "mass")
emitter.setValue(2.0, forKey: "massRange")
self.emitterLayer.emitterCells = [emitter]
let colors: [Any] = [
UIColor.white.withAlphaComponent(0.0).cgColor,
UIColor.white.withAlphaComponent(0.38).cgColor,
UIColor.white.withAlphaComponent(0.38).cgColor,
UIColor.white.withAlphaComponent(0.0).cgColor,
UIColor.white.withAlphaComponent(0.38).cgColor,
UIColor.white.withAlphaComponent(0.38).cgColor,
UIColor.white.withAlphaComponent(0.0).cgColor
]
let colorBehavior = CAEmitterCell.createEmitterBehavior(type: "colorOverLife")
colorBehavior.setValue(colors, forKey: "colors")
emitter.setValue([colorBehavior], forKey: "emitterBehaviors")
}
func update(size: CGSize, value: CGFloat) {
if self.emitterLayer.emitterCells == nil {
self.setupEmitter()
}
self.emitterLayer.setValue(20.0 + Float(value * 200.0), forKeyPath: "emitterCells.emitter.birthRate")
self.emitterLayer.setValue(15.0 + value * 250.0, forKeyPath: "emitterCells.emitter.velocity")
self.emitterLayer.frame = CGRect(origin: .zero, size: size)
self.emitterLayer.emitterPosition = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
self.emitterLayer.emitterSize = size
}
}
private final class AuctionStatComponent: Component {
let context: AccountContext
let gift: StarGift?
let title: [AnimatedTextComponent.Item]
let subtitle: String
let theme: PresentationTheme
init(
context: AccountContext,
gift: StarGift?,
title: [AnimatedTextComponent.Item],
subtitle: String,
theme: PresentationTheme
) {
self.context = context
self.gift = gift
self.title = title
self.subtitle = subtitle
self.theme = theme
}
static func ==(lhs: AuctionStatComponent, rhs: AuctionStatComponent) -> Bool {
if lhs.gift != rhs.gift {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.subtitle != rhs.subtitle {
return false
}
if lhs.theme != rhs.theme {
return false
}
return true
}
final class View: UIView {
let gift = ComponentView<Empty>()
let background = ComponentView<Empty>()
let title = ComponentView<Empty>()
let subtitle = ComponentView<Empty>()
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: AuctionStatComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let backgroundFrame = CGRect(origin: CGPoint(), size: availableSize)
let _ = self.background.update(
transition: transition,
component: AnyComponent(FilledRoundedRectangleComponent(
color: UIColor(rgb: 0x808084, alpha: 0.1),
cornerRadius: .value(12.0),
smoothCorners: true
)),
environment: {},
containerSize: backgroundFrame.size
)
if let backgroundView = self.background.view {
if backgroundView.superview == nil {
self.addSubview(backgroundView)
}
transition.setFrame(view: backgroundView, frame: backgroundFrame)
}
var titleTotalWidth: CGFloat = 0.0
let titleSize = self.title.update(
transition: .spring(duration: 0.2),
component: AnyComponent(AnimatedTextComponent(
font: Font.with(size: 20.0, weight: .semibold, traits: .monospacedNumbers),
color: component.theme.list.itemPrimaryTextColor,
items: component.title,
noDelay: true,
blur: true
)),
environment: {},
containerSize: backgroundFrame.size
)
titleTotalWidth += titleSize.width
var giftSize = CGSize()
if let gift = component.gift, case let .generic(gift) = gift {
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
giftSize = self.gift.update(
transition: .immediate,
component: AnyComponent(
GiftItemComponent(
context: component.context,
theme: presentationData.theme,
strings: presentationData.strings,
peer: nil,
subject: .starGift(gift: gift, price: ""),
mode: .tableIcon
)
),
environment: {},
containerSize: availableSize
)
titleTotalWidth += giftSize.width
titleTotalWidth += 4.0
}
let subtitleSize = self.subtitle.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.subtitle, font: Font.regular(11.0), textColor: component.theme.list.itemPrimaryTextColor))
)),
environment: {},
containerSize: backgroundFrame.size
)
let spacing: CGFloat = 2.0
let giftFrame = CGRect(origin: CGPoint(x: floor((backgroundFrame.width - titleTotalWidth) * 0.5), y: floor((backgroundFrame.height - giftSize.height - spacing - subtitleSize.height) * 0.5)), size: giftSize)
let titleFrame = CGRect(origin: CGPoint(x: floor((backgroundFrame.width + titleTotalWidth) * 0.5) - titleSize.width, y: floor((backgroundFrame.height - titleSize.height - spacing - subtitleSize.height) * 0.5)), size: titleSize)
let subtitleFrame = CGRect(origin: CGPoint(x: floor((backgroundFrame.width - subtitleSize.width) * 0.5), y: titleFrame.maxY + spacing), size: subtitleSize)
if let giftView = self.gift.view {
if giftView.superview == nil {
self.addSubview(giftView)
}
giftView.frame = giftFrame
}
if let titleView = self.title.view {
if titleView.superview == nil {
self.addSubview(titleView)
}
titleView.frame = titleFrame
}
if let subtitleView = self.subtitle.view {
if subtitleView.superview == nil {
self.addSubview(subtitleView)
}
subtitleView.frame = subtitleFrame
}
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private final class GiftViewContextReferenceContentSource: ContextReferenceContentSource {
private let controller: ViewController
private let sourceView: UIView
init(controller: ViewController, sourceView: UIView) {
self.controller = controller
self.sourceView = sourceView
}
func transitionInfo() -> ContextControllerReferenceViewInfo? {
return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds)
}
}