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() private let badgeForeground: SimpleLayer let badgeIcon: UIImageView private let badgeLabel: BadgeLabelView private let badgeLabelMaskView = UIImageView() private let prefix = ComponentView() private let subtitle = ComponentView() 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, 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, 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() 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, 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, 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() private let title = ComponentView() private let amount = ComponentView() 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, 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, 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() private let topBackgroundText = ComponentView() 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, 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, 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() private let backgroundHandleView: UIImageView private let closeButton = ComponentView() private let moreButton = ComponentView() private let moreButtonPlayOnce = ActionSlot() private let title = ComponentView() private let badgeStars = BadgeStarsView() private let sliderBackground = ComponentView() private let slider = ComponentView() private let sliderPlus = ComponentView() private let badge = ComponentView() private var liveStreamPerks: [ComponentView] = [] private var liveStreamMessagePreview: ComponentView? private var myPeerTitle: ComponentView? private var myPeerItem: ComponentView? private var topPeersTitle: ComponentView? private var topPeerItems: [EnginePeer.Id: ComponentView] = [:] private var giftAuctionState: GiftAuctionContext.State? private var giftAuctionDisposable: Disposable? private var giftAuctionTimer: SwiftSignalKit.Timer? private var peersMap: [EnginePeer.Id: EnginePeer] = [:] private let actionButton = ComponentView() 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 in switch error { case .disallowedStarGifts: return .fail(.disallowedStarGift) case .starGiftsUserLimit: return .fail(.starGiftUserLimit) default: return .fail(.generic) } } |> mapToSignal { inputData -> Signal 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, 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[.. 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? var myBidComponent: AnyComponent? var topBidsTitleComponent: AnyComponent? var topBidsComponents: [(EnginePeer.Id, AnyComponent)] = [] 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 let myPeerItem: ComponentView 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 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 = Set() for (peerId, topBidItemComponent) in topBidsComponents { validKeys.insert(peerId) let topPeerItem: ComponentView 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, 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() let background = ComponentView() let title = ComponentView() let subtitle = ComponentView() 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, 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, 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) } }