GLEGram 12.5 — Initial public release

Based on Swiftgram 12.5 (Telegram iOS 12.5).
All GLEGram features ported and organized in GLEGram/ folder.

Features: Ghost Mode, Saved Deleted Messages, Content Protection Bypass,
Font Replacement, Fake Profile, Chat Export, Plugin System, and more.

See CHANGELOG_12.5.md for full details.
This commit is contained in:
Leeksov
2026-04-06 09:48:12 +03:00
commit 4647310322
39685 changed files with 11052678 additions and 0 deletions
@@ -0,0 +1,177 @@
import Foundation
import UIKit
import Display
import ComponentFlow
private let nondigitsCharacterSet = CharacterSet(charactersIn: "0123456789").inverted
private let labelWidth: CGFloat = 16.0
private let labelHeight: CGFloat = 36.0
private let labelSize = CGSize(width: labelWidth, height: labelHeight)
private let font = Font.with(size: 24.0, design: .round, weight: .semibold, traits: [])
final class BadgeLabelView: UIView {
private class StackView: UIView {
var labels: [UILabel] = []
var currentValue: Int32 = 0
var color: UIColor = .white {
didSet {
for view in self.labels {
view.textColor = self.color
}
}
}
init() {
super.init(frame: CGRect(origin: .zero, size: labelSize))
var height: CGFloat = -labelHeight
for i in -1 ..< 10 {
let label = UILabel()
if i == -1 {
label.text = "9"
} else {
label.text = "\(i)"
}
label.textColor = self.color
label.font = font
label.textAlignment = .center
label.frame = CGRect(x: 0, y: height, width: labelWidth, height: labelHeight)
self.addSubview(label)
self.labels.append(label)
height += labelHeight
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(value: Int32, isFirst: Bool, isLast: Bool, transition: ComponentTransition) {
let previousValue = self.currentValue
self.currentValue = value
self.labels[1].alpha = isFirst && !isLast ? 0.0 : 1.0
if previousValue == 9 && value < 9 {
self.bounds = CGRect(
origin: CGPoint(
x: 0.0,
y: -1.0 * labelSize.height
),
size: labelSize
)
}
let bounds = CGRect(
origin: CGPoint(
x: 0.0,
y: CGFloat(value) * labelSize.height
),
size: labelSize
)
transition.setBounds(view: self, bounds: bounds)
}
}
private var itemViews: [Int: StackView] = [:]
private var staticLabel = UILabel()
init() {
super.init(frame: .zero)
self.clipsToBounds = true
self.isUserInteractionEnabled = false
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var color: UIColor = .white {
didSet {
self.staticLabel.textColor = self.color
for (_, view) in self.itemViews {
view.color = self.color
}
}
}
func update(value: String, transition: ComponentTransition) -> CGSize {
let alphaTransition = ComponentTransition.easeInOut(duration: 0.15)
if value.rangeOfCharacter(from: nondigitsCharacterSet) != nil {
for (_, view) in self.itemViews {
alphaTransition.setAlpha(view: view, alpha: 0.0)
}
if self.staticLabel.superview == nil {
self.staticLabel.alpha = 0.0
self.staticLabel.textColor = self.color
self.staticLabel.font = font
self.addSubview(self.staticLabel)
}
alphaTransition.setAlpha(view: self.staticLabel, alpha: 1.0)
self.staticLabel.text = value
let size = self.staticLabel.sizeThatFits(CGSize(width: 100.0, height: 100.0))
self.staticLabel.frame = CGRect(origin: .zero, size: CGSize(width: size.width, height: labelHeight))
return CGSize(width: ceil(self.staticLabel.bounds.width), height: ceil(self.staticLabel.bounds.height))
} else {
for (_, view) in self.itemViews {
alphaTransition.setAlpha(view: view, alpha: 1.0)
}
alphaTransition.setAlpha(view: self.staticLabel, alpha: 0.0)
}
let string = value
let stringArray = Array(string.map { String($0) }.reversed())
let totalWidth = CGFloat(stringArray.count) * labelWidth
var validIds: [Int] = []
for i in 0 ..< stringArray.count {
validIds.append(i)
let itemView: StackView
var itemTransition = transition
if let current = self.itemViews[i] {
itemView = current
} else {
itemTransition = transition.withAnimation(.none)
itemView = StackView()
itemView.color = self.color
self.itemViews[i] = itemView
self.addSubview(itemView)
}
let digit = Int32(stringArray[i]) ?? 0
itemView.update(value: digit, isFirst: i == stringArray.count - 1, isLast: i == 0, transition: transition)
itemTransition.setFrame(
view: itemView,
frame: CGRect(x: totalWidth - labelWidth * CGFloat(i + 1), y: 0.0, width: labelWidth, height: labelHeight)
)
}
var removeIds: [Int] = []
for (id, itemView) in self.itemViews {
if !validIds.contains(id) {
removeIds.append(id)
transition.setAlpha(view: itemView, alpha: 0.0, completion: { _ in
itemView.removeFromSuperview()
})
}
}
for id in removeIds {
self.itemViews.removeValue(forKey: id)
}
return CGSize(width: totalWidth, height: labelHeight)
}
}
@@ -0,0 +1,708 @@
import Foundation
import UIKit
import AsyncDisplayKit
import TelegramPresentationData
import ComponentFlow
import AccountContext
import ViewControllerComponent
import TelegramCore
import SwiftSignalKit
import Display
import MultilineTextComponent
import MultilineTextWithEntitiesComponent
import ButtonComponent
import PlainButtonComponent
import Markdown
import BundleIconComponent
import TextFormat
import TelegramStringFormatting
import GlassBarButtonComponent
import GiftItemComponent
import EdgeEffect
import TableComponent
import PeerTableCellComponent
private final class GiftAuctionAcquiredScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let gift: StarGift
let acquiredGifts: [GiftAuctionAcquiredGift]
init(
context: AccountContext,
gift: StarGift,
acquiredGifts: [GiftAuctionAcquiredGift]
) {
self.context = context
self.gift = gift
self.acquiredGifts = acquiredGifts
}
static func ==(lhs: GiftAuctionAcquiredScreenComponent, rhs: GiftAuctionAcquiredScreenComponent) -> 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)
}
}
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 topEdgeEffectView: EdgeEffectView
private let bottomEdgeEffectView: EdgeEffectView
private let backgroundHandleView: UIImageView
private let closeButton = ComponentView<Empty>()
private let title = ComponentView<Empty>()
private var itemsViews: [Int32: ComponentView<Empty>] = [:]
private let actionButton = ComponentView<Empty>()
private var ignoreScrolling: Bool = false
private var component: GiftAuctionAcquiredScreenComponent?
private weak var state: EmptyComponentState?
private var isUpdating: Bool = false
private var environment: ViewControllerComponentContainer.Environment?
private var itemLayout: ItemLayout?
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.topEdgeEffectView = EdgeEffectView()
self.topEdgeEffectView.clipsToBounds = true
self.topEdgeEffectView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
self.topEdgeEffectView.layer.cornerRadius = 40.0
self.bottomEdgeEffectView = EdgeEffectView()
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(_:))))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
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 !self.backgroundLayer.frame.contains(point) {
return self.dimView
}
if let result = self.navigationBarContainer.hitTest(self.convert(point, to: self.navigationBarContainer), with: event) {
return result
}
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)
}
self.bottomEdgeEffectView.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)
}
self.bottomEdgeEffectView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
}
private func openPeer(_ peer: EnginePeer, dismiss: Bool = true) {
guard let component = self.component, let controller = self.environment?.controller() as? GiftAuctionAcquiredScreen, 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 update(component: GiftAuctionAcquiredScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
let environment = environment[ViewControllerComponentContainer.Environment.self].value
let themeUpdated = self.environment?.theme !== environment.theme
let resetScrolling = self.scrollView.bounds.width != availableSize.width
let fillingSize: CGFloat
if case .regular = environment.metrics.widthClass {
fillingSize = min(availableSize.width, 414.0) - environment.safeInsets.left * 2.0
} else {
fillingSize = min(availableSize.width, environment.deviceMetrics.screenSize.width) - environment.safeInsets.left * 2.0
}
let rawSideInset = floor((availableSize.width - fillingSize) * 0.5)
let sideInset: CGFloat = floor((availableSize.width - fillingSize) * 0.5) + 24.0
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 = 75.0
let tableFont = Font.regular(15.0)
let tableBoldFont = Font.semibold(15.0)
let tableTextColor = environment.theme.list.itemPrimaryTextColor
for gift in component.acquiredGifts {
let id = gift.date
let itemView: ComponentView<Empty>
if let current = self.itemsViews[id] {
itemView = current
} else {
itemView = ComponentView()
self.itemsViews[id] = itemView
}
var items: [TableComponent.Item] = []
var giftSubject: GiftItemComponent.Subject?
var giftTitle: String = ""
if case let .generic(gift) = component.gift {
giftSubject = .starGift(gift: gift, price: "")
giftTitle = gift.title ?? ""
}
if let giftSubject {
let titleString: String
if let number = gift.number {
let fullGiftTitle = "\(giftTitle) #\(formatCollectibleNumber(number, dateTimeFormat: environment.dateTimeFormat))"
titleString = environment.strings.Gift_Acquired_GiftRound(fullGiftTitle, "\(gift.round)").string
} else {
titleString = environment.strings.Gift_Acquired_Round("\(gift.round)").string
}
items.append(.init(
id: "header",
title: nil,
hasBackground: true,
component: AnyComponent(HStack([
AnyComponentWithIdentity(id: "icon", component: AnyComponent(
GiftItemComponent(
context: component.context,
theme: environment.theme,
strings: environment.strings,
peer: nil,
subject: giftSubject,
mode: .tableIcon
)
)),
AnyComponentWithIdentity(
id: "title",
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: titleString, font: tableBoldFont, textColor: tableTextColor)))
)
)
], spacing: 1.0))
))
}
items.append(.init(
id: "recipient",
title: environment.strings.Gift_Acquired_Recipient,
component: AnyComponent(Button(
content: AnyComponent(
PeerTableCellComponent(
context: component.context,
theme: environment.theme,
strings: environment.strings,
peer: gift.peer
)
),
action: { [weak self] in
guard let self else {
return
}
self.openPeer(gift.peer, dismiss: false)
}
))
))
items.append(.init(
id: "date",
title: environment.strings.Gift_Acquired_Date,
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: gift.date, strings: environment.strings, dateTimeFormat: environment.dateTimeFormat), font: tableFont, textColor: tableTextColor))))
))
let valueString = "⭐️\(formatStarsAmountText(StarsAmount(value: gift.bidAmount, nanos: 0), dateTimeFormat: environment.dateTimeFormat))"
let valueAttributedString = NSMutableAttributedString(string: valueString, font: tableFont, textColor: tableTextColor)
let range = (valueAttributedString.string as NSString).range(of: "⭐️")
if range.location != NSNotFound {
valueAttributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range)
valueAttributedString.addAttribute(.baselineOffset, value: 1.0, range: range)
}
items.append(.init(
id: "bid",
title: environment.strings.Gift_Acquired_AcceptedBid,
component: AnyComponent(HStack([
AnyComponentWithIdentity(id: "stars", component: AnyComponent(MultilineTextWithEntitiesComponent(
context: component.context,
animationCache: component.context.animationCache,
animationRenderer: component.context.animationRenderer,
placeholderColor: environment.theme.list.mediaPlaceholderColor,
text: .plain(valueAttributedString),
maximumNumberOfLines: 0
))),
AnyComponentWithIdentity(
id: AnyHashable("info"),
component: AnyComponent(Button(
content: AnyComponent(ButtonContentComponent(
context: component.context,
text: environment.strings.Gift_Acquired_Top("\(gift.position)").string,
color: environment.theme.list.itemAccentColor
)),
action: {
}
))
)
], spacing: 4.0))
))
let itemSize = itemView.update(
transition: transition,
component: AnyComponent(
TableComponent(
theme: environment.theme,
items: items
)
),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0)
)
let itemFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: itemSize)
if let view = itemView.view {
if view.superview == nil {
self.scrollContentView.addSubview(view)
}
view.frame = itemFrame
}
contentHeight += itemSize.height
contentHeight += 20.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: 44.0, height: 44.0),
backgroundColor: nil,
isDark: environment.theme.overallDarkAppearance,
state: .glass,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: environment.theme.chat.inputPanel.panelControlColor
)
)),
action: { [weak self] _ in
guard let self else {
return
}
self.environment?.controller()?.dismiss()
}
)),
environment: {},
containerSize: CGSize(width: 44.0, height: 44.0)
)
let closeButtonFrame = CGRect(origin: CGPoint(x: rawSideInset + 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 containerInset: CGFloat = environment.statusBarHeight + 10.0
var initialContentHeight = contentHeight
let clippingY: CGFloat
let title = self.title
let actionButton = self.actionButton
let titleText = environment.strings.Gift_Acquired_Title(Int32(component.acquiredGifts.count))
let titleSize = title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: titleText, 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)
}
initialContentHeight = contentHeight
let buttonAttributedString = NSMutableAttributedString(string: environment.strings.Common_OK, font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)
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("ok"),
component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString)))
),
isEnabled: true,
displaysProgress: false,
action: { [weak self] in
guard let self else {
return
}
self.environment?.controller()?.dismiss()
}
)),
environment: {},
containerSize: CGSize(width: fillingSize - buttonInsets.left - buttonInsets.right, height: 54.0)
)
let edgeEffectHeight: CGFloat = 80.0
let edgeEffectFrame = CGRect(origin: CGPoint(x: rawSideInset, y: 0.0), size: CGSize(width: fillingSize, height: edgeEffectHeight))
transition.setFrame(view: self.topEdgeEffectView, frame: edgeEffectFrame)
self.topEdgeEffectView.update(content: environment.theme.actionSheet.opaqueItemBackgroundColor, blur: true, alpha: 1.0, rect: edgeEffectFrame, edge: .top, edgeSize: edgeEffectFrame.height, transition: transition)
if self.topEdgeEffectView.superview == nil {
self.navigationBarContainer.insertSubview(self.topEdgeEffectView, at: 0)
}
var bottomPanelHeight = 13.0 + buttonInsets.bottom + actionButtonSize.height
let bottomEdgeEffectHeight: CGFloat = bottomPanelHeight
let bottomEdgeEffectFrame = CGRect(origin: CGPoint(x: rawSideInset, y: availableSize.height - bottomEdgeEffectHeight), size: CGSize(width: fillingSize, height: bottomEdgeEffectHeight))
transition.setFrame(view: self.bottomEdgeEffectView, frame: bottomEdgeEffectFrame)
self.bottomEdgeEffectView.update(content: environment.theme.actionSheet.opaqueItemBackgroundColor, blur: true, alpha: 1.0, rect: bottomEdgeEffectFrame, edge: .bottom, edgeSize: bottomEdgeEffectFrame.height, transition: transition)
if self.bottomEdgeEffectView.superview == nil {
self.containerView.addSubview(self.bottomEdgeEffectView)
}
let actionButtonFrame = CGRect(origin: CGPoint(x: rawSideInset + 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 = 38.0
self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, containerCornerRadius: environment.deviceMetrics.screenCornerRadius, bottomInset: environment.safeInsets.bottom, topInset: topInset)
transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + containerInset), size: CGSize(width: availableSize.width, height: contentHeight)))
transition.setPosition(layer: self.backgroundLayer, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0))
transition.setBounds(layer: self.backgroundLayer, bounds: CGRect(origin: CGPoint(), size: CGSize(width: fillingSize, height: availableSize.height)))
let scrollClippingFrame = CGRect(origin: CGPoint(x: 0.0, y: containerInset), size: CGSize(width: availableSize.width, height: clippingY - containerInset))
transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center)
transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size))
self.ignoreScrolling = true
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height)))
let contentSize = CGSize(width: availableSize.width, height: scrollContentHeight)
if contentSize != self.scrollView.contentSize {
self.scrollView.contentSize = contentSize
}
if resetScrolling {
self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: availableSize)
}
self.ignoreScrolling = false
self.updateScrolling(transition: transition)
transition.setPosition(view: self.containerView, position: CGRect(origin: CGPoint(), size: availableSize).center)
transition.setBounds(view: self.containerView, bounds: CGRect(origin: CGPoint(), size: availableSize))
if let controller = environment.controller(), !controller.automaticallyControlPresentationContextLayout {
let bottomInset: CGFloat = contentHeight - 12.0
let layout = ContainerViewLayout(
size: availableSize,
metrics: environment.metrics,
deviceMetrics: environment.deviceMetrics,
intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomInset, right: 0.0),
safeInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0),
additionalInsets: .zero,
statusBarHeight: environment.statusBarHeight,
inputHeight: nil,
inputHeightIsInteractivellyChanging: false,
inVoiceOver: false
)
controller.presentationContext.containerLayoutUpdated(layout, transition: transition.containedViewLayoutTransition)
}
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public class GiftAuctionAcquiredScreen: ViewControllerComponentContainer {
private let context: AccountContext
private var didPlayAppearAnimation: Bool = false
private var isDismissed: Bool = false
public init(context: AccountContext, gift: StarGift, acquiredGifts: [GiftAuctionAcquiredGift]) {
self.context = context
super.init(context: context, component: GiftAuctionAcquiredScreenComponent(
context: context,
gift: gift,
acquiredGifts: acquiredGifts
), 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? GiftAuctionAcquiredScreenComponent.View {
componentView.animateIn()
}
}
}
override public func dismiss(completion: (() -> Void)? = nil) {
if !self.isDismissed {
self.isDismissed = true
if let componentView = self.node.hostView.componentView as? GiftAuctionAcquiredScreenComponent.View {
componentView.animateOut(completion: { [weak self] in
completion?()
self?.dismiss(animated: false)
})
} else {
self.dismiss(animated: false)
}
}
}
}
@@ -0,0 +1,804 @@
import Foundation
import UIKit
import AsyncDisplayKit
import TelegramPresentationData
import ComponentFlow
import AccountContext
import ViewControllerComponent
import TelegramCore
import SwiftSignalKit
import Display
import MultilineTextComponent
import MultilineTextWithEntitiesComponent
import ButtonComponent
import PlainButtonComponent
import Markdown
import BundleIconComponent
import TextFormat
import TelegramStringFormatting
import GlassBarButtonComponent
import GiftItemComponent
import EdgeEffect
import AnimatedTextComponent
private final class GiftAuctionActiveBidsScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
init(
context: AccountContext
) {
self.context = context
}
static func ==(lhs: GiftAuctionActiveBidsScreenComponent, rhs: GiftAuctionActiveBidsScreenComponent) -> 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)
}
}
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 topEdgeEffectView: EdgeEffectView
private let backgroundHandleView: UIImageView
private let closeButton = ComponentView<Empty>()
private let title = ComponentView<Empty>()
private var itemsViews: [Int64: ComponentView<Empty>] = [:]
private var auctionStates: [GiftAuctionContext.State] = []
private var auctionStatesDisposable: Disposable?
private var ignoreScrolling: Bool = false
private var giftAuctionTimer: SwiftSignalKit.Timer?
private var component: GiftAuctionActiveBidsScreenComponent?
private weak var state: EmptyComponentState?
private var isUpdating: Bool = false
private var environment: ViewControllerComponentContainer.Environment?
private var itemLayout: ItemLayout?
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.topEdgeEffectView = EdgeEffectView()
self.topEdgeEffectView.clipsToBounds = true
self.topEdgeEffectView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
self.topEdgeEffectView.layer.cornerRadius = 40.0
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(_:))))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.auctionStatesDisposable?.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 !self.backgroundLayer.frame.contains(point) {
return self.dimView
}
if let result = self.navigationBarContainer.hitTest(self.convert(point, to: self.navigationBarContainer), with: event) {
return result
}
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)
}
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)
}
func update(component: GiftAuctionActiveBidsScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
let environment = environment[ViewControllerComponentContainer.Environment.self].value
let themeUpdated = self.environment?.theme !== environment.theme
let resetScrolling = self.scrollView.bounds.width != availableSize.width
let fillingSize: CGFloat
if case .regular = environment.metrics.widthClass {
fillingSize = min(availableSize.width, 414.0) - environment.safeInsets.left * 2.0
} else {
fillingSize = min(availableSize.width, environment.deviceMetrics.screenSize.width) - environment.safeInsets.left * 2.0
}
let rawSideInset: CGFloat = floor((availableSize.width - fillingSize) * 0.5)
let sideInset: CGFloat = rawSideInset + 16.0
if self.component == nil, let giftAuctionsManager = component.context.giftAuctionsManager {
self.auctionStatesDisposable = (giftAuctionsManager.state
|> deliverOnMainQueue).start(next: { [weak self] auctionStates in
guard let self else {
return
}
self.auctionStates = auctionStates.filter { state in
if case .ongoing = state.auctionState {
return true
} else {
return false
}
}
self.state?.updated(transition: .immediate)
if self.auctionStates.isEmpty {
self.environment?.controller()?.dismiss()
}
})
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
let theme = environment.theme.withModalBlocksBackground()
if themeUpdated {
self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
self.backgroundLayer.backgroundColor = theme.list.blocksBackgroundColor.cgColor
}
transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize))
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
var contentHeight: CGFloat = 75.0
var validKeys: Set<Int64> = Set()
for auctionState in self.auctionStates {
let id = auctionState.gift.giftId
validKeys.insert(id)
let itemView: ComponentView<Empty>
if let current = self.itemsViews[id] {
itemView = current
} else {
itemView = ComponentView()
self.itemsViews[id] = itemView
}
let itemSize = itemView.update(
transition: transition,
component: AnyComponent(
ActiveAuctionComponent(
context: component.context,
theme: theme,
strings: environment.strings,
dateTimeFormat: environment.dateTimeFormat,
state: auctionState,
currentTime: currentTime,
action: { [weak self] in
guard let self, let component = self.component else {
return
}
if let giftAuctionsManager = component.context.giftAuctionsManager {
let _ = (giftAuctionsManager.auctionContext(for: .giftId(id))
|> deliverOnMainQueue).start(next: { [weak self] auction in
guard let self, let component = self.component, let auction, let controller = environment.controller(), let navigationController = controller.navigationController as? NavigationController else {
return
}
let bidController = component.context.sharedContext.makeGiftAuctionBidScreen(context: component.context, toPeerId: auction.currentBidPeerId ?? component.context.account.peerId, text: nil, entities: nil, hideName: false, auctionContext: auction, acquiredGifts: nil)
navigationController.pushViewController(bidController)
})
}
}
)
),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0)
)
let itemFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: itemSize)
if let view = itemView.view {
if view.superview == nil {
self.scrollContentView.addSubview(view)
}
view.frame = itemFrame
}
contentHeight += itemSize.height
contentHeight += 20.0
}
contentHeight -= 10.0
var removeKeys: [Int64] = []
for (id, item) in self.itemsViews {
if !validKeys.contains(id) {
removeKeys.append(id)
if let itemView = item.view {
transition.setAlpha(view: itemView, alpha: 0.0, completion: { _ in
itemView.removeFromSuperview()
})
}
}
}
for id in removeKeys {
self.itemsViews.removeValue(forKey: id)
}
if self.backgroundHandleView.image == nil {
self.backgroundHandleView.image = generateStretchableFilledCircleImage(diameter: 5.0, color: .white)?.withRenderingMode(.alwaysTemplate)
}
self.backgroundHandleView.tintColor = theme.list.itemPrimaryTextColor.withMultipliedAlpha(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: 44.0, height: 44.0),
backgroundColor: nil,
isDark: environment.theme.overallDarkAppearance,
state: .glass,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: theme.chat.inputPanel.panelControlColor
)
)),
action: { [weak self] _ in
guard let self else {
return
}
self.environment?.controller()?.dismiss()
}
)),
environment: {},
containerSize: CGSize(width: 44.0, height: 44.0)
)
let closeButtonFrame = CGRect(origin: CGPoint(x: rawSideInset + 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 containerInset: CGFloat = environment.statusBarHeight + 10.0
contentHeight += environment.safeInsets.bottom
let clippingY: CGFloat
let titleText: String = environment.strings.Gift_ActiveAuctions_Title(Int32(self.auctionStates.count))
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: titleText, font: Font.semibold(17.0), textColor: 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 = self.title.view {
if titleView.superview == nil {
self.navigationBarContainer.addSubview(titleView)
}
transition.setFrame(view: titleView, frame: titleFrame)
}
let initialContentHeight = contentHeight
let edgeEffectHeight: CGFloat = 80.0
let edgeEffectFrame = CGRect(origin: CGPoint(x: rawSideInset, y: 0.0), size: CGSize(width: fillingSize, height: edgeEffectHeight))
transition.setFrame(view: self.topEdgeEffectView, frame: edgeEffectFrame)
self.topEdgeEffectView.update(content: theme.actionSheet.opaqueItemBackgroundColor, blur: true, alpha: 1.0, rect: edgeEffectFrame, edge: .top, edgeSize: edgeEffectFrame.height, transition: transition)
if self.topEdgeEffectView.superview == nil {
self.navigationBarContainer.insertSubview(self.topEdgeEffectView, at: 0)
}
clippingY = availableSize.height
let topInset: CGFloat = max(0.0, availableSize.height - containerInset - initialContentHeight)
let scrollContentHeight = max(topInset + contentHeight + containerInset, availableSize.height - containerInset)
self.scrollContentClippingView.layer.cornerRadius = 38.0
self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, containerCornerRadius: environment.deviceMetrics.screenCornerRadius, bottomInset: environment.safeInsets.bottom, topInset: topInset)
transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + containerInset), size: CGSize(width: availableSize.width, height: contentHeight)))
transition.setPosition(layer: self.backgroundLayer, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0))
transition.setBounds(layer: self.backgroundLayer, bounds: CGRect(origin: CGPoint(), size: CGSize(width: fillingSize, height: availableSize.height)))
let scrollClippingFrame = CGRect(origin: CGPoint(x: 0.0, y: containerInset), size: CGSize(width: availableSize.width, height: clippingY - containerInset))
transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center)
transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size))
self.ignoreScrolling = true
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height)))
let contentSize = CGSize(width: availableSize.width, height: scrollContentHeight)
if contentSize != self.scrollView.contentSize {
self.scrollView.contentSize = contentSize
}
if resetScrolling {
self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: availableSize)
}
self.ignoreScrolling = false
self.updateScrolling(transition: transition)
transition.setPosition(view: self.containerView, position: CGRect(origin: CGPoint(), size: availableSize).center)
transition.setBounds(view: self.containerView, bounds: CGRect(origin: CGPoint(), size: availableSize))
if let controller = environment.controller(), !controller.automaticallyControlPresentationContextLayout {
let bottomInset: CGFloat = contentHeight - 12.0
let layout = ContainerViewLayout(
size: availableSize,
metrics: environment.metrics,
deviceMetrics: environment.deviceMetrics,
intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomInset, right: 0.0),
safeInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0),
additionalInsets: .zero,
statusBarHeight: environment.statusBarHeight,
inputHeight: nil,
inputHeightIsInteractivellyChanging: false,
inVoiceOver: false
)
controller.presentationContext.containerLayoutUpdated(layout, transition: transition.containedViewLayoutTransition)
}
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public class GiftAuctionActiveBidsScreen: ViewControllerComponentContainer {
private let context: AccountContext
private var didPlayAppearAnimation: Bool = false
private var isDismissed: Bool = false
public init(context: AccountContext) {
self.context = context
super.init(context: context, component: GiftAuctionActiveBidsScreenComponent(
context: context
), 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? GiftAuctionActiveBidsScreenComponent.View {
componentView.animateIn()
}
}
}
override public func dismiss(completion: (() -> Void)? = nil) {
if !self.isDismissed {
self.isDismissed = true
if let componentView = self.node.hostView.componentView as? GiftAuctionActiveBidsScreenComponent.View {
componentView.animateOut(completion: { [weak self] in
completion?()
self?.dismiss(animated: false)
})
} else {
self.dismiss(animated: false)
}
}
}
}
private final class ActiveAuctionComponent: Component {
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let dateTimeFormat: PresentationDateTimeFormat
let state: GiftAuctionContext.State
let currentTime: Int32
let action: () -> Void
init(
context: AccountContext,
theme: PresentationTheme,
strings: PresentationStrings,
dateTimeFormat: PresentationDateTimeFormat,
state: GiftAuctionContext.State,
currentTime: Int32,
action: @escaping () -> Void
) {
self.context = context
self.theme = theme
self.strings = strings
self.dateTimeFormat = dateTimeFormat
self.state = state
self.currentTime = currentTime
self.action = action
}
static func ==(lhs: ActiveAuctionComponent, rhs: ActiveAuctionComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.state != rhs.state {
return false
}
if lhs.currentTime != rhs.currentTime {
return false
}
return true
}
final class View: UIView {
private let icon = ComponentView<Empty>()
private let title = ComponentView<Empty>()
private let subtitle = ComponentView<Empty>()
private let button = ComponentView<Empty>()
private var component: ActiveAuctionComponent?
override init(frame: CGRect) {
super.init(frame: frame)
self.layer.cornerRadius = 26.0
self.clipsToBounds = true
}
required init(coder: NSCoder) {
preconditionFailure()
}
func update(component: ActiveAuctionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.backgroundColor = component.theme.list.itemBlocksBackgroundColor
var size = CGSize(width: availableSize.width, height: 0.0)
size.height += 11.0
if case let .generic(gift) = component.state.gift {
let titleSize = self.icon.update(
transition: .immediate,
component: AnyComponent(GiftItemComponent(
context: component.context,
theme: component.theme,
strings: component.strings,
subject: .starGift(gift: gift, price: ""),
mode: .preview
)),
environment: {},
containerSize: CGSize(width: 64.0, height: 64.0)
)
let iconFrame = CGRect(origin: CGPoint(x: 2.0, y: 0.0), size: titleSize)
if let iconView = self.icon.view {
if iconView.superview == nil {
self.addSubview(iconView)
}
iconView.frame = iconFrame
}
}
var endTime = component.currentTime
var titleText: String = ""
var subtitleText: String = ""
var subtitleTextColor = component.theme.list.itemPrimaryTextColor
if case let .ongoing(_, startDate, _, _, _, _, nextRoundDate, _, currentRound, totalRound, _, _) = component.state.auctionState, let myBid = component.state.myState.bidAmount {
titleText = component.strings.Gift_ActiveAuctions_Round("\(currentRound)", "\(totalRound)").string
let bidString = "#\(presentationStringsFormattedNumber(Int32(clamping: myBid), component.dateTimeFormat.groupingSeparator))"
if component.currentTime < startDate {
subtitleText = component.strings.Gift_ActiveAuctions_UpcomingBid
} else if let place = component.state.place, case let .generic(gift) = component.state.gift, let auctionGiftsPerRound = gift.auctionGiftsPerRound, place > auctionGiftsPerRound {
subtitleText = component.strings.Gift_ActiveAuctions_Outbid(bidString).string
subtitleTextColor = component.theme.list.itemDestructiveColor
} else {
subtitleText = component.strings.Gift_ActiveAuctions_Winning(bidString, "\(component.state.place ?? 0)").string
}
endTime = nextRoundDate
}
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: titleText, font: Font.semibold(17.0), textColor: component.theme.list.itemPrimaryTextColor)),
maximumNumberOfLines: 2
)),
environment: {},
containerSize: CGSize(width: availableSize.width - 63.0 - 20.0, height: 100.0)
)
let titleFrame = CGRect(origin: CGPoint(x: 63.0, y: size.height), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
self.addSubview(titleView)
}
titleView.frame = titleFrame
}
size.height += titleSize.height
size.height += 2.0
let textFont = Font.regular(15.0)
let boldTextFont = Font.semibold(15.0)
let textColor = subtitleTextColor
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: textColor), linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
})
let attributedString = parseMarkdownIntoAttributedString(subtitleText, attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString
if let range = attributedString.string.range(of: "#") {
attributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: attributedString.string))
attributedString.addAttribute(.baselineOffset, value: 2.0, range: NSRange(range, in: attributedString.string))
}
let subtitleSize = self.subtitle.update(
transition: .immediate,
component: AnyComponent(MultilineTextWithEntitiesComponent(context: component.context, animationCache: component.context.animationCache, animationRenderer: component.context.animationRenderer, placeholderColor: .clear, text: .plain(attributedString), maximumNumberOfLines: 2
)),
environment: {},
containerSize: CGSize(width: availableSize.width - 63.0 - 20.0, height: 100.0)
)
let subtitleFrame = CGRect(origin: CGPoint(x: 63.0, y: size.height), size: subtitleSize)
if let subtitleView = self.subtitle.view {
if subtitleView.superview == nil {
self.addSubview(subtitleView)
}
subtitleView.frame = subtitleFrame
}
size.height += subtitleSize.height
size.height += 19.0
let endTimeout = max(0, endTime - component.currentTime)
let hours = Int(endTimeout / 3600)
let minutes = Int((endTimeout % 3600) / 60)
let seconds = Int(endTimeout % 60)
var buttonAnimatedTitleItems: [AnimatedTextComponent.Item] = []
if hours > 0 {
buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "h", content: .number(hours, minDigits: 1)))
buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "colon1", content: .text(":")))
buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "m", content: .number(minutes, minDigits: 2)))
buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "colon2", content: .text(":")))
buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "s", content: .number(seconds, minDigits: 2)))
} else {
buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "m", content: .number(minutes, minDigits: 2)))
buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "colon2", content: .text(":")))
buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "s", content: .number(seconds, minDigits: 2)))
}
let buttonSize = self.button.update(
transition: .spring(duration: 0.2),
component: AnyComponent(
ButtonComponent(
background: ButtonComponent.Background(
style: .glass,
color: component.theme.list.itemCheckColors.fillColor,
foreground: component.theme.list.itemCheckColors.foregroundColor,
pressedColor: component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
),
content: AnyComponentWithIdentity(id: "label", component: AnyComponent(
HStack([
AnyComponentWithIdentity(id: "icon", component: AnyComponent(
BundleIconComponent(name: "Premium/Auction/BidSmall", tintColor: component.theme.list.itemCheckColors.foregroundColor)
)),
AnyComponentWithIdentity(id: "label", component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: component.strings.Gift_ActiveAuctions_RaiseBid, font: Font.semibold(17.0), textColor: component.theme.list.itemCheckColors.foregroundColor)))
)),
AnyComponentWithIdentity(id: "timer", component: AnyComponent(
AnimatedTextComponent(
font: Font.with(size: 17.0, weight: .medium, traits: .monospacedNumbers),
color: component.theme.list.itemCheckColors.foregroundColor.withAlphaComponent(0.7),
items: buttonAnimatedTitleItems,
noDelay: true
)
))
], spacing: 5.0)
)),
action: {
component.action()
}
)
),
environment: {},
containerSize: CGSize(width: availableSize.width - 32.0, height: 52.0)
)
let buttonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - buttonSize.width) / 2.0), y: size.height), size: buttonSize)
if let buttonView = self.button.view {
if buttonView.superview == nil {
self.addSubview(buttonView)
}
buttonView.frame = buttonFrame
}
size.height += buttonSize.height
size.height += 16.0
return size
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,294 @@
import Foundation
import UIKit
import SwiftSignalKit
import AsyncDisplayKit
import Display
import TelegramCore
import TelegramPresentationData
import AccountContext
import UrlEscaping
import ComponentFlow
import AlertComponent
import StarsWithdrawalScreen
func giftAuctionCustomBidController(
context: AccountContext,
title: String,
text: String,
placeholder: String,
action: String,
minValue: Int64,
value: Int64,
apply: @escaping (Int64) -> Void,
cancel: @escaping () -> Void
) -> ViewController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let strings = presentationData.strings
let inputState = AlertAmountFieldComponent.ExternalState()
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
content.append(AnyComponentWithIdentity(
id: "title",
component: AnyComponent(
AlertTitleComponent(title: title)
)
))
content.append(AnyComponentWithIdentity(
id: "text",
component: AnyComponent(
AlertTextComponent(content: .plain(text))
)
))
var applyImpl: (() -> Void)?
content.append(AnyComponentWithIdentity(
id: "input",
component: AnyComponent(
AlertAmountFieldComponent(
context: context,
initialValue: value,
minValue: minValue,
maxValue: nil,
placeholder: placeholder,
isInitiallyFocused: true,
externalState: inputState,
returnKeyAction: {
applyImpl?()
}
)
)
))
let alertController = AlertScreen(
context: context,
configuration: AlertScreen.Configuration(allowInputInset: true),
content: content,
actions: [
.init(title: strings.Common_Cancel, action: {
cancel()
}),
.init(title: action, type: .default, action: {
applyImpl?()
}, autoDismiss: false)
]
)
applyImpl = {
if let value = inputState.value, value >= minValue {
apply(value)
} else {
inputState.resetToMinValue()
inputState.animateError()
}
}
return alertController
}
private final class AlertAmountFieldComponent: Component {
public typealias EnvironmentType = AlertComponentEnvironment
public class ExternalState {
public fileprivate(set) var value: Int64?
public fileprivate(set) var animateError: () -> Void = {}
public fileprivate(set) var activateInput: () -> Void = {}
public fileprivate(set) var resetToMinValue: () -> Void = {}
fileprivate let valuePromise = ValuePromise<Int64?>(nil)
public var valueSignal: Signal<Int64?, NoError> {
return self.valuePromise.get()
}
public init() {
}
}
let context: AccountContext
let initialValue: Int64?
let minValue: Int64?
let maxValue: Int64?
let placeholder: String
let isInitiallyFocused: Bool
let externalState: ExternalState
let returnKeyAction: (() -> Void)?
public init(
context: AccountContext,
initialValue: Int64? = nil,
minValue: Int64? = nil,
maxValue: Int64? = nil,
placeholder: String,
isInitiallyFocused: Bool = false,
externalState: ExternalState,
returnKeyAction: (() -> Void)? = nil
) {
self.context = context
self.initialValue = initialValue
self.minValue = minValue
self.maxValue = maxValue
self.placeholder = placeholder
self.isInitiallyFocused = isInitiallyFocused
self.externalState = externalState
self.returnKeyAction = returnKeyAction
}
public static func ==(lhs: AlertAmountFieldComponent, rhs: AlertAmountFieldComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.initialValue != rhs.initialValue {
return false
}
if lhs.minValue != rhs.minValue {
return false
}
if lhs.maxValue != rhs.maxValue {
return false
}
if lhs.placeholder != rhs.placeholder {
return false
}
if lhs.isInitiallyFocused != rhs.isInitiallyFocused {
return false
}
return true
}
public final class View: UIView, UITextFieldDelegate {
private let background = ComponentView<Empty>()
private let amountField = ComponentView<Empty>()
private var currentValue: Int64?
private var component: AlertAmountFieldComponent?
private weak var state: EmptyComponentState?
private var isUpdating = false
func activateInput() {
if let amountFieldView = self.amountField.view as? AmountFieldComponent.View {
amountFieldView.activateInput()
}
}
func resetToMinValue() {
self.currentValue = self.component?.minValue
self.state?.updated()
if let amountFieldView = self.amountField.view as? AmountFieldComponent.View {
amountFieldView.resetValue()
amountFieldView.selectAll()
}
}
func animateError() {
if let amountFieldView = self.amountField.view as? AmountFieldComponent.View {
amountFieldView.animateError()
}
}
func update(component: AlertAmountFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
if self.component == nil {
self.currentValue = component.initialValue
component.externalState.animateError = { [weak self] in
self?.animateError()
}
component.externalState.activateInput = { [weak self] in
self?.activateInput()
}
component.externalState.resetToMinValue = { [weak self] in
self?.resetToMinValue()
}
}
let isFirstTime = self.component == nil
self.component = component
self.state = state
let environment = environment[AlertComponentEnvironment.self]
let topInset: CGFloat = 15.0
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let amountFieldSize = self.amountField.update(
transition: .immediate,
component: AnyComponent(
AmountFieldComponent(
textColor: environment.theme.actionSheet.primaryTextColor,
secondaryColor: environment.theme.actionSheet.secondaryTextColor,
placeholderColor: environment.theme.actionSheet.inputPlaceholderColor,
accentColor: environment.theme.actionSheet.controlAccentColor,
value: self.currentValue,
minValue: component.minValue,
forceMinValue: false,
allowZero: false,
maxValue: nil,
placeholderText: component.placeholder,
textFieldOffset: CGPoint(x: -4.0, y: -1.0),
labelText: nil,
currency: .stars,
dateTimeFormat: presentationData.dateTimeFormat,
amountUpdated: { [weak self] value in
guard let self else {
return
}
self.currentValue = value
component.externalState.value = value
component.externalState.valuePromise.set(value)
},
tag: nil
)
),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 44.0)
)
var amountFieldFrame = CGRect(origin: CGPoint(x: -16.0, y: topInset - 1.0 + UIScreenPixel), size: amountFieldSize)
if let amountFieldView = self.amountField.view {
if amountFieldView.superview == nil {
amountFieldView.clipsToBounds = true
self.addSubview(amountFieldView)
}
amountFieldFrame.size.width -= 14.0
amountFieldView.frame = amountFieldFrame
}
let backgroundPadding: CGFloat = 14.0
let size = CGSize(width: availableSize.width, height: 50.0)
let backgroundSize = self.background.update(
transition: transition,
component: AnyComponent(
FilledRoundedRectangleComponent(color: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.1), cornerRadius: .value(25.0), smoothCorners: false)
),
environment: {},
containerSize: CGSize(width: size.width + backgroundPadding * 2.0, height: size.height)
)
let backgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - backgroundSize.width) / 2.0), y: topInset ), size: backgroundSize)
if let backgroundView = self.background.view {
if backgroundView.superview == nil {
self.addSubview(backgroundView)
}
transition.setFrame(view: backgroundView, frame: backgroundFrame)
}
if isFirstTime && component.isInitiallyFocused {
self.activateInput()
}
return CGSize(width: availableSize.width, height: size.height + topInset)
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,649 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SwiftSignalKit
import AccountContext
import TelegramPresentationData
import PresentationDataUtils
import ComponentFlow
import ViewControllerComponent
import SheetComponent
import MultilineTextComponent
import BalancedTextComponent
import BundleIconComponent
import Markdown
import TextFormat
import TelegramStringFormatting
import GlassBarButtonComponent
import ButtonComponent
import LottieComponent
private final class GiftAuctionInfoSheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let gift: StarGift
let auctionContext: GiftAuctionContext
let animateOut: ActionSlot<Action<()>>
let getController: () -> ViewController?
init(
context: AccountContext,
gift: StarGift,
auctionContext: GiftAuctionContext,
animateOut: ActionSlot<Action<()>>,
getController: @escaping () -> ViewController?
) {
self.context = context
self.gift = gift
self.auctionContext = auctionContext
self.animateOut = animateOut
self.getController = getController
}
static func ==(lhs: GiftAuctionInfoSheetContent, rhs: GiftAuctionInfoSheetContent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.gift != rhs.gift {
return false
}
return true
}
final class State: ComponentState {
private let context: AccountContext
private let auctionContext: GiftAuctionContext
private let animateOut: ActionSlot<Action<()>>
private let getController: () -> ViewController?
private(set) var rounds: Int32 = 50
fileprivate let playButtonAnimation = ActionSlot<Void>()
private var didPlayAnimation = false
init(
context: AccountContext,
auctionContext: GiftAuctionContext,
animateOut: ActionSlot<Action<()>>,
getController: @escaping () -> ViewController?
) {
self.context = context
self.auctionContext = auctionContext
self.animateOut = animateOut
self.getController = getController
super.init()
let _ = (self.auctionContext.state
|> deliverOnMainQueue).startStandalone(next: { [weak self] state in
if let self, case let .ongoing(_, _, _, _, _, _, _, _, _, totalRounds, _, _) = state?.auctionState {
self.rounds = totalRounds
self.updated()
}
})
}
func playAnimationIfNeeded() {
if !self.didPlayAnimation {
self.didPlayAnimation = true
self.playButtonAnimation.invoke(Void())
}
}
func dismiss(animated: Bool) {
guard let controller = self.getController() as? GiftAuctionInfoScreen else {
return
}
if animated {
self.animateOut.invoke(Action { [weak controller] _ in
controller?.dismiss(completion: nil)
})
} else {
controller.dismiss(animated: false)
}
}
}
func makeState() -> State {
return State(context: self.context, auctionContext: self.auctionContext, animateOut: self.animateOut, getController: self.getController)
}
static var body: Body {
let closeButton = Child(GlassBarButtonComponent.self)
let icon = Child(BundleIconComponent.self)
let title = Child(BalancedTextComponent.self)
let text = Child(BalancedTextComponent.self)
let list = Child(List<Empty>.self)
let button = Child(ButtonComponent.self)
return { context in
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
let state = context.state
let controller = environment.controller
let theme = environment.theme
let strings = environment.strings
let sideInset: CGFloat = 30.0 + environment.safeInsets.left
let textSideInset: CGFloat = 30.0 + environment.safeInsets.left
let titleFont = Font.bold(24.0)
let textFont = Font.regular(15.0)
let textColor = theme.actionSheet.primaryTextColor
let secondaryTextColor = theme.actionSheet.secondaryTextColor
let linkColor = theme.actionSheet.controlAccentColor
let spacing: CGFloat = 16.0
var contentSize = CGSize(width: context.availableSize.width, height: 33.0)
var auctionGiftsPerRound: Int32 = 50
if case let .generic(gift) = context.component.gift, let auctionGiftsPerRoundValue = gift.auctionGiftsPerRound {
auctionGiftsPerRound = auctionGiftsPerRoundValue
}
let icon = icon.update(
component: BundleIconComponent(
name: "Premium/Auction/BidLarge",
tintColor: linkColor
),
availableSize: context.availableSize,
transition: context.transition
)
context.add(icon
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + icon.size.height / 2.0))
)
contentSize.height += icon.size.height
contentSize.height += 8.0
let title = title.update(
component: BalancedTextComponent(
text: .plain(NSAttributedString(string: strings.Gift_Auction_Info_Title, font: titleFont, textColor: textColor)),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.1
),
availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height),
transition: .immediate
)
context.add(title
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height / 2.0))
)
contentSize.height += title.size.height
contentSize.height += spacing - 8.0
let text = text.update(
component: BalancedTextComponent(
text: .plain(NSAttributedString(string: strings.Gift_Auction_Info_Description, font: textFont, textColor: secondaryTextColor)),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.2
),
availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height),
transition: .immediate
)
context.add(text
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + text.size.height / 2.0))
)
contentSize.height += text.size.height
contentSize.height += spacing + 9.0
var items: [AnyComponentWithIdentity<Empty>] = []
items.append(
AnyComponentWithIdentity(
id: "top",
component: AnyComponent(ParagraphComponent(
title: strings.Gift_Auction_Info_TopBidders_Title(auctionGiftsPerRound),
titleColor: textColor,
text: strings.Gift_Auction_Info_TopBidders_Text(strings.Gift_Auction_Info_TopBidders_Gifts(auctionGiftsPerRound), strings.Gift_Auction_Info_TopBidders_Rounds(state.rounds), strings.Gift_Auction_Info_TopBidders_Bidders(auctionGiftsPerRound)).string,
textColor: secondaryTextColor,
accentColor: linkColor,
iconName: "Premium/Auction/Drop",
iconColor: linkColor
))
)
)
items.append(
AnyComponentWithIdentity(
id: "carryover",
component: AnyComponent(ParagraphComponent(
title: strings.Gift_Auction_Info_Carryover_Title,
titleColor: textColor,
text: strings.Gift_Auction_Info_Carryover_Text("\(auctionGiftsPerRound)").string,
textColor: secondaryTextColor,
accentColor: linkColor,
iconName: "Premium/Auction/NextDrop",
iconColor: linkColor
))
)
)
items.append(
AnyComponentWithIdentity(
id: "missed",
component: AnyComponent(ParagraphComponent(
title: strings.Gift_Auction_Info_Missed_Title,
titleColor: textColor,
text: strings.Gift_Auction_Info_Missed_Text,
textColor: secondaryTextColor,
accentColor: linkColor,
iconName: "Premium/Auction/Refund",
iconColor: linkColor
))
)
)
let list = list.update(
component: List(items),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 10000.0),
transition: context.transition
)
context.add(list
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + list.size.height / 2.0))
)
contentSize.height += list.size.height
contentSize.height += spacing + 8.0
let closeButton = closeButton.update(
component: GlassBarButtonComponent(
size: CGSize(width: 44.0, height: 44.0),
backgroundColor: nil,
isDark: theme.overallDarkAppearance,
state: .glass,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: theme.chat.inputPanel.panelControlColor
)
)),
action: { [weak state] _ in
guard let state else {
return
}
state.dismiss(animated: true)
}
),
availableSize: CGSize(width: 44.0, height: 44.0),
transition: .immediate
)
context.add(closeButton
.position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0))
)
var buttonTitle: [AnyComponentWithIdentity<Empty>] = []
buttonTitle.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(name: "anim_ok"),
color: theme.list.itemCheckColors.foregroundColor,
startingPosition: .begin,
size: CGSize(width: 28.0, height: 28.0),
playOnce: state.playButtonAnimation
))))
buttonTitle.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(ButtonTextContentComponent(
text: strings.Gift_Auction_Info_Understood,
badge: 0,
textColor: theme.list.itemCheckColors.foregroundColor,
badgeBackground: theme.list.itemCheckColors.foregroundColor,
badgeForeground: theme.list.itemCheckColors.fillColor
))))
let button = button.update(
component: ButtonComponent(
background: ButtonComponent.Background(
style: .glass,
color: theme.list.itemCheckColors.fillColor,
foreground: theme.list.itemCheckColors.foregroundColor,
pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
),
content: AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(HStack(buttonTitle, spacing: 2.0))
),
isEnabled: true,
displaysProgress: false,
action: { [weak state] in
guard let state else {
return
}
state.dismiss(animated: true)
if let controller = controller() as? GiftAuctionInfoScreen {
controller.completion?()
}
}
),
availableSize: CGSize(width: context.availableSize.width - 30.0 * 2.0, height: 52.0),
transition: .immediate
)
context.add(button
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + button.size.height / 2.0))
)
contentSize.height += button.size.height
contentSize.height += 30.0
state.playAnimationIfNeeded()
return contentSize
}
}
}
final class GiftAuctionInfoSheetComponent: CombinedComponent {
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: GiftAuctionInfoSheetComponent, rhs: GiftAuctionInfoSheetComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.gift != rhs.gift {
return false
}
return true
}
static var body: Body {
let sheet = Child(SheetComponent<EnvironmentType>.self)
let animateOut = StoredActionSlot(Action<Void>.self)
let sheetExternalState = SheetComponent<EnvironmentType>.ExternalState()
return { context in
let environment = context.environment[EnvironmentType.self]
let controller = environment.controller
let sheet = sheet.update(
component: SheetComponent<EnvironmentType>(
content: AnyComponent<EnvironmentType>(GiftAuctionInfoSheetContent(
context: context.component.context,
gift: context.component.gift,
auctionContext: context.component.auctionContext,
animateOut: animateOut,
getController: controller
)),
style: .glass,
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
followContentSizeChanges: true,
clipsContent: true,
autoAnimateOut: false,
externalState: sheetExternalState,
animateOut: animateOut,
onPan: {
},
willDismiss: {
}
),
environment: {
environment
SheetComponentEnvironment(
metrics: environment.metrics,
deviceMetrics: environment.deviceMetrics,
isDisplaying: environment.value.isVisible,
isCentered: environment.metrics.widthClass == .regular,
hasInputHeight: !environment.inputHeight.isZero,
regularMetricsSize: CGSize(width: 430.0, height: 900.0),
dismiss: { animated in
if animated {
if let controller = controller() as? GiftAuctionInfoScreen {
animateOut.invoke(Action { _ in
controller.dismiss(completion: nil)
})
}
} else {
if let controller = controller() as? GiftAuctionInfoScreen {
controller.dismiss(completion: nil)
}
}
}
)
},
availableSize: context.availableSize,
transition: context.transition
)
context.add(sheet
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
)
if let controller = controller(), !controller.automaticallyControlPresentationContextLayout {
var sideInset: CGFloat = 0.0
var bottomInset: CGFloat = max(environment.safeInsets.bottom, sheetExternalState.contentHeight)
if case .regular = environment.metrics.widthClass {
sideInset = floor((context.availableSize.width - 430.0) / 2.0) - 12.0
bottomInset = (context.availableSize.height - sheetExternalState.contentHeight) / 2.0 + sheetExternalState.contentHeight
}
let layout = ContainerViewLayout(
size: context.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: max(sideInset, environment.safeInsets.left), bottom: 0.0, right: max(sideInset, environment.safeInsets.right)),
additionalInsets: .zero,
statusBarHeight: environment.statusBarHeight,
inputHeight: nil,
inputHeightIsInteractivellyChanging: false,
inVoiceOver: false
)
controller.presentationContext.containerLayoutUpdated(layout, transition: context.transition.containedViewLayoutTransition)
}
return context.availableSize
}
}
}
public final class GiftAuctionInfoScreen: ViewControllerComponentContainer {
private let context: AccountContext
fileprivate let completion: (() -> Void)?
public init(
context: AccountContext,
auctionContext: GiftAuctionContext,
completion: (() -> Void)?
) {
self.context = context
self.completion = completion
super.init(
context: context,
component: GiftAuctionInfoSheetComponent(
context: context,
gift: auctionContext.gift,
auctionContext: auctionContext
),
navigationBarAppearance: .none,
statusBarStyle: .ignore,
theme: .default
)
self.navigationPresentation = .flatModal
self.automaticallyControlPresentationContextLayout = false
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
public override func viewDidLoad() {
super.viewDidLoad()
self.view.disablesInteractiveModalDismiss = true
}
public override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
}
public func dismissAnimated() {
if let view = self.node.hostView.findTaggedView(tag: SheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? SheetComponent<ViewControllerComponentContainer.Environment>.View {
view.dismissAnimated()
}
}
}
private final class ParagraphComponent: CombinedComponent {
let title: String
let titleColor: UIColor
let text: String
let textColor: UIColor
let accentColor: UIColor
let iconName: String
let iconColor: UIColor
let action: () -> Void
public init(
title: String,
titleColor: UIColor,
text: String,
textColor: UIColor,
accentColor: UIColor,
iconName: String,
iconColor: UIColor,
action: @escaping () -> Void = {}
) {
self.title = title
self.titleColor = titleColor
self.text = text
self.textColor = textColor
self.accentColor = accentColor
self.iconName = iconName
self.iconColor = iconColor
self.action = action
}
static func ==(lhs: ParagraphComponent, rhs: ParagraphComponent) -> Bool {
if lhs.title != rhs.title {
return false
}
if lhs.titleColor != rhs.titleColor {
return false
}
if lhs.text != rhs.text {
return false
}
if lhs.textColor != rhs.textColor {
return false
}
if lhs.accentColor != rhs.accentColor {
return false
}
if lhs.iconName != rhs.iconName {
return false
}
if lhs.iconColor != rhs.iconColor {
return false
}
return true
}
static var body: Body {
let title = Child(MultilineTextComponent.self)
let text = Child(MultilineTextComponent.self)
let icon = Child(BundleIconComponent.self)
return { context in
let component = context.component
let leftInset: CGFloat = 32.0
let rightInset: CGFloat = 24.0
let textSideInset: CGFloat = leftInset + 8.0
let spacing: CGFloat = 5.0
let textTopInset: CGFloat = 9.0
let title = title.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(
string: component.title,
font: Font.semibold(15.0),
textColor: component.titleColor,
paragraphAlignment: .natural
)),
horizontalAlignment: .center,
maximumNumberOfLines: 1
),
availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
let textFont = Font.regular(15.0)
let boldTextFont = Font.semibold(15.0)
let textColor = component.textColor
let accentColor = component.accentColor
let markdownAttributes = MarkdownAttributes(
body: MarkdownAttributeSet(font: textFont, textColor: textColor),
bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor),
link: MarkdownAttributeSet(font: textFont, textColor: accentColor),
linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
}
)
let text = text.update(
component: MultilineTextComponent(
text: .markdown(text: component.text, attributes: markdownAttributes),
horizontalAlignment: .natural,
maximumNumberOfLines: 0,
lineSpacing: 0.2,
highlightColor: accentColor.withAlphaComponent(0.1),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
} else {
return nil
}
},
tapAction: { _, _ in
component.action()
}
),
availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: context.availableSize.height),
transition: .immediate
)
let icon = icon.update(
component: BundleIconComponent(
name: component.iconName,
tintColor: component.iconColor
),
availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height),
transition: .immediate
)
context.add(title
.position(CGPoint(x: textSideInset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0))
)
context.add(text
.position(CGPoint(x: textSideInset + text.size.width / 2.0, y: textTopInset + title.size.height + spacing + text.size.height / 2.0))
)
context.add(icon
.position(CGPoint(x: 15.0, y: textTopInset + 18.0))
)
return CGSize(width: context.availableSize.width, height: textTopInset + title.size.height + text.size.height + 20.0)
}
}
}
@@ -0,0 +1,788 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SwiftSignalKit
import AccountContext
import TelegramPresentationData
import PresentationDataUtils
import ComponentFlow
import ViewControllerComponent
import SheetComponent
import MultilineTextComponent
import MultilineTextWithEntitiesComponent
import BundleIconComponent
import ButtonComponent
import Markdown
import BalancedTextComponent
import AvatarNode
import TextFormat
import TelegramStringFormatting
import PlainButtonComponent
import GiftItemComponent
import GiftAnimationComponent
import GlassBarButtonComponent
import GiftRemainingCountComponent
import AnimatedTextComponent
import AvatarComponent
private final class GiftAuctionWearPreviewSheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let auctionContext: GiftAuctionContext
let attributes: [StarGift.UniqueGift.Attribute]
let animateOut: ActionSlot<Action<()>>
let getController: () -> ViewController?
init(
context: AccountContext,
auctionContext: GiftAuctionContext,
attributes: [StarGift.UniqueGift.Attribute],
animateOut: ActionSlot<Action<()>>,
getController: @escaping () -> ViewController?
) {
self.context = context
self.auctionContext = auctionContext
self.attributes = attributes
self.animateOut = animateOut
self.getController = getController
}
static func ==(lhs: GiftAuctionWearPreviewSheetContent, rhs: GiftAuctionWearPreviewSheetContent) -> Bool {
if lhs.context !== rhs.context {
return false
}
return true
}
final class State: ComponentState {
private let context: AccountContext
private var peerDisposable: Disposable?
var peerMap: [EnginePeer.Id: EnginePeer] = [:]
var cachedSmallChevronImage: (UIImage, PresentationTheme)?
private var disposable: Disposable?
private(set) var giftAuctionState: GiftAuctionContext.State?
private var giftAuctionTimer: SwiftSignalKit.Timer?
private var previewTimer: SwiftSignalKit.Timer?
private(set) var previewModelIndex: Int = 0
private(set) var previewBackdropIndex: Int = 0
private(set) var previewSymbolIndex: Int = 0
private(set) var previewModels: [StarGift.UniqueGift.Attribute] = []
private(set) var previewBackdrops: [StarGift.UniqueGift.Attribute] = []
private(set) var previewSymbols: [StarGift.UniqueGift.Attribute] = []
init(
context: AccountContext,
auctionContext: GiftAuctionContext,
attributes: [StarGift.UniqueGift.Attribute]
) {
self.context = context
super.init()
for attribute in attributes {
switch attribute {
case .model:
self.previewModels.append(attribute)
case .backdrop:
self.previewBackdrops.append(attribute)
case .pattern:
self.previewSymbols.append(attribute)
default:
break
}
}
let peerIds: [EnginePeer.Id] = [context.account.peerId]
self.peerDisposable = (
context.engine.data.get(EngineDataMap(
peerIds.map { peerId -> TelegramEngine.EngineData.Item.Peer.Peer in
return TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)
}
))
|> deliverOnMainQueue
).startStrict(next: { [weak self] peers in
if let strongSelf = self {
var peersMap: [EnginePeer.Id: EnginePeer] = [:]
for (peerId, maybePeer) in peers {
if let peer = maybePeer {
peersMap[peerId] = peer
}
}
strongSelf.peerMap = peersMap
strongSelf.updated(transition: .immediate)
}
})
self.previewTimer = SwiftSignalKit.Timer(timeout: 3.0, repeat: true, completion: { [weak self] in
guard let self else {
return
}
self.previewTimerTick()
}, queue: Queue.mainQueue())
self.previewTimer?.start()
self.disposable = (auctionContext.state
|> deliverOnMainQueue).start(next: { [weak self] auctionState in
guard let self else {
return
}
self.giftAuctionState = auctionState
self.updated()
})
self.giftAuctionTimer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in
self?.updated()
}, queue: Queue.mainQueue())
self.giftAuctionTimer?.start()
}
deinit {
self.disposable?.dispose()
self.giftAuctionTimer?.invalidate()
self.peerDisposable?.dispose()
self.previewTimer?.invalidate()
}
private func previewTimerTick() {
guard !self.previewModels.isEmpty else { return }
self.previewModelIndex = (self.previewModelIndex + 1) % self.previewModels.count
let previousSymbolIndex = self.previewSymbolIndex
var randomSymbolIndex = previousSymbolIndex
while randomSymbolIndex == previousSymbolIndex && !self.previewSymbols.isEmpty {
randomSymbolIndex = Int.random(in: 0 ..< self.previewSymbols.count)
}
if !self.previewSymbols.isEmpty { self.previewSymbolIndex = randomSymbolIndex }
let previousBackdropIndex = self.previewBackdropIndex
var randomBackdropIndex = previousBackdropIndex
while randomBackdropIndex == previousBackdropIndex && !self.previewBackdrops.isEmpty {
randomBackdropIndex = Int.random(in: 0 ..< self.previewBackdrops.count)
}
if !self.previewBackdrops.isEmpty { self.previewBackdropIndex = randomBackdropIndex }
self.updated(transition: .easeInOut(duration: 0.25))
}
}
func makeState() -> State {
return State(context: self.context, auctionContext: self.auctionContext, attributes: self.attributes)
}
static var body: Body {
let closeButton = Child(GlassBarButtonComponent.self)
let animation = Child(GiftCompositionComponent.self)
let avatar = Child(AvatarComponent.self)
let peerName = Child(MultilineTextComponent.self)
let learnMore = Child(PlainButtonComponent.self)
let initialGift = Child(GiftItemComponent.self)
let upgradedGift = Child(GiftItemComponent.self)
let arrow = Child(BundleIconComponent.self)
let upgradeLabel = Child(MultilineTextComponent.self)
let remainingCount = Child(GiftRemainingCountComponent.self)
let auctionFooter = Child(MultilineTextComponent.self)
let button = Child(ButtonComponent.self)
let giftCompositionExternalState = GiftCompositionComponent.ExternalState()
return { context in
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
let component = context.component
let theme = environment.theme.withModalBlocksBackground()
let strings = environment.strings
let nameDisplayOrder = component.context.sharedContext.currentPresentationData.with { $0 }.nameDisplayOrder
let controller = environment.controller
let state = context.state
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
var contentHeight: CGFloat = 0.0
let headerHeight: CGFloat = 226.0
var peerNameString = ""
if let peer = state.peerMap[component.context.account.peerId] {
peerNameString = peer.displayTitle(strings: strings, displayOrder: nameDisplayOrder)
}
let peerName = peerName.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(
string: peerNameString,
font: Font.bold(20.0),
textColor: .white,
paragraphAlignment: .center
)),
horizontalAlignment: .center,
maximumNumberOfLines: 1
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
let animationOffset: CGPoint? = CGPoint(x: peerName.size.width / 2.0 + 20.0 - 12.0, y: 79.0)
let animationScale: CGFloat = 0.19
var attributes: [StarGift.UniqueGift.Attribute] = []
if !state.previewModels.isEmpty {
attributes.append(state.previewModels[state.previewModelIndex])
if !state.previewBackdrops.isEmpty {
attributes.append(state.previewBackdrops[state.previewBackdropIndex])
}
if !state.previewSymbols.isEmpty {
attributes.append(state.previewSymbols[state.previewSymbolIndex])
}
}
let animation = animation.update(
component: GiftCompositionComponent(
context: component.context,
theme: environment.theme,
subject: .preview(attributes),
animationOffset: animationOffset,
animationScale: animationScale,
displayAnimationStars: true,
animateScaleOnTransition: false,
externalState: giftCompositionExternalState,
requestUpdate: { [weak state] transition in
state?.updated(transition: transition)
}
),
availableSize: CGSize(width: context.availableSize.width, height: headerHeight),
transition: context.transition
)
context.add(animation
.position(CGPoint(x: context.availableSize.width / 2.0, y: headerHeight / 2.0))
)
contentHeight += headerHeight
if let peer = state.peerMap[component.context.account.peerId] {
let avatar = avatar.update(
component: AvatarComponent(
context: component.context,
theme: theme,
peer: peer
),
environment: {},
availableSize: CGSize(width: 100.0, height: 100.0),
transition: context.transition
)
context.add(avatar
.position(CGPoint(x: context.availableSize.width / 2.0, y: 86.0))
)
}
context.add(peerName
.position(CGPoint(x: context.availableSize.width / 2.0 - 12.0, y: 167.0))
)
var buttonColor: UIColor = .white.withAlphaComponent(0.1)
var secondaryTextColor: UIColor = .white.withAlphaComponent(0.4)
if let backdropAttribute = attributes.first(where: { attribute in
if case .backdrop = attribute {
return true
} else {
return false
}
}), case let .backdrop(_, _, innerColor, outerColor, _, _, _) = backdropAttribute {
buttonColor = UIColor(rgb: UInt32(bitPattern: outerColor)).mixedWith(.white, alpha: 0.2)
secondaryTextColor = UIColor(rgb: UInt32(bitPattern: innerColor)).withMultiplied(hue: 1.0, saturation: 1.02, brightness: 1.25).mixedWith(UIColor.white, alpha: 0.3)
}
let closeButton = closeButton.update(
component: GlassBarButtonComponent(
size: CGSize(width: 44.0, height: 44.0),
backgroundColor: buttonColor,
isDark: false,
state: .tintedGlass,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: .white
)
)),
action: { _ in
(controller() as? GiftAuctionWearPreviewScreen)?.dismissAnimated()
}
),
availableSize: CGSize(width: 44.0, height: 44.0),
transition: .immediate
)
context.add(closeButton
.position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0))
)
let textFont = Font.regular(13.0)
let boldTextFont = Font.semibold(13.0)
let textColor = UIColor.white
let linkColor = UIColor.white
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
})
if state.cachedSmallChevronImage == nil || state.cachedSmallChevronImage?.1 !== environment.theme {
state.cachedSmallChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: linkColor)!, theme)
}
let learnMoreAttributedString = parseMarkdownIntoAttributedString(strings.Gift_WearPreview_LearnMore, attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString
if let range = learnMoreAttributedString.string.range(of: ">"), let chevronImage = state.cachedSmallChevronImage?.0 {
learnMoreAttributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: learnMoreAttributedString.string))
}
let learnMore = learnMore.update(
component: PlainButtonComponent(
content: AnyComponent(BalancedTextComponent(
text: .plain(learnMoreAttributedString),
maximumNumberOfLines: 1,
tintColor: secondaryTextColor
)),
action: {
let controller = component.context.sharedContext.makeGiftWearPreviewScreen(context: component.context, gift: component.auctionContext.gift, attributes: component.attributes)
environment.controller()?.push(controller)
},
animateScale: false
),
availableSize: context.availableSize,
transition: context.transition
)
context.add(learnMore
.position(CGPoint(x: context.availableSize.width / 2.0, y: 198.0))
)
if case let .generic(gift) = component.auctionContext.gift {
let initialGift = initialGift.update(
component: GiftItemComponent(
context: component.context,
theme: theme,
strings: strings,
subject: .starGift(gift: gift, price: ""),
ribbon: GiftItemComponent.Ribbon(text: strings.Gift_WearPreview_Limited, color: .blue),
mode: .thumbnail
),
availableSize: CGSize(width: 120.0, height: 120.0),
transition: context.transition
)
context.add(initialGift
.position(CGPoint(x: sideInset + initialGift.size.width * 0.5, y: contentHeight + 76.0))
)
}
var ribbonColor: GiftItemComponent.Ribbon.Color = .blue
for attribute in attributes {
if case let .backdrop(_, _, innerColor, outerColor, _, _, _) = attribute {
ribbonColor = .custom(outerColor, innerColor)
break
}
}
let upgradedGift = upgradedGift.update(
component: GiftItemComponent(
context: component.context,
theme: theme,
strings: strings,
subject: .preview(attributes: attributes, rarity: nil),
ribbon: GiftItemComponent.Ribbon(text: strings.Gift_WearPreview_Upgraded, color: ribbonColor),
animateChanges: true,
mode: .thumbnail
),
availableSize: CGSize(width: 120.0, height: 120.0),
transition: context.transition
)
context.add(upgradedGift
.position(CGPoint(x: context.availableSize.width - sideInset - upgradedGift.size.width * 0.5, y: contentHeight + 76.0))
)
let arrow = arrow.update(
component: BundleIconComponent(name: "Premium/Auction/Upgrade", tintColor: theme.list.itemSecondaryTextColor),
availableSize: context.availableSize,
transition: context.transition
)
context.add(arrow
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + 56.0))
)
let upgradeLabel = upgradeLabel.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(string: strings.Gift_WearPreview_FreeUpgrade, font: Font.medium(13.0), textColor: theme.list.itemSecondaryTextColor)),
horizontalAlignment: .center,
maximumNumberOfLines: 2,
lineSpacing: 0.1
),
availableSize: context.availableSize,
transition: context.transition
)
context.add(upgradeLabel
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + 94.0))
)
contentHeight += 137.0
if case let .generic(gift) = component.auctionContext.gift, let availability = gift.availability {
var remains: Int32 = availability.remains
if let auctionState = state.giftAuctionState {
switch auctionState.auctionState {
case let .ongoing(_, _, _, _, _, _, _, giftsLeft, _, _, _, _):
remains = giftsLeft
case .finished:
remains = 0
}
}
let total = availability.total
let position = CGFloat(remains) / CGFloat(total)
let sold = total - remains
let remainingCount = remainingCount.update(
component: GiftRemainingCountComponent(
inactiveColor: theme.list.itemBlocksBackgroundColor,
activeColors: [UIColor(rgb: 0x72d6ff), UIColor(rgb: 0x32a0f9)],
inactiveTitle: strings.Gift_Send_Remains(remains),
inactiveValue: "",
inactiveTitleColor: theme.list.itemSecondaryTextColor,
activeTitle: "",
activeValue: sold > 0 ? strings.Gift_Send_Sold(sold) : "",
activeTitleColor: .white,
badgeText: "",
badgePosition: position,
badgeGraphPosition: position,
invertProgress: true,
leftString: "",
groupingSeparator: environment.dateTimeFormat.groupingSeparator
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height),
transition: context.transition
)
context.add(remainingCount
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight))
)
if let giftsPerRound = gift.auctionGiftsPerRound {
let footerAttributes = MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.list.freeTextColor),
bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: theme.list.freeTextColor),
link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.list.itemAccentColor),
linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
}
)
let parsedString = parseMarkdownIntoAttributedString(strings.Gift_Setup_AuctionInfo(environment.strings.Gift_Setup_AuctionInfo_Gifts(giftsPerRound), strings.Gift_Setup_AuctionInfo_Bidders(giftsPerRound)).string, attributes: footerAttributes)
let auctionFooterText = NSMutableAttributedString(attributedString: parsedString)
if state.cachedSmallChevronImage == nil || state.cachedSmallChevronImage?.1 !== environment.theme {
state.cachedSmallChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: theme.list.itemAccentColor)!, environment.theme)
}
if let range = auctionFooterText.string.range(of: ">"), let chevronImage = state.cachedSmallChevronImage?.0 {
auctionFooterText.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: auctionFooterText.string))
}
let auctionFooter = auctionFooter.update(
component: MultilineTextComponent(
text: .plain(auctionFooterText),
maximumNumberOfLines: 0,
highlightColor: theme.list.itemAccentColor.withAlphaComponent(0.1),
highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
} else {
return nil
}
},
tapAction: { _, _ in
guard let controller = controller() else {
return
}
let infoController = component.context.sharedContext.makeGiftAuctionInfoScreen(context: component.context, auctionContext: component.auctionContext, completion: nil)
controller.push(infoController)
}
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 16.0 * 2.0, height: 10000.0),
transition: context.transition
)
context.add(auctionFooter
.position(CGPoint(x: sideInset + 16.0 + auctionFooter.size.width * 0.5, y: contentHeight + 52.0 + auctionFooter.size.height * 0.5))
)
contentHeight += auctionFooter.size.height
}
}
contentHeight += 80.0
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
var startTime = currentTime
var endTime = currentTime
var isUpcoming = false
if let auctionState = state.giftAuctionState {
startTime = auctionState.startDate
endTime = auctionState.endDate
}
var buttonTitle = strings.Gift_Auction_Join
let endTimeout: Int32
if currentTime < startTime {
isUpcoming = true
endTimeout = max(0, startTime - currentTime)
} else {
endTimeout = max(0, endTime - currentTime)
}
let hours = Int(endTimeout / 3600)
let minutes = Int((endTimeout % 3600) / 60)
let seconds = Int(endTimeout % 60)
let rawString: String
if isUpcoming {
buttonTitle = strings.Gift_Auction_EarlyBid
rawString = hours > 0 ? strings.Gift_Auction_StartsInHours : strings.Gift_Auction_StartsInMinutes
} else {
rawString = hours > 0 ? strings.Gift_Auction_TimeLeftHours : strings.Gift_Auction_TimeLeftMinutes
}
var buttonAnimatedTitleItems: [AnimatedTextComponent.Item] = []
var startIndex = rawString.startIndex
while true {
if let range = rawString.range(of: "{", range: startIndex ..< rawString.endIndex) {
if range.lowerBound != startIndex {
buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "prefix_\(buttonAnimatedTitleItems.count)", content: .text(String(rawString[startIndex ..< range.lowerBound]))))
}
startIndex = range.upperBound
if let endRange = rawString.range(of: "}", range: startIndex ..< rawString.endIndex) {
let controlString = rawString[range.upperBound ..< endRange.lowerBound]
if controlString == "h" {
buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "h", content: .number(hours, minDigits: 2)))
} else if controlString == "m" {
buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "m", content: .number(minutes, minDigits: 2)))
} else if controlString == "s" {
buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "s", content: .number(seconds, minDigits: 2)))
}
startIndex = endRange.upperBound
}
} else {
break
}
}
if startIndex != rawString.endIndex {
buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "suffix_\(buttonAnimatedTitleItems.count)", content: .text(String(rawString[startIndex ..< rawString.endIndex]))))
}
let buttonAttributedString = NSMutableAttributedString(string: buttonTitle, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)
let items: [AnyComponentWithIdentity<Empty>] = [
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString)))),
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(AnimatedTextComponent(
font: Font.with(size: 12.0, weight: .medium, traits: .monospacedNumbers),
color: theme.list.itemCheckColors.foregroundColor.withAlphaComponent(0.7),
items: buttonAnimatedTitleItems,
noDelay: true
)))
]
let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0)
let buttonSize = CGSize(width: context.availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0)
let buttonBackground = ButtonComponent.Background(
style: .glass,
color: theme.list.itemCheckColors.fillColor,
foreground: theme.list.itemCheckColors.foregroundColor,
pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
)
let button = button.update(
component: ButtonComponent(
background: buttonBackground,
content: AnyComponentWithIdentity(
id: AnyHashable("bid"),
component: AnyComponent(VStack(items, spacing: 1.0))
),
isEnabled: true,
displaysProgress: false,
action: {
if let controller = controller() as? GiftAuctionWearPreviewScreen {
controller.completion()
controller.dismissAnimated()
}
}),
availableSize: buttonSize,
transition: .spring(duration: 0.2)
)
let buttonFrame = CGRect(origin: CGPoint(x: buttonInsets.left, y: contentHeight), size: button.size)
context.add(button
.position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY))
)
contentHeight += button.size.height
contentHeight += 7.0
let effectiveBottomInset: CGFloat = environment.metrics.isTablet ? 0.0 : environment.safeInsets.bottom
return CGSize(width: context.availableSize.width, height: contentHeight + 5.0 + effectiveBottomInset)
}
}
}
final class GiftAuctionWearPreviewSheetComponent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let auctionContext: GiftAuctionContext
let attributes: [StarGift.UniqueGift.Attribute]
init(
context: AccountContext,
auctionContext: GiftAuctionContext,
attributes: [StarGift.UniqueGift.Attribute]
) {
self.context = context
self.auctionContext = auctionContext
self.attributes = attributes
}
static func ==(lhs: GiftAuctionWearPreviewSheetComponent, rhs: GiftAuctionWearPreviewSheetComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
return true
}
static var body: Body {
let sheet = Child(SheetComponent<EnvironmentType>.self)
let animateOut = StoredActionSlot(Action<Void>.self)
let sheetExternalState = SheetComponent<EnvironmentType>.ExternalState()
return { context in
let environment = context.environment[EnvironmentType.self]
let controller = environment.controller
let theme = environment.theme.withModalBlocksBackground()
let sheet = sheet.update(
component: SheetComponent<EnvironmentType>(
content: AnyComponent<EnvironmentType>(GiftAuctionWearPreviewSheetContent(
context: context.component.context,
auctionContext: context.component.auctionContext,
attributes: context.component.attributes,
animateOut: animateOut,
getController: controller
)),
style: .glass,
backgroundColor: .color(theme.list.blocksBackgroundColor),
followContentSizeChanges: true,
clipsContent: true,
hasDimView: false,
autoAnimateOut: false,
externalState: sheetExternalState,
animateOut: animateOut,
onPan: {
},
willDismiss: {
if let controller = controller() as? GiftAuctionWearPreviewScreen {
controller.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.3).withUserData(ViewControllerComponentContainer.AnimateOutTransition()))
}
}
),
environment: {
environment
SheetComponentEnvironment(
metrics: environment.metrics,
deviceMetrics: environment.deviceMetrics,
isDisplaying: environment.value.isVisible,
isCentered: environment.metrics.widthClass == .regular,
hasInputHeight: !environment.inputHeight.isZero,
regularMetricsSize: CGSize(width: 430.0, height: 900.0),
dismiss: { animated in
if animated {
if let controller = controller() as? GiftAuctionWearPreviewScreen {
controller.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.3).withUserData(ViewControllerComponentContainer.AnimateOutTransition()))
animateOut.invoke(Action { _ in
controller.dismiss(completion: nil)
})
}
} else {
if let controller = controller() as? GiftAuctionWearPreviewScreen {
controller.dismiss(completion: nil)
}
}
}
)
},
availableSize: context.availableSize,
transition: context.transition
)
context.add(sheet
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
)
if let controller = controller(), !controller.automaticallyControlPresentationContextLayout {
var sideInset: CGFloat = 0.0
var bottomInset: CGFloat = max(environment.safeInsets.bottom, sheetExternalState.contentHeight)
if case .regular = environment.metrics.widthClass {
sideInset = floor((context.availableSize.width - 430.0) / 2.0) - 12.0
bottomInset = (context.availableSize.height - sheetExternalState.contentHeight) / 2.0 + sheetExternalState.contentHeight
}
let layout = ContainerViewLayout(
size: context.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: max(sideInset, environment.safeInsets.left), bottom: 0.0, right: max(sideInset, environment.safeInsets.right)),
additionalInsets: .zero,
statusBarHeight: environment.statusBarHeight,
inputHeight: nil,
inputHeightIsInteractivellyChanging: false,
inVoiceOver: false
)
controller.presentationContext.containerLayoutUpdated(layout, transition: context.transition.containedViewLayoutTransition)
}
return context.availableSize
}
}
}
public class GiftAuctionWearPreviewScreen: ViewControllerComponentContainer {
private let context: AccountContext
fileprivate let completion: () -> Void
public init(
context: AccountContext,
auctionContext: GiftAuctionContext,
attributes: [StarGift.UniqueGift.Attribute],
completion: @escaping () -> Void
) {
self.context = context
self.completion = completion
super.init(
context: context,
component: GiftAuctionWearPreviewSheetComponent(context: context, auctionContext: auctionContext, attributes: attributes),
navigationBarAppearance: .none,
statusBarStyle: .ignore,
theme: .default
)
self.navigationPresentation = .flatModal
self.automaticallyControlPresentationContextLayout = false
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func viewDidLoad() {
super.viewDidLoad()
self.view.disablesInteractiveModalDismiss = true
}
public func dismissAnimated() {
if let view = self.node.hostView.findTaggedView(tag: SheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? SheetComponent<ViewControllerComponentContainer.Environment>.View {
view.dismissAnimated()
}
}
}
@@ -0,0 +1,260 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ComponentFlow
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import AccountContext
import AppBundle
import GiftItemComponent
import TooltipUI
import MultilineTextComponent
import BundleIconComponent
import TelegramStringFormatting
import AlertComponent
import TableComponent
import AvatarComponent
import AlertTransferHeaderComponent
import AlertTableComponent
public func giftOfferAlertController(
context: AccountContext,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?,
gift: StarGift.UniqueGift,
peer: EnginePeer,
amount: CurrencyAmount,
commit: @escaping () -> Void
) -> ViewController {
let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
let strings = presentationData.strings
let title = strings.Chat_GiftPurchaseOffer_AcceptConfirmation_Title
let buttonText: String = strings.Chat_GiftPurchaseOffer_AcceptConfirmation_Confirm
let priceString: String
switch amount.currency {
case .stars:
priceString = strings.Chat_GiftPurchaseOffer_AcceptConfirmation_Text_Stars(Int32(clamping: amount.amount.value))
case .ton:
priceString = formatTonAmountText(amount.amount.value, dateTimeFormat: presentationData.dateTimeFormat) + " TON"
}
let resaleConfiguration = StarsSubscriptionConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
let finalPriceString: String
switch amount.currency {
case .stars:
let starsValue = Int32(floor(Float(amount.amount.value) * Float(resaleConfiguration.starGiftCommissionStarsPermille) / 1000.0))
finalPriceString = strings.Chat_GiftPurchaseOffer_AcceptConfirmation_Text_Stars(starsValue)
case .ton:
let tonValue = Int64(Float(amount.amount.value) * Float(resaleConfiguration.starGiftCommissionTonPermille) / 1000.0)
finalPriceString = formatTonAmountText(tonValue, dateTimeFormat: presentationData.dateTimeFormat, maxDecimalPositions: 3) + " TON"
}
let giftTitle = "\(gift.title) #\(formatCollectibleNumber(gift.number, dateTimeFormat: presentationData.dateTimeFormat))"
let text = strings.Chat_GiftPurchaseOffer_AcceptConfirmation_Text(giftTitle, peer.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder), priceString, finalPriceString).string
let tableFont = Font.regular(15.0)
let tableTextColor = presentationData.theme.list.itemPrimaryTextColor
let modelButtonTag = GenericComponentViewTag()
let backdropButtonTag = GenericComponentViewTag()
let symbolButtonTag = GenericComponentViewTag()
var showAttributeInfoImpl: ((Any, String) -> Void)?
var tableItems: [TableComponent.Item] = []
let order: [StarGift.UniqueGift.Attribute.AttributeType] = [
.model, .pattern, .backdrop, .originalInfo
]
var attributeMap: [StarGift.UniqueGift.Attribute.AttributeType: StarGift.UniqueGift.Attribute] = [:]
for attribute in gift.attributes {
attributeMap[attribute.attributeType] = attribute
}
for type in order {
if let attribute = attributeMap[type] {
let id: String?
let title: String?
let value: NSAttributedString
let percentage: Float?
let tag: AnyObject?
switch attribute {
case let .model(name, _, rarity, _):
id = "model"
title = strings.Gift_Unique_Model
value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor)
percentage = Float(rarity.permilleValue) * 0.1
tag = modelButtonTag
case let .backdrop(name, _, _, _, _, _, rarity):
id = "backdrop"
title = strings.Gift_Unique_Backdrop
value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor)
percentage = Float(rarity.permilleValue) * 0.1
tag = backdropButtonTag
case let .pattern(name, _, rarity):
id = "pattern"
title = strings.Gift_Unique_Symbol
value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor)
percentage = Float(rarity.permilleValue) * 0.1
tag = symbolButtonTag
case .originalInfo:
continue
}
var items: [AnyComponentWithIdentity<Empty>] = []
items.append(
AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(
MultilineTextComponent(text: .plain(value))
)
)
)
if let percentage, let tag {
items.append(AnyComponentWithIdentity(
id: AnyHashable(1),
component: AnyComponent(Button(
content: AnyComponent(ButtonContentComponent(
context: context,
text: formatPercentage(percentage),
color: presentationData.theme.list.itemAccentColor
)),
action: {
showAttributeInfoImpl?(tag, strings.Gift_Unique_AttributeDescription(formatPercentage(percentage)).string)
}
).tagged(tag))
))
}
let itemComponent = AnyComponent(
HStack(items, spacing: 4.0)
)
tableItems.append(.init(
id: id,
title: title,
hasBackground: false,
component: itemComponent
))
}
}
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
content.append(AnyComponentWithIdentity(
id: "header",
component: AnyComponent(
AlertTransferHeaderComponent(
fromComponent: AnyComponentWithIdentity(id: "gift", component: AnyComponent(
GiftItemComponent(
context: context,
theme: presentationData.theme,
strings: strings,
peer: nil,
subject: .uniqueGift(gift: gift, price: nil),
mode: .thumbnail
)
)),
toComponent: AnyComponentWithIdentity(id: "avatar", component: AnyComponent(
AvatarComponent(
context: context,
theme: presentationData.theme,
peer: peer
)
)),
type: .transfer
)
)
))
content.append(AnyComponentWithIdentity(
id: "title",
component: AnyComponent(
AlertTitleComponent(title: title)
)
))
content.append(AnyComponentWithIdentity(
id: "text",
component: AnyComponent(
AlertTextComponent(content: .plain(text))
)
))
content.append(AnyComponentWithIdentity(
id: "table",
component: AnyComponent(
AlertTableComponent(items: tableItems)
)
))
if let valueAmount = gift.valueUsdAmount {
let resaleConfiguration = StarsSubscriptionConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
let usdRate: Double
switch amount.currency {
case .stars:
usdRate = Double(resaleConfiguration.usdWithdrawRate) / 1000.0 / 100.0
case .ton:
usdRate = Double(resaleConfiguration.tonUsdRate) / 1000.0 / 1000000.0
}
let offerUsdValue = Double(amount.amount.value) * usdRate
let giftUsdValue = Double(valueAmount) / 100.0
let fraction = giftUsdValue / offerUsdValue
let percentage = Int(fraction * 100) - 100
if percentage > 20 {
let warningText = strings.Chat_GiftPurchaseOffer_AcceptConfirmation_BadValue("\(percentage)%").string
content.append(AnyComponentWithIdentity(
id: "warning",
component: AnyComponent(
AlertTextComponent(content: .plain(warningText), color: .destructive, style: .plain(.small))
)
))
}
}
let updatedPresentationDataSignal = updatedPresentationData?.signal ?? context.sharedContext.presentationData
let alertController = AlertScreen(
configuration: AlertScreen.Configuration(actionAlignment: .vertical, dismissOnOutsideTap: true, allowInputInset: false),
content: content,
actions: [
.init(title: buttonText, type: .default, action: {
commit()
}),
.init(title: strings.Common_Cancel)
],
updatedPresentationData: (initial: presentationData, signal: updatedPresentationDataSignal)
)
var dismissAllTooltipsImpl: (() -> Void)?
showAttributeInfoImpl = { [weak alertController] tag, text in
dismissAllTooltipsImpl?()
guard let alertController, let sourceView = alertController.node.hostView.findTaggedView(tag: tag), let absoluteLocation = sourceView.superview?.convert(sourceView.center, to: alertController.view) else {
return
}
let location = CGRect(origin: CGPoint(x: absoluteLocation.x, y: absoluteLocation.y - 12.0), size: CGSize())
let tooltipController = TooltipScreen(account: context.account, sharedContext: context.sharedContext, text: .plain(text: text), style: .wide, location: .point(location, .bottom), displayDuration: .default, inset: 16.0, shouldDismissOnTouch: { _, _ in
return .dismiss(consume: false)
})
alertController.present(tooltipController, in: .current)
}
dismissAllTooltipsImpl = { [weak alertController] in
guard let alertController else {
return
}
alertController.window?.forEachController({ controller in
if let controller = controller as? TooltipScreen {
controller.dismiss(inPlace: false)
}
})
alertController.forEachController({ controller in
if let controller = controller as? TooltipScreen {
controller.dismiss(inPlace: false)
}
return true
})
}
return alertController
}
@@ -0,0 +1,295 @@
import Foundation
import UIKit
import ComponentFlow
import Display
import TelegramPresentationData
import ViewControllerComponent
import SheetComponent
import AccountContext
final class GiftPagerComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
public final class Item: Equatable {
let id: AnyHashable
let subject: GiftViewScreen.Subject
public init(id: AnyHashable, subject: GiftViewScreen.Subject) {
self.id = id
self.subject = subject
}
public static func ==(lhs: Item, rhs: Item) -> Bool {
if lhs.id != rhs.id {
return false
}
if lhs.subject != rhs.subject {
return false
}
return true
}
}
let context: AccountContext
let items: [Item]
let index: Int
let itemSpacing: CGFloat
let isSwitching: Bool
let updated: (CGFloat, Int) -> Void
public init(
context: AccountContext,
items: [Item],
index: Int = 0,
itemSpacing: CGFloat = 0.0,
isSwitching: Bool = false,
updated: @escaping (CGFloat, Int) -> Void
) {
self.context = context
self.items = items
self.index = index
self.itemSpacing = itemSpacing
self.isSwitching = isSwitching
self.updated = updated
}
public static func ==(lhs: GiftPagerComponent, rhs: GiftPagerComponent) -> Bool {
if lhs.items != rhs.items {
return false
}
if lhs.index != rhs.index {
return false
}
if lhs.itemSpacing != rhs.itemSpacing {
return false
}
if lhs.isSwitching != rhs.isSwitching {
return false
}
return true
}
final class View: UIView, UIScrollViewDelegate {
private let dimView: UIView
private let scrollView: UIScrollView
private var itemViews: [AnyHashable: ComponentHostView<EnvironmentType>] = [:]
private var component: GiftPagerComponent?
private var environment: Environment<EnvironmentType>?
override init(frame: CGRect) {
self.dimView = UIView()
self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.4)
self.scrollView = UIScrollView(frame: frame)
self.scrollView.clipsToBounds = true
self.scrollView.isPagingEnabled = true
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.showsVerticalScrollIndicator = false
self.scrollView.alwaysBounceHorizontal = false
self.scrollView.bounces = false
self.scrollView.layer.cornerRadius = 10.0
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.scrollView.contentInsetAdjustmentBehavior = .never
}
super.init(frame: frame)
self.addSubview(self.dimView)
self.scrollView.delegate = self
self.addSubview(self.scrollView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func animateIn() {
self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
}
private func animateOut() {
self.dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
}
private var isSwiping: Bool = false
private var lastScrollTime: TimeInterval = 0
private let swipeInactiveThreshold: TimeInterval = 0.5
public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
self.isSwiping = true
self.lastScrollTime = CACurrentMediaTime()
}
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate {
self.isSwiping = false
}
}
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
self.isSwiping = false
}
private var ignoreContentOffsetChange = false
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard let component = self.component, let environment = self.environment, !self.ignoreContentOffsetChange && !self.isUpdating else {
return
}
if self.isSwiping {
self.lastScrollTime = CACurrentMediaTime()
}
self.ignoreContentOffsetChange = true
let _ = self.update(component: component, availableSize: self.bounds.size, environment: environment, transition: .immediate)
component.updated(self.scrollView.contentOffset.x / (self.scrollView.contentSize.width - self.scrollView.frame.width), component.items.count)
self.ignoreContentOffsetChange = false
}
private var previousIsDisplaying: Bool = false
private var isUpdating = true
func update(component: GiftPagerComponent, availableSize: CGSize, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize), completion: nil)
var validIds: [AnyHashable] = []
let previousComponent = self.component
self.component = component
self.environment = environment
var countDecreased = false
if let previousComponent, previousComponent.items.count != component.items.count {
countDecreased = true
}
let firstTime = self.itemViews.isEmpty || countDecreased
let itemWidth = availableSize.width
let totalWidth = itemWidth * CGFloat(component.items.count) + component.itemSpacing * 2.0 * CGFloat(max(0, component.items.count))
let contentSize = CGSize(width: totalWidth, height: availableSize.height)
if self.scrollView.contentSize != contentSize {
self.scrollView.contentSize = contentSize
}
let scrollFrame = CGRect(origin: CGPoint(x: -component.itemSpacing / 2.0, y: 0.0), size: CGSize(width: availableSize.width + component.itemSpacing * 2.0, height: availableSize.height))
if self.scrollView.frame != scrollFrame {
self.scrollView.frame = scrollFrame
}
if firstTime {
let initialOffset = CGFloat(component.index) * (itemWidth + component.itemSpacing * 2.0)
self.scrollView.contentOffset = CGPoint(x: initialOffset, y: 0.0)
var position: CGFloat
if self.scrollView.contentSize.width > self.scrollView.frame.width {
position = self.scrollView.contentOffset.x / (self.scrollView.contentSize.width - self.scrollView.frame.width)
} else {
position = 0.0
}
component.updated(position, component.items.count)
}
let viewportCenter = self.scrollView.contentOffset.x + availableSize.width * 0.5
let currentTime = CACurrentMediaTime()
let isSwipingActive = self.isSwiping || (currentTime - self.lastScrollTime < self.swipeInactiveThreshold)
var i = 0
for item in component.items {
let itemOriginX = component.itemSpacing * 0.5 + (itemWidth + component.itemSpacing * 2.0) * CGFloat(i)
let itemFrame = CGRect(origin: CGPoint(x: itemOriginX, y: 0.0), size: CGSize(width: itemWidth, height: availableSize.height))
let centerDelta = itemFrame.midX - viewportCenter
let position = centerDelta / (availableSize.width * 0.75)
i += 1
if !isSwipingActive && abs(position) > 0.5 {
continue
} else if isSwipingActive && abs(position) > 1.5 {
continue
}
validIds.append(item.id)
let itemView: ComponentHostView<EnvironmentType>
var itemTransition = transition
if let current = self.itemViews[item.id] {
itemView = current
} else {
itemTransition = transition.withAnimation(.none)
itemView = ComponentHostView<EnvironmentType>()
self.itemViews[item.id] = itemView
self.scrollView.addSubview(itemView)
}
let environment = environment[EnvironmentType.self]
let _ = itemView.update(
transition: itemTransition,
component: AnyComponent(GiftViewSheetComponent(
context: component.context,
subject: item.subject
)),
environment: { environment },
containerSize: availableSize
)
itemView.frame = itemFrame
}
var animateTransitionMovement = false
var removeIds: [AnyHashable] = []
for (id, itemView) in self.itemViews {
if !validIds.contains(id) {
removeIds.append(id)
if countDecreased && !transition.animation.isImmediate {
self.addSubview(itemView)
itemView.center = CGPoint(x: self.scrollView.frame.width / 2.0 - component.itemSpacing, y: self.scrollView.frame.height / 2.0)
transition.setPosition(view: itemView, position: CGPoint(x: itemView.center.x - itemView.frame.width, y: itemView.center.y), completion: { _ in
itemView.removeFromSuperview()
})
animateTransitionMovement = true
} else {
itemView.removeFromSuperview()
}
}
}
for id in removeIds {
self.itemViews.removeValue(forKey: id)
}
if animateTransitionMovement {
transition.animatePosition(view: self.scrollView, from: CGPoint(x: self.scrollView.frame.width, y: 0.0), to: .zero, additive: true)
}
let viewEnvironment = environment[ViewControllerComponentContainer.Environment.self].value
if let _ = transition.userData(ViewControllerComponentContainer.AnimateInTransition.self) {
self.animateIn()
} else if self.previousIsDisplaying, let _ = transition.userData(ViewControllerComponentContainer.AnimateOutTransition.self) {
self.animateOut()
}
self.previousIsDisplaying = viewEnvironment.isVisible
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition)
}
}
@@ -0,0 +1,419 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ComponentFlow
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import AppBundle
import AvatarNode
import GiftItemComponent
import ChatMessagePaymentAlertController
import TabSelectorComponent
import BundleIconComponent
import MultilineTextComponent
import TelegramStringFormatting
import TooltipUI
import AlertComponent
import AlertTransferHeaderComponent
import AvatarComponent
import AlertTableComponent
import TableComponent
public func giftPurchaseAlertController(
context: AccountContext,
gift: StarGift.UniqueGift,
showAttributes: Bool,
peer: EnginePeer,
animateBalanceOverlay: Bool = false,
autoDismissOnCommit: Bool = true,
navigationController: NavigationController?,
commit: @escaping (CurrencyAmount.Currency) -> Void,
dismissed: @escaping () -> Void
) -> ViewController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let strings = presentationData.strings
let currencyPromise = ValuePromise<CurrencyAmount.Currency>(.stars)
if gift.resellForTonOnly {
currencyPromise.set(.ton)
}
var showAttributeInfoImpl: ((Any, String) -> Void)?
let contentSignal = currencyPromise.get()
|> map { currency in
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
if gift.resellForTonOnly {
content.append(AnyComponentWithIdentity(
id: "tonOnly",
component: AnyComponent(
AlertTextComponent(
content: .plain(strings.Gift_Buy_AcceptsTonOnly),
alignment: .center,
color: .secondary,
style: .plain(.small),
insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 8.0, right: 0.0)
)
)
))
} else {
content.append(AnyComponentWithIdentity(
id: "currency",
component: AnyComponent(
AlertCurrencyComponent(
currency: currency,
updatedCurrency: { currency in
currencyPromise.set(currency)
}
)
)
))
}
content.append(AnyComponentWithIdentity(
id: "header",
component: AnyComponent(
AlertTransferHeaderComponent(
fromComponent: AnyComponentWithIdentity(id: "gift", component: AnyComponent(
GiftItemComponent(
context: context,
theme: presentationData.theme,
strings: strings,
peer: nil,
subject: .uniqueGift(gift: gift, price: nil),
mode: .thumbnail
)
)),
toComponent: AnyComponentWithIdentity(id: "avatar", component: AnyComponent(
AvatarComponent(
context: context,
theme: presentationData.theme,
peer: peer
)
)),
type: .transfer
)
)
))
content.append(AnyComponentWithIdentity(
id: "title",
component: AnyComponent(
AlertTitleComponent(title: strings.Gift_Buy_Confirm_Title)
)
))
let giftTitle = "\(gift.title) #\(presentationStringsFormattedNumber(gift.number, presentationData.dateTimeFormat.groupingSeparator))"
var priceString = ""
switch currency {
case .stars:
if let resellAmount = gift.resellAmounts?.first(where: { $0.currency == .stars }) {
priceString = strings.Gift_Buy_Confirm_Text_Stars(Int32(clamping: resellAmount.amount.value))
}
case .ton:
if let resellAmount = gift.resellAmounts?.first(where: { $0.currency == .ton }) {
priceString = "**\(formatTonAmountText(resellAmount.amount.value, dateTimeFormat: presentationData.dateTimeFormat)) TON**"
}
}
let text: String
if peer.id == context.account.peerId {
text = strings.Gift_Buy_Confirm_Text(giftTitle, priceString).string
} else {
text = strings.Gift_Buy_Confirm_GiftText(giftTitle, priceString, peer.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder)).string
}
content.append(AnyComponentWithIdentity(
id: "text",
component: AnyComponent(
AlertTextComponent(content: .plain(text))
)
))
if showAttributes {
let tableFont = Font.regular(15.0)
let tableTextColor = presentationData.theme.list.itemPrimaryTextColor
let modelButtonTag = GenericComponentViewTag()
let backdropButtonTag = GenericComponentViewTag()
let symbolButtonTag = GenericComponentViewTag()
var tableItems: [TableComponent.Item] = []
let order: [StarGift.UniqueGift.Attribute.AttributeType] = [
.model, .pattern, .backdrop, .originalInfo
]
var attributeMap: [StarGift.UniqueGift.Attribute.AttributeType: StarGift.UniqueGift.Attribute] = [:]
for attribute in gift.attributes {
attributeMap[attribute.attributeType] = attribute
}
for type in order {
if let attribute = attributeMap[type] {
let id: String?
let title: String?
let value: NSAttributedString
let percentage: Float?
let tag: AnyObject?
switch attribute {
case let .model(name, _, rarity, _):
id = "model"
title = strings.Gift_Unique_Model
value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor)
percentage = Float(rarity.permilleValue) * 0.1
tag = modelButtonTag
case let .backdrop(name, _, _, _, _, _, rarity):
id = "backdrop"
title = strings.Gift_Unique_Backdrop
value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor)
percentage = Float(rarity.permilleValue) * 0.1
tag = backdropButtonTag
case let .pattern(name, _, rarity):
id = "pattern"
title = strings.Gift_Unique_Symbol
value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor)
percentage = Float(rarity.permilleValue) * 0.1
tag = symbolButtonTag
case .originalInfo:
continue
}
var items: [AnyComponentWithIdentity<Empty>] = []
items.append(
AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(
MultilineTextComponent(text: .plain(value))
)
)
)
if let percentage, let tag {
items.append(AnyComponentWithIdentity(
id: AnyHashable(1),
component: AnyComponent(Button(
content: AnyComponent(ButtonContentComponent(
context: context,
text: formatPercentage(percentage),
color: presentationData.theme.list.itemAccentColor
)),
action: {
showAttributeInfoImpl?(tag, strings.Gift_Unique_AttributeDescription(formatPercentage(percentage)).string)
}
).tagged(tag))
))
}
let itemComponent = AnyComponent(
HStack(items, spacing: 4.0)
)
tableItems.append(.init(
id: id,
title: title,
hasBackground: false,
component: itemComponent
))
}
}
content.append(AnyComponentWithIdentity(
id: "table",
component: AnyComponent(
AlertTableComponent(items: tableItems)
)
))
}
return content
}
let actionProgress = ValuePromise<Bool>(false)
let actionsSignal = currencyPromise.get()
|> map { currency in
var actions: [AlertScreen.Action] = []
var buyString = ""
switch currency {
case .stars:
if let resellAmount = gift.resellAmounts?.first(where: { $0.currency == .stars }) {
buyString = strings.Gift_Buy_Confirm_BuyFor(Int32(resellAmount.amount.value))
}
case .ton:
if let resellAmount = gift.resellAmounts?.first(where: { $0.currency == .ton }) {
buyString = strings.Gift_Buy_Confirm_BuyForTon(formatTonAmountText(resellAmount.amount.value, dateTimeFormat: presentationData.dateTimeFormat)).string
}
}
actions.append(.init(id: "buy", title: buyString, type: .default, action: {
if !autoDismissOnCommit {
actionProgress.set(true)
}
commit(currency)
}, autoDismiss: autoDismissOnCommit, progress: actionProgress.get()))
actions.append(.init(title: strings.Common_Cancel))
return actions
}
let alertController = ChatMessagePaymentAlertController(
context: context,
presentationData: presentationData,
updatedPresentationData: (presentationData, context.sharedContext.presentationData),
configuration: AlertScreen.Configuration(actionAlignment: .vertical, dismissOnOutsideTap: true, allowInputInset: false),
contentSignal: contentSignal,
actionsSignal: actionsSignal,
navigationController: navigationController,
chatPeerId: context.account.peerId,
showBalance: true,
currencySignal: currencyPromise.get(),
animateBalanceOverlay: animateBalanceOverlay
)
alertController.dismissed = { _ in
dismissed()
}
var dismissAllTooltipsImpl: (() -> Void)?
showAttributeInfoImpl = { [weak alertController] tag, text in
dismissAllTooltipsImpl?()
guard let alertController, let sourceView = alertController.node.hostView.findTaggedView(tag: tag), let absoluteLocation = sourceView.superview?.convert(sourceView.center, to: alertController.view) else {
return
}
let location = CGRect(origin: CGPoint(x: absoluteLocation.x, y: absoluteLocation.y - 12.0), size: CGSize())
let tooltipController = TooltipScreen(account: context.account, sharedContext: context.sharedContext, text: .plain(text: text), style: .wide, location: .point(location, .bottom), displayDuration: .default, inset: 16.0, shouldDismissOnTouch: { _, _ in
return .dismiss(consume: false)
})
alertController.present(tooltipController, in: .current)
}
dismissAllTooltipsImpl = { [weak alertController] in
guard let alertController else {
return
}
alertController.window?.forEachController({ controller in
if let controller = controller as? TooltipScreen {
controller.dismiss(inPlace: false)
}
})
alertController.forEachController({ controller in
if let controller = controller as? TooltipScreen {
controller.dismiss(inPlace: false)
}
return true
})
}
// if !gift.resellForTonOnly {
// Queue.mainQueue().after(0.3) {
// if let headerView = contentNode?.header.view as? TabSelectorComponent.View {
// let absoluteFrame = headerView.convert(headerView.bounds, to: nil)
// var originX = absoluteFrame.width * 0.75
// if let itemFrame = headerView.frameForItem(AnyHashable(1)) {
// originX = itemFrame.midX
// }
// let location = CGRect(origin: CGPoint(x: absoluteFrame.minX + floor(originX), y: absoluteFrame.minY - 8.0), size: CGSize())
// let tooltipController = TooltipScreen(account: context.account, sharedContext: context.sharedContext, text: .plain(text: presentationData.strings.Gift_Buy_PayInTon_Tooltip), style: .wide, location: .point(location, .bottom), displayDuration: .default, inset: 16.0, shouldDismissOnTouch: { _, _ in
// return .dismiss(consume: false)
// })
// controller.present(tooltipController, in: .window(.root))
// }
// }
// }
return alertController
}
private final class AlertCurrencyComponent: Component {
public typealias EnvironmentType = AlertComponentEnvironment
let currency: CurrencyAmount.Currency
let updatedCurrency: (CurrencyAmount.Currency) -> Void
public init(
currency: CurrencyAmount.Currency,
updatedCurrency: @escaping (CurrencyAmount.Currency) -> Void
) {
self.currency = currency
self.updatedCurrency = updatedCurrency
}
public static func ==(lhs: AlertCurrencyComponent, rhs: AlertCurrencyComponent) -> Bool {
if lhs.currency != rhs.currency {
return false
}
return true
}
final class View: UIView {
private let header = ComponentView<Empty>()
private var component: AlertCurrencyComponent?
private weak var state: EmptyComponentState?
func update(component: AlertCurrencyComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let environment = environment[AlertComponentEnvironment.self]
let headerSize = self.header.update(
transition: transition,
component: AnyComponent(TabSelectorComponent(
colors: TabSelectorComponent.Colors(
foreground: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.35),
selection: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.1),
simple: true
),
theme: environment.theme,
customLayout: TabSelectorComponent.CustomLayout(
font: Font.medium(14.0),
spacing: 10.0
),
items: [
TabSelectorComponent.Item(
id: AnyHashable(0),
content: .text(environment.strings.Gift_Buy_PayInStars)
),
TabSelectorComponent.Item(
id: AnyHashable(1),
content: .text(environment.strings.Gift_Buy_PayInTon)
)
],
selectedId: component.currency == .ton ? AnyHashable(1) : AnyHashable(0),
setSelectedId: { [weak self] id in
guard let self, let component = self.component else {
return
}
let currency: CurrencyAmount.Currency
if id == AnyHashable(0) {
currency = .stars
} else {
currency = .ton
}
if currency != component.currency {
component.updatedCurrency(currency)
}
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width + 54.0, height: 100.0)
)
let headerFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - headerSize.width) / 2.0), y: 0.0), size: headerSize)
if let view = self.header.view {
if view.superview == nil {
self.addSubview(view)
}
view.frame = headerFrame
}
return CGSize(width: availableSize.width, height: headerSize.height + 12.0)
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,109 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ComponentFlow
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import AppBundle
import ChatMessagePaymentAlertController
import TelegramStringFormatting
import TextFormat
import AlertComponent
public func giftRemoveInfoAlertController(
context: AccountContext,
gift: StarGift.UniqueGift,
peers: [EnginePeer.Id: EnginePeer],
removeInfoStars: Int64,
navigationController: NavigationController?,
commit: @escaping () -> Void
) -> ViewController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let strings = presentationData.strings
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
content.append(AnyComponentWithIdentity(
id: "title",
component: AnyComponent(
AlertTitleComponent(title: strings.Gift_RemoveDetails_Title)
)
))
content.append(AnyComponentWithIdentity(
id: "text",
component: AnyComponent(
AlertTextComponent(content: .plain(strings.Gift_RemoveDetails_Text))
)
))
for attribute in gift.attributes {
if case let .originalInfo(senderPeerId, recipientPeerId, date, text, entities) = attribute {
let textColor = presentationData.theme.actionSheet.primaryTextColor
let linkColor = presentationData.theme.actionSheet.controlAccentColor
let textFont = Font.regular(15.0)
let boldTextFont = Font.semibold(15.0)
let italicTextFont = Font.italic(15.0)
let boldItalicTextFont = Font.with(size: 15.0, weight: .semibold, traits: .italic)
let fixedTextFont = Font.monospace(15.0)
let senderName = senderPeerId.flatMap { peers[$0]?.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder) }
let recipientName = peers[recipientPeerId]?.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder) ?? ""
let dateString = stringForMediumDate(timestamp: date, strings: strings, dateTimeFormat: presentationData.dateTimeFormat, withTime: false)
let value: NSAttributedString
if let text {
let attributedText = stringWithAppliedEntities(text, entities: entities ?? [], baseColor: textColor, linkColor: linkColor, baseFont: textFont, linkFont: textFont, boldFont: boldTextFont, italicFont: italicTextFont, boldItalicFont: boldItalicTextFont, fixedFont: fixedTextFont, blockQuoteFont: textFont, message: nil)
let format = senderName != nil ? strings.Gift_Unique_OriginalInfoSenderWithText(senderName!, recipientName, dateString, "") : strings.Gift_Unique_OriginalInfoWithText(recipientName, dateString, "")
let string = NSMutableAttributedString(string: format.string, font: textFont, textColor: textColor)
string.replaceCharacters(in: format.ranges[format.ranges.count - 1].range, with: attributedText)
if let _ = senderPeerId {
string.addAttribute(.foregroundColor, value: linkColor, range: format.ranges[0].range)
string.addAttribute(.foregroundColor, value: linkColor, range: format.ranges[1].range)
} else {
string.addAttribute(.foregroundColor, value: linkColor, range: format.ranges[0].range)
}
value = string
} else {
let format = senderName != nil ? strings.Gift_Unique_OriginalInfoSender(senderName!, recipientName, dateString) : strings.Gift_Unique_OriginalInfo(recipientName, dateString)
let string = NSMutableAttributedString(string: format.string, font: textFont, textColor: textColor)
if let _ = senderPeerId {
string.addAttribute(.foregroundColor, value: linkColor, range: format.ranges[0].range)
string.addAttribute(.foregroundColor, value: linkColor, range: format.ranges[1].range)
} else {
string.addAttribute(.foregroundColor, value: linkColor, range: format.ranges[0].range)
}
value = string
}
content.append(AnyComponentWithIdentity(
id: "info",
component: AnyComponent(
AlertTextComponent(content: .attributed(value), style: .background(.small))
)
))
}
}
let alertController = ChatMessagePaymentAlertController(
context: context,
presentationData: presentationData,
configuration: AlertScreen.Configuration(actionAlignment: .vertical),
content: content,
actions: [
.init(title: strings.Gift_RemoveDetails_Action(" $ \(presentationStringsFormattedNumber(Int32(clamping: removeInfoStars), presentationData.dateTimeFormat.groupingSeparator))").string, type: .default, action: {
commit()
}),
.init(title: strings.Common_Cancel)
],
navigationController: navigationController,
chatPeerId: context.account.peerId,
showBalance: removeInfoStars > 0
)
return alertController
}
@@ -0,0 +1,238 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ComponentFlow
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import AppBundle
import AvatarNode
import GiftItemComponent
import TabSelectorComponent
import BundleIconComponent
import MultilineTextComponent
import TelegramStringFormatting
import TooltipUI
import AlertComponent
import AlertTransferHeaderComponent
import AvatarComponent
import AlertTableComponent
import TableComponent
import StarsAvatarComponent
public func giftSaleAlertController(
context: AccountContext,
gift: StarGift.UniqueGift,
resellAmount: CurrencyAmount,
commit: @escaping () -> Void,
dismissed: @escaping () -> Void
) -> ViewController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let strings = presentationData.strings
var showAttributeInfoImpl: ((Any, String) -> Void)?
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
content.append(AnyComponentWithIdentity(
id: "header",
component: AnyComponent(
AlertTransferHeaderComponent(
fromComponent: AnyComponentWithIdentity(id: "gift", component: AnyComponent(
GiftItemComponent(
context: context,
theme: presentationData.theme,
strings: strings,
peer: nil,
subject: .uniqueGift(gift: gift, price: nil),
mode: .thumbnail
)
)),
toComponent: AnyComponentWithIdentity(id: "avatar", component: AnyComponent(
ZStack([
AnyComponentWithIdentity(id: "background", component: AnyComponent(
RoundedRectangle(colors: [
UIColor(rgb: 0x72d5fd),
UIColor(rgb: 0x2a9ef1)
], cornerRadius: 30.0, gradientDirection: .vertical, size: CGSize(width: 60.0, height: 60.0))
)),
AnyComponentWithIdentity(id: "icon", component: AnyComponent(
BundleIconComponent(name: "Premium/Stars/GiftStore", tintColor: .white)
))
])
)),
type: .transfer
)
)
))
content.append(AnyComponentWithIdentity(
id: "title",
component: AnyComponent(
AlertTitleComponent(title: strings.Gift_Sell_Confirm_Title)
)
))
let giftTitle = "\(gift.title) #\(presentationStringsFormattedNumber(gift.number, presentationData.dateTimeFormat.groupingSeparator))"
var priceString = ""
switch resellAmount.currency {
case .stars:
priceString = strings.Gift_Buy_Confirm_Text_Stars(Int32(clamping: resellAmount.amount.value))
case .ton:
priceString = "**\(formatTonAmountText(resellAmount.amount.value, dateTimeFormat: presentationData.dateTimeFormat)) TON**"
}
let text = strings.Gift_Sell_Confirm_Text(giftTitle, priceString).string
content.append(AnyComponentWithIdentity(
id: "text",
component: AnyComponent(
AlertTextComponent(content: .plain(text))
)
))
let tableFont = Font.regular(15.0)
let tableTextColor = presentationData.theme.list.itemPrimaryTextColor
let modelButtonTag = GenericComponentViewTag()
let backdropButtonTag = GenericComponentViewTag()
let symbolButtonTag = GenericComponentViewTag()
var tableItems: [TableComponent.Item] = []
let order: [StarGift.UniqueGift.Attribute.AttributeType] = [
.model, .pattern, .backdrop, .originalInfo
]
var attributeMap: [StarGift.UniqueGift.Attribute.AttributeType: StarGift.UniqueGift.Attribute] = [:]
for attribute in gift.attributes {
attributeMap[attribute.attributeType] = attribute
}
for type in order {
if let attribute = attributeMap[type] {
let id: String?
let title: String?
let value: NSAttributedString
let percentage: Float?
let tag: AnyObject?
switch attribute {
case let .model(name, _, rarity, _):
id = "model"
title = strings.Gift_Unique_Model
value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor)
percentage = Float(rarity.permilleValue) * 0.1
tag = modelButtonTag
case let .backdrop(name, _, _, _, _, _, rarity):
id = "backdrop"
title = strings.Gift_Unique_Backdrop
value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor)
percentage = Float(rarity.permilleValue) * 0.1
tag = backdropButtonTag
case let .pattern(name, _, rarity):
id = "pattern"
title = strings.Gift_Unique_Symbol
value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor)
percentage = Float(rarity.permilleValue) * 0.1
tag = symbolButtonTag
case .originalInfo:
continue
}
var items: [AnyComponentWithIdentity<Empty>] = []
items.append(
AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(
MultilineTextComponent(text: .plain(value))
)
)
)
if let percentage, let tag {
items.append(AnyComponentWithIdentity(
id: AnyHashable(1),
component: AnyComponent(Button(
content: AnyComponent(ButtonContentComponent(
context: context,
text: formatPercentage(percentage),
color: presentationData.theme.list.itemAccentColor
)),
action: {
showAttributeInfoImpl?(tag, strings.Gift_Unique_AttributeDescription(formatPercentage(percentage)).string)
}
).tagged(tag))
))
}
let itemComponent = AnyComponent(
HStack(items, spacing: 4.0)
)
tableItems.append(.init(
id: id,
title: title,
hasBackground: false,
component: itemComponent
))
}
}
content.append(AnyComponentWithIdentity(
id: "table",
component: AnyComponent(
AlertTableComponent(items: tableItems)
)
))
var actions: [AlertScreen.Action] = []
var listString = ""
switch resellAmount.currency {
case .stars:
listString = strings.Gift_Sell_Confirm_ListFor(Int32(resellAmount.amount.value))
case .ton:
listString = strings.Gift_Sell_Confirm_ListForTon(formatTonAmountText(resellAmount.amount.value, dateTimeFormat: presentationData.dateTimeFormat)).string
}
actions.append(.init(id: "list", title: listString, type: .default, action: {
commit()
}))
actions.append(.init(title: strings.Common_Cancel))
let alertController = AlertScreen(
context: context,
configuration: AlertScreen.Configuration(actionAlignment: .vertical),
content: content,
actions: actions
)
var dismissAllTooltipsImpl: (() -> Void)?
showAttributeInfoImpl = { [weak alertController] tag, text in
dismissAllTooltipsImpl?()
guard let alertController, let sourceView = alertController.node.hostView.findTaggedView(tag: tag), let absoluteLocation = sourceView.superview?.convert(sourceView.center, to: alertController.view) else {
return
}
let location = CGRect(origin: CGPoint(x: absoluteLocation.x, y: absoluteLocation.y - 12.0), size: CGSize())
let tooltipController = TooltipScreen(account: context.account, sharedContext: context.sharedContext, text: .plain(text: text), style: .wide, location: .point(location, .bottom), displayDuration: .default, inset: 16.0, shouldDismissOnTouch: { _, _ in
return .dismiss(consume: false)
})
alertController.present(tooltipController, in: .current)
}
dismissAllTooltipsImpl = { [weak alertController] in
guard let alertController else {
return
}
alertController.window?.forEachController({ controller in
if let controller = controller as? TooltipScreen {
controller.dismiss(inPlace: false)
}
})
alertController.forEachController({ controller in
if let controller = controller as? TooltipScreen {
controller.dismiss(inPlace: false)
}
return true
})
}
return alertController
}
@@ -0,0 +1,221 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ComponentFlow
import Postbox
import TelegramCore
import TelegramPresentationData
import AccountContext
import AppBundle
import GiftItemComponent
import ChatMessagePaymentAlertController
import TooltipUI
import MultilineTextComponent
import TelegramStringFormatting
import AlertComponent
import TableComponent
import AvatarComponent
import AlertTransferHeaderComponent
import AlertTableComponent
public func giftTransferAlertController(
context: AccountContext,
gift: StarGift.UniqueGift,
peer: EnginePeer,
transferStars: Int64,
navigationController: NavigationController?,
commit: @escaping () -> Void
) -> AlertScreen {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let strings = presentationData.strings
let title = strings.Gift_Transfer_Confirmation_Title
let text: String
let buttonText: String
if transferStars > 0 {
text = strings.Gift_Transfer_Confirmation_Text("\(gift.title) #\(presentationStringsFormattedNumber(gift.number, presentationData.dateTimeFormat.groupingSeparator))", peer.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder), strings.Gift_Transfer_Confirmation_Text_Stars(Int32(clamping: transferStars))).string
buttonText = "\(strings.Gift_Transfer_Confirmation_Transfer) $ \(transferStars)"
} else {
text = strings.Gift_Transfer_Confirmation_TextFree("\(gift.title) #\(presentationStringsFormattedNumber(gift.number, presentationData.dateTimeFormat.groupingSeparator))", peer.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder)).string
buttonText = strings.Gift_Transfer_Confirmation_TransferFree
}
let tableFont = Font.regular(15.0)
let tableTextColor = presentationData.theme.list.itemPrimaryTextColor
let modelButtonTag = GenericComponentViewTag()
let backdropButtonTag = GenericComponentViewTag()
let symbolButtonTag = GenericComponentViewTag()
var showAttributeInfoImpl: ((Any, String) -> Void)?
var tableItems: [TableComponent.Item] = []
let order: [StarGift.UniqueGift.Attribute.AttributeType] = [
.model, .pattern, .backdrop, .originalInfo
]
var attributeMap: [StarGift.UniqueGift.Attribute.AttributeType: StarGift.UniqueGift.Attribute] = [:]
for attribute in gift.attributes {
attributeMap[attribute.attributeType] = attribute
}
for type in order {
if let attribute = attributeMap[type] {
let id: String?
let title: String?
let value: NSAttributedString
let percentage: Float?
let tag: AnyObject?
switch attribute {
case let .model(name, _, rarity, _):
id = "model"
title = strings.Gift_Unique_Model
value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor)
percentage = Float(rarity.permilleValue) * 0.1
tag = modelButtonTag
case let .backdrop(name, _, _, _, _, _, rarity):
id = "backdrop"
title = strings.Gift_Unique_Backdrop
value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor)
percentage = Float(rarity.permilleValue) * 0.1
tag = backdropButtonTag
case let .pattern(name, _, rarity):
id = "pattern"
title = strings.Gift_Unique_Symbol
value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor)
percentage = Float(rarity.permilleValue) * 0.1
tag = symbolButtonTag
case .originalInfo:
continue
}
var items: [AnyComponentWithIdentity<Empty>] = []
items.append(
AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(
MultilineTextComponent(text: .plain(value))
)
)
)
if let percentage, let tag {
items.append(AnyComponentWithIdentity(
id: AnyHashable(1),
component: AnyComponent(Button(
content: AnyComponent(ButtonContentComponent(
context: context,
text: formatPercentage(percentage),
color: presentationData.theme.list.itemAccentColor
)),
action: {
showAttributeInfoImpl?(tag, strings.Gift_Unique_AttributeDescription(formatPercentage(percentage)).string)
}
).tagged(tag))
))
}
let itemComponent = AnyComponent(
HStack(items, spacing: 4.0)
)
tableItems.append(.init(
id: id,
title: title,
hasBackground: false,
component: itemComponent
))
}
}
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
content.append(AnyComponentWithIdentity(
id: "header",
component: AnyComponent(
AlertTransferHeaderComponent(
fromComponent: AnyComponentWithIdentity(id: "gift", component: AnyComponent(
GiftItemComponent(
context: context,
theme: presentationData.theme,
strings: strings,
peer: nil,
subject: .uniqueGift(gift: gift, price: nil),
mode: .thumbnail
)
)),
toComponent: AnyComponentWithIdentity(id: "avatar", component: AnyComponent(
AvatarComponent(
context: context,
theme: presentationData.theme,
peer: peer
)
)),
type: .transfer
)
)
))
content.append(AnyComponentWithIdentity(
id: "title",
component: AnyComponent(
AlertTitleComponent(title: title)
)
))
content.append(AnyComponentWithIdentity(
id: "text",
component: AnyComponent(
AlertTextComponent(content: .plain(text))
)
))
content.append(AnyComponentWithIdentity(
id: "table",
component: AnyComponent(
AlertTableComponent(items: tableItems)
)
))
let alertController = ChatMessagePaymentAlertController(
context: context,
presentationData: presentationData,
configuration: AlertScreen.Configuration(actionAlignment: .vertical, dismissOnOutsideTap: true, allowInputInset: false),
content: content,
actions: [
.init(title: buttonText, type: .default, action: {
commit()
}),
.init(title: strings.Common_Cancel)
],
navigationController: navigationController,
chatPeerId: context.account.peerId,
showBalance: transferStars > 0
)
var dismissAllTooltipsImpl: (() -> Void)?
showAttributeInfoImpl = { [weak alertController] tag, text in
dismissAllTooltipsImpl?()
guard let alertController, let sourceView = alertController.node.hostView.findTaggedView(tag: tag), let absoluteLocation = sourceView.superview?.convert(sourceView.center, to: alertController.view) else {
return
}
let location = CGRect(origin: CGPoint(x: absoluteLocation.x, y: absoluteLocation.y - 12.0), size: CGSize())
let tooltipController = TooltipScreen(account: context.account, sharedContext: context.sharedContext, text: .plain(text: text), style: .wide, location: .point(location, .bottom), displayDuration: .default, inset: 16.0, shouldDismissOnTouch: { _, _ in
return .dismiss(consume: false)
})
alertController.present(tooltipController, in: .current)
}
dismissAllTooltipsImpl = { [weak alertController] in
guard let alertController else {
return
}
alertController.window?.forEachController({ controller in
if let controller = controller as? TooltipScreen {
controller.dismiss(inPlace: false)
}
})
alertController.forEachController({ controller in
if let controller = controller as? TooltipScreen {
controller.dismiss(inPlace: false)
}
return true
})
}
return alertController
}
@@ -0,0 +1,442 @@
import Foundation
import UIKit
import SwiftSignalKit
import Display
import TelegramCore
import TelegramPresentationData
import ComponentFlow
import ComponentDisplayAdapters
import AccountContext
import ViewControllerComponent
import MultilineTextComponent
import MultilineTextWithEntitiesComponent
import BalancedTextComponent
import ButtonComponent
import PresentationDataUtils
import LottieComponent
import ProfileLevelRatingBarComponent
import TextFormat
import TelegramStringFormatting
import TableComponent
import ResizableSheetComponent
import GlassBarButtonComponent
import BundleIconComponent
private final class GiftUpgradeCostScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let upgradePreview: StarGiftUpgradePreview
init(
context: AccountContext,
upgradePreview: StarGiftUpgradePreview
) {
self.context = context
self.upgradePreview = upgradePreview
}
static func ==(lhs: GiftUpgradeCostScreenComponent, rhs: GiftUpgradeCostScreenComponent) -> Bool {
return true
}
final class View: UIView {
private let descriptionText = ComponentView<Empty>()
private let bar = ComponentView<Empty>()
private let table = ComponentView<Empty>()
private let additionalDescription = ComponentView<Empty>()
private var component: GiftUpgradeCostScreenComponent?
private weak var state: EmptyComponentState?
private var environment: ViewControllerComponentContainer.Environment?
private var isUpdating: Bool = false
private var upgradePreviewTimer: SwiftSignalKit.Timer?
private var effectiveUpgradePrice: StarGiftUpgradePreview.Price?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
func upgradePreviewTimerTick() {
guard let upgradePreview = self.component?.upgradePreview else {
return
}
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
if let currentPrice = self.effectiveUpgradePrice {
if let price = upgradePreview.nextPrices.reversed().first(where: { currentTime >= $0.date }) {
if price.stars != currentPrice.stars {
self.effectiveUpgradePrice = price
if !self.isUpdating {
self.state?.updated(transition: .immediate.withUserData(ProfileLevelRatingBarComponent.TransitionHint(animate: true)))
}
}
} else {
self.upgradePreviewTimer?.invalidate()
self.upgradePreviewTimer = nil
}
} else if let price = upgradePreview.nextPrices.reversed().first(where: { currentTime >= $0.date}) {
self.effectiveUpgradePrice = price
if !self.isUpdating {
self.state?.updated()
}
}
}
func update(component: GiftUpgradeCostScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
let environment = environment[ViewControllerComponentContainer.Environment.self].value
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
let isFirstTime = self.component == nil
self.component = component
self.state = state
self.environment = environment
if isFirstTime {
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
if let _ = component.upgradePreview.nextPrices.first(where: { currentTime < $0.date }) {
self.upgradePreviewTimer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in
self?.upgradePreviewTimerTick()
}, queue: Queue.mainQueue())
self.upgradePreviewTimer?.start()
self.upgradePreviewTimerTick()
}
}
var contentHeight: CGFloat = 56.0
var value: CGFloat = 0.0
if let startStars = component.upgradePreview.prices.first?.stars, let endStars = component.upgradePreview.prices.last?.stars {
let effectiveValue = self.effectiveUpgradePrice?.stars ?? endStars
value = (CGFloat(effectiveValue - endStars) / CGFloat(startStars - endStars))
}
value = pow(value, 0.6)
value = min(0.96, 1.0 - value)
let barSize = self.bar.update(
transition: transition,
component: AnyComponent(ProfileLevelRatingBarComponent(
theme: environment.theme,
value: value,
leftLabel: environment.strings.Gift_UpgradeCost_Stars(Int32(clamping: component.upgradePreview.prices.first?.stars ?? 0)),
rightLabel: environment.strings.Gift_UpgradeCost_Stars(Int32(clamping: component.upgradePreview.prices.last?.stars ?? 0)),
badgeValue: "\(self.effectiveUpgradePrice?.stars ?? 0)",
badgeTotal: "",
level: 0,
icon: .stars,
inversed: true
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 110.0)
)
let barFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - barSize.width) * 0.5), y: contentHeight), size: barSize)
if let barView = self.bar.view {
if barView.superview == nil {
self.addSubview(barView)
}
transition.setFrame(view: barView, frame: barFrame)
}
contentHeight += barSize.height + 25.0
let descriptionSize = self.descriptionText.update(
transition: transition,
component: AnyComponent(BalancedTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.Gift_UpgradeCost_Description,
font: Font.regular(15.0),
textColor: environment.theme.list.itemPrimaryTextColor,
paragraphAlignment: .center
)),
horizontalAlignment: .center,
maximumNumberOfLines: 3,
lineSpacing: 0.2
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 50.0, height: .greatestFiniteMagnitude)
)
let descriptionFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - descriptionSize.width) * 0.5), y: contentHeight), size: descriptionSize)
if let descriptionView = self.descriptionText.view {
if descriptionView.superview == nil {
self.addSubview(descriptionView)
}
transition.setFrame(view: descriptionView, frame: descriptionFrame)
}
contentHeight += descriptionSize.height + 23.0
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
var tableItems: [TableComponent.Item] = []
for price in component.upgradePreview.prices {
if price.date < currentTime {
continue
}
let valueString = "⭐️\(presentationStringsFormattedNumber(abs(Int32(clamping: price.stars)), environment.dateTimeFormat.groupingSeparator))"
let valueAttributedString = NSMutableAttributedString(string: valueString, font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor)
let range = (valueAttributedString.string as NSString).range(of: "⭐️")
if range.location != NSNotFound {
valueAttributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range)
valueAttributedString.addAttribute(.baselineOffset, value: 1.0, range: range)
}
tableItems.append(TableComponent.Item(
id: price.stars,
title: stringForGiftUpgradeTimestamp(strings: environment.strings, dateTimeFormat: environment.dateTimeFormat, timestamp: price.date),
titleFont: .bold,
component: AnyComponent(MultilineTextWithEntitiesComponent(context: component.context, animationCache: component.context.animationCache, animationRenderer: component.context.animationRenderer, placeholderColor: .white, text: .plain(valueAttributedString)))
))
}
let tableSize = self.table.update(
transition: transition,
component: AnyComponent(TableComponent(
theme: environment.theme,
items: tableItems
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude)
)
let tableFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - tableSize.width) * 0.5), y: contentHeight), size: tableSize)
if let tableView = self.table.view {
if tableView.superview == nil {
self.addSubview(tableView)
}
transition.setFrame(view: tableView, frame: tableFrame)
}
contentHeight += tableSize.height + 15.0
let additionalDescriptionSize = self.additionalDescription.update(
transition: transition,
component: AnyComponent(BalancedTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.Gift_UpgradeCost_AdditionalDescription,
font: Font.regular(13.0),
textColor: environment.theme.list.itemSecondaryTextColor,
paragraphAlignment: .center
)),
horizontalAlignment: .center,
maximumNumberOfLines: 5,
lineSpacing: 0.2
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 50.0, height: .greatestFiniteMagnitude)
)
let additionalDescriptionFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - additionalDescriptionSize.width) * 0.5), y: contentHeight), size: additionalDescriptionSize)
if let additionalDescriptionView = self.additionalDescription.view {
if additionalDescriptionView.superview == nil {
self.addSubview(additionalDescriptionView)
}
transition.setFrame(view: additionalDescriptionView, frame: additionalDescriptionFrame)
}
contentHeight += additionalDescriptionSize.height + 15.0
let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0)
contentHeight += 52.0
contentHeight += buttonInsets.bottom
return CGSize(width: availableSize.width, height: contentHeight)
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private final class SheetContainerComponent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let upgradePreview: StarGiftUpgradePreview
init(
context: AccountContext,
upgradePreview: StarGiftUpgradePreview
) {
self.context = context
self.upgradePreview = upgradePreview
}
static func ==(lhs: SheetContainerComponent, rhs: SheetContainerComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.upgradePreview != rhs.upgradePreview {
return false
}
return true
}
final class State: ComponentState {
}
func makeState() -> State {
return State()
}
static var body: Body {
let sheet = Child(ResizableSheetComponent<EnvironmentType>.self)
let animateOut = StoredActionSlot(Action<Void>.self)
return { context in
let component = context.component
let environment = context.environment[EnvironmentType.self]
let controller = environment.controller
let dismiss: (Bool) -> Void = { animated in
if animated {
animateOut.invoke(Action { _ in
if let controller = controller() {
controller.dismiss(completion: nil)
}
})
} else {
if let controller = controller() {
controller.dismiss(completion: nil)
}
}
}
let theme = environment.theme
let backgroundColor = environment.theme.list.modalPlainBackgroundColor
var buttonTitle: [AnyComponentWithIdentity<Empty>] = []
let playButtonAnimation = ActionSlot<Void>()
buttonTitle.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(name: "anim_ok"),
color: environment.theme.list.itemCheckColors.foregroundColor,
startingPosition: .begin,
size: CGSize(width: 28.0, height: 28.0),
playOnce: playButtonAnimation
))))
buttonTitle.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(ButtonTextContentComponent(
text: environment.strings.Gift_UpgradeCost_Done,
badge: 0,
textColor: environment.theme.list.itemCheckColors.foregroundColor,
badgeBackground: environment.theme.list.itemCheckColors.foregroundColor,
badgeForeground: environment.theme.list.itemCheckColors.fillColor
))))
let sheet = sheet.update(
component: ResizableSheetComponent<EnvironmentType>(
content: AnyComponent<EnvironmentType>(
GiftUpgradeCostScreenComponent(
context: component.context,
upgradePreview: component.upgradePreview
)
),
titleItem: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Gift_UpgradeCost_Title, font: Font.semibold(17.0), textColor: environment.theme.actionSheet.primaryTextColor)))
),
leftItem: AnyComponent(
GlassBarButtonComponent(
size: CGSize(width: 44.0, height: 44.0),
backgroundColor: nil,
isDark: theme.overallDarkAppearance,
state: .glass,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: theme.chat.inputPanel.panelControlColor
)
)),
action: { _ in
dismiss(true)
}
)
),
rightItem: nil,
bottomItem: 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)
),
content: AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(HStack(buttonTitle, spacing: 2.0))
),
action: {
dismiss(true)
}
)
),
backgroundColor: .color(backgroundColor),
isFullscreen: false,
animateOut: animateOut
),
environment: {
environment
ResizableSheetComponentEnvironment(
theme: theme,
statusBarHeight: environment.statusBarHeight,
safeInsets: environment.safeInsets,
metrics: environment.metrics,
deviceMetrics: environment.deviceMetrics,
isDisplaying: environment.value.isVisible,
isCentered: environment.metrics.widthClass == .regular,
screenSize: context.availableSize,
regularMetricsSize: CGSize(width: 430.0, height: 900.0),
dismiss: { animated in
dismiss(animated)
}
)
},
availableSize: context.availableSize,
transition: context.transition
)
context.add(sheet
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
)
return context.availableSize
}
}
}
public class GiftUpgradeCostScreen: ViewControllerComponentContainer {
private let context: AccountContext
private var isDismissed: Bool = false
public init(
context: AccountContext,
upgradePreview: StarGiftUpgradePreview
) {
self.context = context
super.init(context: context, component: SheetContainerComponent(
context: context,
upgradePreview: upgradePreview
), navigationBarAppearance: .none, theme: .default)
self.statusBar.statusBarStyle = .Ignore
self.navigationPresentation = .flatModal
self.blocksBackgroundWhenInOverlay = true
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
}
@@ -0,0 +1,862 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SwiftSignalKit
import AccountContext
import TelegramPresentationData
import PresentationDataUtils
import ComponentFlow
import ViewControllerComponent
import SheetComponent
import MultilineTextComponent
import MultilineTextWithEntitiesComponent
import BundleIconComponent
import Markdown
import BalancedTextComponent
import TextFormat
import TelegramStringFormatting
import StarsAvatarComponent
import PlainButtonComponent
import TooltipUI
import GiftAnimationComponent
import ContextUI
import GiftItemComponent
import GlassBarButtonComponent
import TableComponent
private final class GiftValueSheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let gift: StarGift
let valueInfo: StarGift.UniqueGift.ValueInfo
let animateOut: ActionSlot<Action<()>>
let getController: () -> ViewController?
init(
context: AccountContext,
gift: StarGift,
valueInfo: StarGift.UniqueGift.ValueInfo,
animateOut: ActionSlot<Action<()>>,
getController: @escaping () -> ViewController?
) {
self.context = context
self.gift = gift
self.valueInfo = valueInfo
self.animateOut = animateOut
self.getController = getController
}
static func ==(lhs: GiftValueSheetContent, rhs: GiftValueSheetContent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.gift != rhs.gift {
return false
}
if lhs.valueInfo != rhs.valueInfo {
return false
}
return true
}
final class State: ComponentState {
let lastSalePriceTag = GenericComponentViewTag()
let floorPriceTag = GenericComponentViewTag()
let averagePriceTag = GenericComponentViewTag()
private let context: AccountContext
private let animateOut: ActionSlot<Action<()>>
private let getController: () -> ViewController?
private var disposable: Disposable?
var initialized = false
var starGiftsMap: [Int64: StarGift.Gift] = [:]
var cachedStarImage: (UIImage, PresentationTheme)?
var cachedSmallStarImage: (UIImage, PresentationTheme)?
var cachedSubtitleStarImage: (UIImage, PresentationTheme)?
var cachedTonImage: (UIImage, PresentationTheme)?
var cachedChevronImage: (UIImage, PresentationTheme)?
var cachedSmallChevronImage: (UIImage, PresentationTheme)?
init(
context: AccountContext,
animateOut: ActionSlot<Action<()>>,
getController: @escaping () -> ViewController?
) {
self.context = context
self.animateOut = animateOut
self.getController = getController
super.init()
self.disposable = (context.engine.payments.cachedStarGifts()
|> deliverOnMainQueue).startStrict(next: { [weak self] starGifts in
if let strongSelf = self {
var starGiftsMap: [Int64: StarGift.Gift] = [:]
if let starGifts {
for gift in starGifts {
if case let .generic(gift) = gift {
starGiftsMap[gift.id] = gift
}
}
}
strongSelf.starGiftsMap = starGiftsMap
strongSelf.updated(transition: .immediate)
}
})
}
deinit {
self.disposable?.dispose()
}
func showAttributeInfo(tag: Any, text: String) {
guard let controller = self.getController() as? GiftValueScreen else {
return
}
controller.dismissAllTooltips()
guard let sourceView = controller.node.hostView.findTaggedView(tag: tag), let absoluteLocation = sourceView.superview?.convert(sourceView.center, to: controller.view) else {
return
}
let location = CGRect(origin: CGPoint(x: absoluteLocation.x, y: absoluteLocation.y - 12.0), size: CGSize())
let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: .markdown(text: text), style: .wide, location: .point(location, .bottom), displayDuration: .default, inset: 16.0, shouldDismissOnTouch: { _, _ in
return .dismiss(consume: false)
})
controller.present(tooltipController, in: .current)
}
func openGiftResale(gift: StarGift.Gift) {
guard let controller = self.getController() as? GiftValueScreen else {
return
}
let storeController = self.context.sharedContext.makeGiftStoreController(
context: self.context,
peerId: self.context.account.peerId,
gift: gift
)
controller.push(storeController)
}
func openGiftFragmentResale(url: String) {
guard let controller = self.getController() as? GiftValueScreen, let navigationController = controller.navigationController as? NavigationController else {
return
}
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: navigationController, dismissInput: {})
}
func dismiss(animated: Bool) {
guard let controller = self.getController() as? GiftValueScreen else {
return
}
if animated {
controller.dismissAllTooltips()
self.animateOut.invoke(Action { [weak controller] _ in
controller?.dismiss(completion: nil)
})
} else {
controller.dismiss(animated: false)
}
}
}
func makeState() -> State {
return State(context: self.context, animateOut: self.animateOut, getController: self.getController)
}
static var body: Body {
let closeButton = Child(GlassBarButtonComponent.self)
let animation = Child(GiftCompositionComponent.self)
let titleBackground = Child(RoundedRectangle.self)
let title = Child(MultilineTextComponent.self)
let description = Child(MultilineTextComponent.self)
let table = Child(TableComponent.self)
let telegramSaleButton = Child(PlainButtonComponent.self)
let fragmentSaleButton = Child(PlainButtonComponent.self)
let giftCompositionExternalState = GiftCompositionComponent.ExternalState()
return { context in
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
let component = context.component
let theme = environment.theme
let strings = environment.strings
let dateTimeFormat = environment.dateTimeFormat
let state = context.state
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
let titleString: String = formatCurrencyAmount(component.valueInfo.value, currency: component.valueInfo.currency)
var giftTitle: String = ""
var giftCollectionTitle: String = ""
var animationFile: TelegramMediaFile?
var giftIconSubject: GiftItemComponent.Subject?
var genericGift: StarGift.Gift?
switch component.gift {
case let .generic(gift):
animationFile = gift.file
giftIconSubject = .starGift(gift: gift, price: "")
case let .unique(gift):
for attribute in gift.attributes {
if case let .model(_, file, _, _) = attribute {
animationFile = file
}
}
giftCollectionTitle = gift.title
giftTitle = "\(gift.title) #\(formatCollectibleNumber(gift.number, dateTimeFormat: dateTimeFormat))"
if let gift = state.starGiftsMap[gift.giftId] {
giftIconSubject = .starGift(gift: gift, price: "")
genericGift = gift
}
}
var originY: CGFloat = 0.0
let headerHeight: CGFloat = 210.0
let headerSubject: GiftCompositionComponent.Subject?
if let animationFile {
headerSubject = .generic(animationFile)
} else {
headerSubject = nil
}
if let headerSubject {
let animation = animation.update(
component: GiftCompositionComponent(
context: component.context,
theme: environment.theme,
subject: headerSubject,
animationOffset: nil,
animationScale: nil,
displayAnimationStars: false,
externalState: giftCompositionExternalState,
requestUpdate: { [weak state] _ in
state?.updated()
}
),
availableSize: CGSize(width: context.availableSize.width, height: headerHeight),
transition: context.transition
)
context.add(animation
.position(CGPoint(x: context.availableSize.width / 2.0, y: headerHeight / 2.0))
)
}
originY += headerHeight
let title = title.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(
string: titleString,
font: Font.with(size: 24.0, design: .round, weight: .bold),
textColor: theme.list.itemCheckColors.foregroundColor,
paragraphAlignment: .center
)),
horizontalAlignment: .center,
maximumNumberOfLines: 1
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
let titleBackground = titleBackground.update(
component: RoundedRectangle(color: theme.actionSheet.controlAccentColor, cornerRadius: 24.0),
environment: {},
availableSize: CGSize(width: title.size.width + 32.0, height: 48.0),
transition: .immediate
)
context.add(titleBackground
.position(CGPoint(x: context.availableSize.width / 2.0, y: 187.0))
)
context.add(title
.position(CGPoint(x: context.availableSize.width / 2.0, y: 187.0))
)
var descriptionText: String
if component.valueInfo.valueIsAverage {
descriptionText = strings.Gift_Value_DescriptionAveragePrice(giftCollectionTitle).string
} else {
if component.valueInfo.isLastSaleOnFragment {
descriptionText = strings.Gift_Value_DescriptionLastPriceFragment(giftTitle).string
} else {
descriptionText = strings.Gift_Value_DescriptionLastPriceTelegram(giftTitle).string
}
}
if !descriptionText.isEmpty {
let linkColor = theme.actionSheet.controlAccentColor
if state.cachedSmallStarImage == nil || state.cachedSmallStarImage?.1 !== environment.theme {
state.cachedSmallStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Premium/Stars/ButtonStar"), color: .white)!, theme)
}
if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== environment.theme {
state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: linkColor)!, theme)
}
let textFont = Font.regular(15.0)
let boldTextFont = Font.semibold(15.0)
let textColor = theme.list.itemPrimaryTextColor
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
})
descriptionText = descriptionText.replacingOccurrences(of: " >]", with: "\u{00A0}>]")
let attributedString = parseMarkdownIntoAttributedString(descriptionText, attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString
if let range = attributedString.string.range(of: "*"), let starImage = state.cachedSmallStarImage?.0 {
attributedString.addAttribute(.font, value: Font.regular(13.0), range: NSRange(range, in: attributedString.string))
attributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: attributedString.string))
attributedString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: attributedString.string))
}
if let range = attributedString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 {
attributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedString.string))
}
let description = description.update(
component: MultilineTextComponent(
text: .plain(attributedString),
horizontalAlignment: .center,
maximumNumberOfLines: 5,
lineSpacing: 0.2,
highlightColor: linkColor.withAlphaComponent(0.1),
highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
} else {
return nil
}
},
tapAction: { _, _ in
}
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 50.0, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
context.add(description
.position(CGPoint(x: context.availableSize.width / 2.0, y: 231.0 + description.size.height / 2.0))
.appear(.default(alpha: true))
.disappear(.default(alpha: true))
)
originY += description.size.height
originY += 42.0
} else {
originY += 9.0
}
let tableFont = Font.regular(15.0)
let tableTextColor = theme.list.itemPrimaryTextColor
var tableItems: [TableComponent.Item] = []
tableItems.append(.init(
id: "initialDate",
title: strings.Gift_Value_InitialSale,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: component.valueInfo.initialSaleDate, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor)))
)
))
let valueString = "⭐️\(formatStarsAmountText(StarsAmount(value: component.valueInfo.initialSaleStars, nanos: 0), dateTimeFormat: dateTimeFormat)) (~\(formatCurrencyAmount(component.valueInfo.initialSalePrice, currency: component.valueInfo.currency)))"
let valueAttributedString = NSMutableAttributedString(string: valueString, font: tableFont, textColor: tableTextColor)
let range = (valueAttributedString.string as NSString).range(of: "⭐️")
if range.location != NSNotFound {
valueAttributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range)
valueAttributedString.addAttribute(.baselineOffset, value: 1.0, range: range)
}
tableItems.append(.init(
id: "initialPrice",
title: strings.Gift_Value_InitialPrice,
component: AnyComponent(MultilineTextWithEntitiesComponent(
context: component.context,
animationCache: component.context.animationCache,
animationRenderer: component.context.animationRenderer,
placeholderColor: theme.list.mediaPlaceholderColor,
text: .plain(valueAttributedString),
maximumNumberOfLines: 0
)),
insets: UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 12.0)
))
if let lastSaleDate = component.valueInfo.lastSaleDate {
tableItems.append(.init(
id: "lastDate",
title: strings.Gift_Value_LastSale,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: lastSaleDate, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor)))
)
))
}
if let lastSalePrice = component.valueInfo.lastSalePrice {
let lastSalePriceString = formatCurrencyAmount(lastSalePrice, currency: component.valueInfo.currency)
let tag = state.lastSalePriceTag
var items: [AnyComponentWithIdentity<Empty>] = []
items.append(
AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: lastSalePriceString, font: tableFont, textColor: tableTextColor)))
)
)
)
let percentage = Int32(floor(Double(lastSalePrice) / Double(component.valueInfo.initialSalePrice) * 100.0 - 100.0))
let percentageString = (percentage > 0 ? "+\(percentage)" : "\(percentage)") + "%"
items.append(AnyComponentWithIdentity(
id: AnyHashable(1),
component: AnyComponent(Button(
content: AnyComponent(ButtonContentComponent(
context: component.context,
text: percentageString,
color: theme.list.itemAccentColor
)),
action: { [weak state] in
state?.showAttributeInfo(tag: tag, text: strings.Gift_Value_LastPriceInfo(lastSalePriceString, giftCollectionTitle).string)
}
).tagged(tag))
))
let itemComponent = AnyComponent(
HStack(items, spacing: 4.0)
)
tableItems.append(.init(
id: "lastPrice",
title: strings.Gift_Value_LastPrice,
hasBackground: false,
component: itemComponent
))
}
if let floorPrice = component.valueInfo.floorPrice {
let floorPriceString = formatCurrencyAmount(floorPrice, currency: component.valueInfo.currency)
let tag = state.floorPriceTag
var items: [AnyComponentWithIdentity<Empty>] = []
items.append(
AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: floorPriceString, font: tableFont, textColor: tableTextColor)))
)
)
)
items.append(AnyComponentWithIdentity(
id: AnyHashable(1),
component: AnyComponent(Button(
content: AnyComponent(ButtonContentComponent(
context: component.context,
text: "?",
color: theme.list.itemAccentColor
)),
action: { [weak state] in
state?.showAttributeInfo(tag: tag, text: strings.Gift_Value_MinimumPriceInfo(floorPriceString, giftCollectionTitle).string)
}
).tagged(tag))
))
let itemComponent = AnyComponent(
HStack(items, spacing: 4.0)
)
tableItems.append(.init(
id: "floorPrice",
title: strings.Gift_Value_MinimumPrice,
hasBackground: false,
component: itemComponent
))
}
if let averagePrice = component.valueInfo.averagePrice {
let averagePriceString = formatCurrencyAmount(averagePrice, currency: component.valueInfo.currency)
let tag = state.averagePriceTag
var items: [AnyComponentWithIdentity<Empty>] = []
items.append(
AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: averagePriceString, font: tableFont, textColor: tableTextColor)))
)
)
)
items.append(AnyComponentWithIdentity(
id: AnyHashable(1),
component: AnyComponent(Button(
content: AnyComponent(ButtonContentComponent(
context: component.context,
text: "?",
color: theme.list.itemAccentColor
)),
action: { [weak state] in
state?.showAttributeInfo(tag: tag, text: strings.Gift_Value_AveragePriceInfo(averagePriceString, giftCollectionTitle).string)
}
).tagged(tag))
))
let itemComponent = AnyComponent(
HStack(items, spacing: 4.0)
)
tableItems.append(.init(
id: "averagePrice",
title: strings.Gift_Value_AveragePrice,
hasBackground: false,
component: itemComponent
))
}
let table = table.update(
component: TableComponent(
theme: environment.theme,
items: tableItems
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude),
transition: context.transition
)
context.add(table
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY + table.size.height / 2.0))
.appear(.default(alpha: true))
.disappear(.default(alpha: true))
)
originY += table.size.height + 23.0
if component.valueInfo.listedCount != nil || component.valueInfo.fragmentListedCount != nil {
originY += 5.0
}
if let listedCount = component.valueInfo.listedCount, let giftIconSubject {
let telegramSaleButton = telegramSaleButton.update(
component: PlainButtonComponent(
content: AnyComponent(
HStack([
AnyComponentWithIdentity(id: "count", component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: presentationStringsFormattedNumber(listedCount, dateTimeFormat.groupingSeparator), font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor)))
)),
AnyComponentWithIdentity(id: "spacing", component: AnyComponent(
Rectangle(color: .clear, width: 8.0, height: 1.0)
)),
AnyComponentWithIdentity(id: "icon", component: AnyComponent(
GiftItemComponent(
context: component.context,
theme: theme,
strings: strings,
peer: nil,
subject: giftIconSubject,
mode: .buttonIcon
)
)),
AnyComponentWithIdentity(id: "label", component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: " \(strings.Gift_Value_ForSaleOnTelegram)", font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor)))
)),
AnyComponentWithIdentity(id: "arrow", component: AnyComponent(
BundleIconComponent(name: "Chat/Context Menu/Arrow", tintColor: theme.actionSheet.controlAccentColor)
))
], spacing: 0.0)
),
action: { [weak state] in
guard let state, let genericGift else {
return
}
state.openGiftResale(gift: genericGift)
},
animateScale: false
),
environment: {},
availableSize: context.availableSize,
transition: .immediate
)
context.add(telegramSaleButton
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY + telegramSaleButton.size.height / 2.0))
)
originY += telegramSaleButton.size.height
originY += 12.0
}
if let listedCount = component.valueInfo.fragmentListedCount, let fragmentListedUrl = component.valueInfo.fragmentListedUrl, let giftIconSubject {
if component.valueInfo.listedCount != nil {
originY += 18.0
}
let fragmentSaleButton = fragmentSaleButton.update(
component: PlainButtonComponent(
content: AnyComponent(
HStack([
AnyComponentWithIdentity(id: "count", component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: presentationStringsFormattedNumber(listedCount, dateTimeFormat.groupingSeparator), font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor)))
)),
AnyComponentWithIdentity(id: "spacing", component: AnyComponent(
Rectangle(color: .clear, width: 8.0, height: 1.0)
)),
AnyComponentWithIdentity(id: "icon", component: AnyComponent(
GiftItemComponent(
context: component.context,
theme: theme,
strings: strings,
peer: nil,
subject: giftIconSubject,
mode: .buttonIcon
)
)),
AnyComponentWithIdentity(id: "label", component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: " \(strings.Gift_Value_ForSaleOnFragment)", font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor)))
)),
AnyComponentWithIdentity(id: "arrow", component: AnyComponent(
BundleIconComponent(name: "Chat/Context Menu/Arrow", tintColor: theme.actionSheet.controlAccentColor)
))
], spacing: 0.0)
),
action: { [weak state] in
state?.openGiftFragmentResale(url: fragmentListedUrl)
},
animateScale: false
),
environment: {},
availableSize: context.availableSize,
transition: .immediate
)
context.add(fragmentSaleButton
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY + fragmentSaleButton.size.height / 2.0))
)
originY += fragmentSaleButton.size.height
originY += 12.0
}
let closeButton = closeButton.update(
component: GlassBarButtonComponent(
size: CGSize(width: 44.0, height: 44.0),
backgroundColor: nil,
isDark: theme.overallDarkAppearance,
state: .glass,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: theme.chat.inputPanel.panelControlColor
)
)),
action: { [weak state] _ in
guard let state else {
return
}
state.dismiss(animated: true)
}
),
availableSize: CGSize(width: 44.0, height: 44.0),
transition: .immediate
)
context.add(closeButton
.position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0))
)
let effectiveBottomInset: CGFloat = environment.metrics.isTablet ? 0.0 : environment.safeInsets.bottom
return CGSize(width: context.availableSize.width, height: originY + 5.0 + effectiveBottomInset)
}
}
}
final class GiftValueSheetComponent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let gift: StarGift
let valueInfo: StarGift.UniqueGift.ValueInfo
init(
context: AccountContext,
gift: StarGift,
valueInfo: StarGift.UniqueGift.ValueInfo
) {
self.context = context
self.gift = gift
self.valueInfo = valueInfo
}
static func ==(lhs: GiftValueSheetComponent, rhs: GiftValueSheetComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.gift != rhs.gift {
return false
}
if lhs.valueInfo != rhs.valueInfo {
return false
}
return true
}
static var body: Body {
let sheet = Child(SheetComponent<EnvironmentType>.self)
let animateOut = StoredActionSlot(Action<Void>.self)
let sheetExternalState = SheetComponent<EnvironmentType>.ExternalState()
return { context in
let environment = context.environment[EnvironmentType.self]
let controller = environment.controller
let sheet = sheet.update(
component: SheetComponent<EnvironmentType>(
content: AnyComponent<EnvironmentType>(GiftValueSheetContent(
context: context.component.context,
gift: context.component.gift,
valueInfo: context.component.valueInfo,
animateOut: animateOut,
getController: controller
)),
style: .glass,
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
followContentSizeChanges: true,
clipsContent: true,
autoAnimateOut: false,
externalState: sheetExternalState,
animateOut: animateOut,
onPan: {
if let controller = controller() as? GiftValueScreen {
controller.dismissAllTooltips()
}
},
willDismiss: {
}
),
environment: {
environment
SheetComponentEnvironment(
metrics: environment.metrics,
deviceMetrics: environment.deviceMetrics,
isDisplaying: environment.value.isVisible,
isCentered: environment.metrics.widthClass == .regular,
hasInputHeight: !environment.inputHeight.isZero,
regularMetricsSize: CGSize(width: 430.0, height: 900.0),
dismiss: { animated in
if animated {
if let controller = controller() as? GiftValueScreen {
controller.dismissAllTooltips()
animateOut.invoke(Action { _ in
controller.dismiss(completion: nil)
})
}
} else {
if let controller = controller() as? GiftValueScreen {
controller.dismissAllTooltips()
controller.dismiss(completion: nil)
}
}
}
)
},
availableSize: context.availableSize,
transition: context.transition
)
context.add(sheet
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
)
if let controller = controller(), !controller.automaticallyControlPresentationContextLayout {
var sideInset: CGFloat = 0.0
var bottomInset: CGFloat = max(environment.safeInsets.bottom, sheetExternalState.contentHeight)
if case .regular = environment.metrics.widthClass {
sideInset = floor((context.availableSize.width - 430.0) / 2.0) - 12.0
bottomInset = (context.availableSize.height - sheetExternalState.contentHeight) / 2.0 + sheetExternalState.contentHeight
}
let layout = ContainerViewLayout(
size: context.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: max(sideInset, environment.safeInsets.left), bottom: 0.0, right: max(sideInset, environment.safeInsets.right)),
additionalInsets: .zero,
statusBarHeight: environment.statusBarHeight,
inputHeight: nil,
inputHeightIsInteractivellyChanging: false,
inVoiceOver: false
)
controller.presentationContext.containerLayoutUpdated(layout, transition: context.transition.containedViewLayoutTransition)
}
return context.availableSize
}
}
}
final class GiftValueScreen: ViewControllerComponentContainer {
private let context: AccountContext
private let gift: StarGift
private let valueInfo: StarGift.UniqueGift.ValueInfo
public init(
context: AccountContext,
gift: StarGift,
valueInfo: StarGift.UniqueGift.ValueInfo
) {
self.context = context
self.gift = gift
self.valueInfo = valueInfo
super.init(
context: context,
component: GiftValueSheetComponent(
context: context,
gift: gift,
valueInfo: valueInfo
),
navigationBarAppearance: .none,
statusBarStyle: .ignore,
theme: .default
)
self.navigationPresentation = .flatModal
self.automaticallyControlPresentationContextLayout = false
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
public override func viewDidLoad() {
super.viewDidLoad()
self.view.disablesInteractiveModalDismiss = true
}
public override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.dismissAllTooltips()
}
public func dismissAnimated() {
self.dismissAllTooltips()
if let view = self.node.hostView.findTaggedView(tag: SheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? SheetComponent<ViewControllerComponentContainer.Environment>.View {
view.dismissAnimated()
}
}
fileprivate func dismissAllTooltips() {
self.window?.forEachController({ controller in
if let controller = controller as? TooltipScreen {
controller.dismiss(inPlace: false)
}
})
self.forEachController({ controller in
if let controller = controller as? TooltipScreen {
controller.dismiss(inPlace: false)
}
return true
})
}
}
@@ -0,0 +1,254 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import TelegramCore
import AccountContext
import PresentationDataUtils
import TelegramStringFormatting
import BalanceNeededScreen
public func buyStarGiftImpl(
context: AccountContext,
recipientPeerId: EnginePeer.Id,
uniqueGift: StarGift.UniqueGift,
showAttributes: Bool,
acceptedPrice: CurrencyAmount? = nil,
skipConfirmation: Bool = false,
starsTopUpOptions: Signal<[StarsTopUpOption]?, NoError>,
buyGift: ((String, EnginePeer.Id, CurrencyAmount?) -> Signal<Never, BuyStarGiftError>)?,
getController: @escaping () -> ViewController?,
updateProgress: @escaping (Bool) -> Void,
updateIsBalanceVisible: @escaping (Bool) -> Void,
completion: @escaping () -> Void
) {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let action: (CurrencyAmount.Currency, @escaping () -> Void) -> Void = { currency, beforeCompletion in
guard let resellAmount = uniqueGift.resellAmounts?.first(where: { $0.currency == currency }) else {
guard let controller = getController() else {
return
}
let alertController = textAlertController(
context: context,
title: nil,
text: presentationData.strings.Gift_Buy_ErrorUnknown,
actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})],
parseMarkdown: true
)
controller.present(alertController, in: .window(.root))
beforeCompletion()
return
}
let proceed: () -> Void = {
updateProgress(true)
let buyGiftImpl: ((String, EnginePeer.Id, CurrencyAmount?) -> Signal<Never, BuyStarGiftError>)
if let buyGift {
buyGiftImpl = { slug, peerId, price in
return buyGift(slug, peerId, price)
}
} else {
buyGiftImpl = { slug, peerId, price in
return context.engine.payments.buyStarGift(slug: slug, peerId: peerId, price: price)
}
}
let finalPrice = acceptedPrice ?? resellAmount
let _ = (buyGiftImpl(uniqueGift.slug, recipientPeerId, finalPrice)
|> deliverOnMainQueue).start(error: { error in
guard let controller = getController() else {
return
}
beforeCompletion()
updateProgress(false)
HapticFeedback().error()
switch error {
case .serverProvided:
return
case let .priceChanged(newPrice):
let errorTitle = presentationData.strings.Gift_Buy_ErrorPriceChanged_Title
let originalPriceString: String
switch resellAmount.currency {
case .stars:
originalPriceString = presentationData.strings.Gift_Buy_ErrorPriceChanged_Text_Stars(Int32(clamping: resellAmount.amount.value))
case .ton:
originalPriceString = formatTonAmountText(resellAmount.amount.value, dateTimeFormat: presentationData.dateTimeFormat, maxDecimalPositions: nil) + " TON"
}
let newPriceString: String
let buttonText: String
switch newPrice.currency {
case .stars:
newPriceString = presentationData.strings.Gift_Buy_ErrorPriceChanged_Text_Stars(Int32(clamping: newPrice.amount.value))
buttonText = presentationData.strings.Gift_Buy_Confirm_BuyFor(Int32(newPrice.amount.value))
case .ton:
let tonValueString = formatTonAmountText(newPrice.amount.value, dateTimeFormat: presentationData.dateTimeFormat, maxDecimalPositions: nil)
newPriceString = tonValueString + " TON"
buttonText = presentationData.strings.Gift_Buy_Confirm_BuyForTon(tonValueString).string
}
let errorText = presentationData.strings.Gift_Buy_ErrorPriceChanged_Text(originalPriceString, newPriceString).string
let alertController = textAlertController(
context: context,
title: errorTitle,
text: errorText,
actions: [
TextAlertAction(type: .defaultAction, title: buttonText, action: {
buyStarGiftImpl(
context: context,
recipientPeerId: recipientPeerId,
uniqueGift: uniqueGift,
showAttributes: showAttributes,
acceptedPrice: newPrice,
skipConfirmation: true,
starsTopUpOptions: starsTopUpOptions,
buyGift: buyGift,
getController: getController,
updateProgress: updateProgress,
updateIsBalanceVisible: updateIsBalanceVisible,
completion: completion
)
}),
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {})
],
actionLayout: .vertical,
parseMarkdown: true
)
controller.present(alertController, in: .window(.root))
default:
let alertController = textAlertController(context: context, title: nil, text: presentationData.strings.Gift_Buy_ErrorUnknown, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})], parseMarkdown: true)
controller.present(alertController, in: .window(.root))
}
},
completed: {
beforeCompletion()
completion()
Queue.mainQueue().after(2.5) {
switch finalPrice.currency {
case .stars:
context.starsContext?.load(force: true)
case .ton:
context.tonContext?.load(force: true)
}
}
})
}
if resellAmount.currency == .stars, let starsContext = context.starsContext, let starsState = context.starsContext?.currentState, starsState.balance < resellAmount.amount {
let _ = (starsTopUpOptions
|> filter { $0 != nil }
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { options in
guard let controller = getController() else {
return
}
let purchaseController = context.sharedContext.makeStarsPurchaseScreen(
context: context,
starsContext: starsContext,
options: options ?? [],
purpose: .buyStarGift(requiredStars: resellAmount.amount.value),
targetPeerId: nil,
customTheme: nil,
completion: { stars in
guard let starsContext = context.starsContext else {
return
}
updateProgress(true)
starsContext.add(balance: StarsAmount(value: stars, nanos: 0))
let _ = (starsContext.onUpdate
|> deliverOnMainQueue).start(next: {
Queue.mainQueue().after(0.1, {
guard let starsContext = context.starsContext, let starsState = starsContext.currentState else {
return
}
if starsState.balance < resellAmount.amount {
updateProgress(false)
buyStarGiftImpl(
context: context,
recipientPeerId: recipientPeerId,
uniqueGift: uniqueGift,
showAttributes: showAttributes,
skipConfirmation: true,
starsTopUpOptions: starsTopUpOptions,
buyGift: buyGift,
getController: getController,
updateProgress: updateProgress,
updateIsBalanceVisible: updateIsBalanceVisible,
completion: completion
)
} else {
proceed()
}
});
})
}
)
controller.push(purchaseController)
})
} else if resellAmount.currency == .ton, let tonState = context.tonContext?.currentState, tonState.balance < resellAmount.amount {
guard let controller = getController() else {
return
}
let needed = resellAmount.amount - tonState.balance
var fragmentUrl = "https://fragment.com/ads/topup"
if let data = context.currentAppConfiguration.with({ $0 }).data, let value = data["ton_topup_url"] as? String {
fragmentUrl = value
}
controller.push(BalanceNeededScreen(
context: context,
amount: needed,
buttonAction: {
context.sharedContext.applicationBindings.openUrl(fragmentUrl)
}
))
beforeCompletion()
} else {
proceed()
}
}
if skipConfirmation {
action(acceptedPrice?.currency ?? .stars, {})
} else {
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: recipientPeerId))
|> deliverOnMainQueue).start(next: { peer in
guard let peer, let controller = getController() else {
return
}
var dismissImpl: (() -> Void)?
let alertController = giftPurchaseAlertController(
context: context,
gift: uniqueGift,
showAttributes: showAttributes,
peer: peer,
animateBalanceOverlay: showAttributes,
autoDismissOnCommit: !showAttributes,
navigationController: controller.navigationController as? NavigationController,
commit: { currency in
action(currency, {
dismissImpl?()
})
},
dismissed: {
updateIsBalanceVisible(true)
}
)
controller.present(alertController, in: .window(.root))
dismissImpl = { [weak alertController] in
alertController?.dismiss(animated: true)
}
updateIsBalanceVisible(false)
})
}
}
@@ -0,0 +1,235 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ComponentFlow
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import AppBundle
import Markdown
import GiftItemComponent
import StarsAvatarComponent
import PasswordSetupUI
import PresentationDataUtils
import AlertComponent
import AlertTransferHeaderComponent
import AlertInputFieldComponent
public func giftWithdrawAlertController(
context: AccountContext,
gift: StarGift.UniqueGift,
commit: @escaping () -> Void
) -> ViewController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let strings = presentationData.strings
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
content.append(AnyComponentWithIdentity(
id: "header",
component: AnyComponent(
AlertTransferHeaderComponent(
fromComponent: AnyComponentWithIdentity(id: "gift", component: AnyComponent(
GiftItemComponent(
context: context,
theme: presentationData.theme,
strings: strings,
peer: nil,
subject: .uniqueGift(gift: gift, price: nil),
mode: .thumbnail
)
)),
toComponent: AnyComponentWithIdentity(id: "fragment", component: AnyComponent(
StarsAvatarComponent(
context: context,
theme: presentationData.theme,
peer: .transactionPeer(.fragment),
photo: nil,
media: [],
gift: nil,
backgroundColor: .clear,
size: CGSize(width: 60.0, height: 60.0)
)
)),
type: .transfer
)
)
))
content.append(AnyComponentWithIdentity(
id: "title",
component: AnyComponent(
AlertTitleComponent(title: strings.Gift_Withdraw_Title)
)
))
content.append(AnyComponentWithIdentity(
id: "text",
component: AnyComponent(
AlertTextComponent(content: .plain(strings.Gift_Withdraw_Text("\(gift.title) #\(presentationStringsFormattedNumber(gift.number, presentationData.dateTimeFormat.groupingSeparator))").string))
)
))
let alertController = AlertScreen(
context: context,
configuration: AlertScreen.Configuration(actionAlignment: .vertical),
content: content,
actions: [
.init(title: strings.Gift_Withdraw_Proceed, type: .default, action: {
commit()
}),
.init(title: strings.Common_Cancel)
]
)
return alertController
}
public func confirmGiftWithdrawalController(
context: AccountContext,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil,
reference: StarGiftReference,
present: @escaping (ViewController, Any?) -> Void,
completion: @escaping (String) -> Void
) -> ViewController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let strings = presentationData.strings
let inputState = AlertInputFieldComponent.ExternalState()
let doneIsEnabled: Signal<Bool, NoError> = inputState.valueSignal
|> map { value in
return !value.isEmpty
}
let doneInProgressPromise = ValuePromise<Bool>(false)
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
content.append(AnyComponentWithIdentity(
id: "title",
component: AnyComponent(
AlertTitleComponent(title: strings.Gift_Withdraw_EnterPassword_Title)
)
))
content.append(AnyComponentWithIdentity(
id: "text",
component: AnyComponent(
AlertTextComponent(content: .plain(strings.Gift_Withdraw_EnterPassword_Text))
)
))
var applyImpl: (() -> Void)?
content.append(AnyComponentWithIdentity(
id: "input",
component: AnyComponent(
AlertInputFieldComponent(
context: context,
placeholder: strings.Channel_OwnershipTransfer_PasswordPlaceholder,
isSecureTextEntry: true,
isInitiallyFocused: true,
externalState: inputState,
returnKeyAction: {
applyImpl?()
}
)
)
))
var effectiveUpdatedPresentationData: (PresentationData, Signal<PresentationData, NoError>)
if let updatedPresentationData {
effectiveUpdatedPresentationData = updatedPresentationData
} else {
effectiveUpdatedPresentationData = (presentationData, context.sharedContext.presentationData)
}
var dismissImpl: (() -> Void)?
let alertController = AlertScreen(
configuration: AlertScreen.Configuration(allowInputInset: true),
content: content,
actions: [
.init(title: strings.Common_Cancel),
.init(title: strings.Gift_Withdraw_EnterPassword_Done, type: .default, action: {
applyImpl?()
}, autoDismiss: false, isEnabled: doneIsEnabled, progress: doneInProgressPromise.get())
],
updatedPresentationData: effectiveUpdatedPresentationData
)
applyImpl = {
doneInProgressPromise.set(true)
let _ = (context.engine.payments.requestStarGiftWithdrawalUrl(reference: reference, password: inputState.value)
|> deliverOnMainQueue).start(next: { url in
dismissImpl?()
completion(url)
}, error: { error in
var errorTextAndActions: (String, [TextAlertAction])?
switch error {
case .invalidPassword:
inputState.animateError()
case .limitExceeded:
errorTextAndActions = (strings.TwoStepAuth_FloodError, [TextAlertAction(type: .defaultAction, title: strings.Common_OK, action: {})])
default:
errorTextAndActions = (strings.Login_UnknownError, [TextAlertAction(type: .defaultAction, title: strings.Common_OK, action: {})])
}
doneInProgressPromise.set(false)
if let (text, actions) = errorTextAndActions {
dismissImpl?()
present(textAlertController(context: context, title: nil, text: text, actions: actions), nil)
}
})
}
dismissImpl = { [weak alertController] in
alertController?.dismiss(completion: nil)
}
return alertController
}
public func giftWithdrawalController(
context: AccountContext,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil,
reference: StarGiftReference,
initialError: RequestStarGiftWithdrawalError,
present: @escaping (ViewController, Any?) -> Void,
completion: @escaping (String) -> Void
) -> ViewController {
let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
let strings = presentationData.strings
var title: String? = strings.Gift_Withdraw_SecurityCheck
var text = strings.Gift_Withdraw_SecurityRequirements
var actions: [AlertScreen.Action] = [
.init(title: strings.Common_OK, type: .default)
]
switch initialError {
case .requestPassword:
return confirmGiftWithdrawalController(context: context, updatedPresentationData: updatedPresentationData, reference: reference, present: present, completion: completion)
case .twoStepAuthTooFresh, .authSessionTooFresh:
text = text + presentationData.strings.Gift_Withdraw_ComeBackLater
case .twoStepAuthMissing:
actions = [
.init(title: strings.Gift_Withdraw_SetupTwoStepAuth, type: .default, action: {
let controller = SetupTwoStepVerificationController(context: context, initialState: .automatic, stateUpdated: { update, shouldDismiss, controller in
if shouldDismiss {
controller.dismiss()
}
})
present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}),
.init(title: strings.Common_Cancel)
]
default:
title = nil
text = strings.Login_UnknownError
}
return AlertScreen(
context: context,
configuration: AlertScreen.Configuration(actionAlignment: .vertical),
title: title,
text: text,
actions: actions
)
}
@@ -0,0 +1,496 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import ImageBlur
private let additionalInset: CGFloat = 4.0
private let maskInset: CGFloat = 8.0
final class SlotsComponent<ChildEnvironment: Equatable>: Component {
public typealias EnvironmentType = ChildEnvironment
private let item: AnyComponent<ChildEnvironment>
private let items: [AnyComponentWithIdentity<ChildEnvironment>]
private let isAnimating: Bool
private let tintColor: UIColor?
private let verticalOffset: CGFloat
private let motionBlur: Bool
private let size: CGSize
public init(
item: AnyComponent<ChildEnvironment>,
items: [AnyComponentWithIdentity<ChildEnvironment>],
isAnimating: Bool,
tintColor: UIColor? = nil,
verticalOffset: CGFloat = 0.0,
motionBlur: Bool = true,
size: CGSize
) {
self.item = item
self.items = items
self.isAnimating = isAnimating
self.tintColor = tintColor
self.verticalOffset = verticalOffset
self.motionBlur = motionBlur
self.size = size
}
public static func == (lhs: SlotsComponent<ChildEnvironment>, rhs: SlotsComponent<ChildEnvironment>) -> Bool {
if lhs.item != rhs.item {
return false
}
if lhs.items != rhs.items {
return false
}
if lhs.isAnimating != rhs.isAnimating {
return false
}
if lhs.tintColor != rhs.tintColor {
return false
}
if lhs.verticalOffset != rhs.verticalOffset {
return false
}
if lhs.motionBlur != rhs.motionBlur {
return false
}
if lhs.size != rhs.size {
return false
}
return true
}
public final class View: UIView {
private var itemViews: [AnyHashable: ComponentView<ChildEnvironment>] = [:]
private var motionBlurLayers: [AnyHashable: SimpleLayer] = [:]
private var order: [AnyHashable] = []
private let containerView = UIView()
private let maskLayer = SimpleGradientLayer()
private enum SpinState {
case idle
case spinning
case decelerating
case settled
}
private var spinState: SpinState = .idle
private var isAnimating = false
private var animationLink: SharedDisplayLinkDriver.Link?
private var currentIds = Set<AnyHashable>()
private var lastSpawnTime: Double?
private var currentInterval: Double = 0.09
private var motionBlurFactor: CGFloat = 1.0
private var decelQueue: [AnyComponentWithIdentity<ChildEnvironment>] = []
private var decelTotalSteps: Int = 0
private var decelStepIndex: Int = 0
private let minSpawnInterval: Double = 0.10
private let maxSpawnInterval: Double = 0.80
private let baseAnimDuration: Double = 0.18
private let maxAnimDuration: Double = 0.5
private var component: SlotsComponent?
private var environment: Environment<ChildEnvironment>?
private var availableSize: CGSize?
@inline(__always) private func lerp(_ a: Double, _ b: Double, _ t: Double) -> Double { a + (b - a) * t }
@inline(__always) private func clamp01(_ x: Double) -> Double { max(0.0, min(1.0, x)) }
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(self.containerView)
self.containerView.clipsToBounds = true
self.maskLayer.startPoint = CGPoint(x: 0.5, y: 0.0)
self.maskLayer.endPoint = CGPoint(x: 0.5, y: 1.0)
let fade: CGFloat = 0.2
self.maskLayer.locations = [0.0, NSNumber(value: Float(fade)), NSNumber(value: Float(1.0 - fade)), 1.0]
self.maskLayer.colors = [
UIColor.black.withAlphaComponent(0.0).cgColor,
UIColor.black.withAlphaComponent(1.0).cgColor,
UIColor.black.withAlphaComponent(1.0).cgColor,
UIColor.black.withAlphaComponent(0.0).cgColor
]
self.containerView.layer.mask = self.maskLayer
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func spawnRandomSlot(availableSize: CGSize) {
guard var items = self.component?.items, !items.isEmpty else { return }
items = items.filter { !self.currentIds.contains($0.id) }
guard let randomItem = items.randomElement() else { return }
self.spawnSlot(item: randomItem, availableSize: availableSize) {}
}
private func spawnSlot(
item: AnyComponentWithIdentity<ChildEnvironment>,
isFinal: Bool = false,
availableSize: CGSize,
animDuration: Double? = nil,
completion: @escaping () -> Void
) {
guard let component = self.component, let environment = self.environment else { return }
self.currentIds.insert(item.id)
self.lastSpawnTime = CACurrentMediaTime()
let itemView = self.itemViews[item.id] ?? ComponentView<ChildEnvironment>()
self.itemViews[item.id] = itemView
let size = itemView.update(
transition: .immediate,
component: item.component,
environment: { environment[ChildEnvironment.self] },
containerSize: availableSize
)
if let view = itemView.view {
if view.superview == nil {
if let tintColor = component.tintColor {
view.layer.layerTintColor = tintColor.cgColor
}
view.layer.allowsGroupOpacity = true
self.containerView.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: 0.0, y: -size.height - additionalInset), size: size)
let travelDistance = (size.height + maskInset + additionalInset) * 2.0
let pitch = (size.height + additionalInset)
if isFinal {
var finalFrame = view.frame
finalFrame.origin.y = maskInset
view.frame = finalFrame
let fromY = size.height + maskInset + additionalInset
let overshoot: CGFloat = 7.0
let anim = CAKeyframeAnimation(keyPath: "position.y")
anim.isAdditive = true
anim.values = [ fromY, -overshoot, 0.0 ]
anim.keyTimes = [0.0, 0.7, 1.0]
anim.timingFunctions = [
CAMediaTimingFunction(name: .easeOut),
CAMediaTimingFunction(name: .easeInEaseOut)
]
anim.duration = 0.5
CATransaction.begin()
CATransaction.setCompletionBlock {
completion()
self.currentIds.remove(item.id)
self.finishSettled()
}
view.layer.add(anim, forKey: "finalOvershoot")
CATransaction.commit()
}
else {
let duration: Double = animDuration ?? baseAnimDuration
view.layer.animatePosition(
from: CGPoint(x: 0.0, y: (size.height + maskInset + additionalInset) * 2.0),
to: .zero,
duration: duration,
timingFunction: CAMediaTimingFunctionName.linear.rawValue,
additive: true,
completion: { _ in
completion()
self.currentIds.remove(item.id)
}
)
let intervalForConstantSpacing = Double(pitch / travelDistance) * duration
self.currentInterval = intervalForConstantSpacing
}
}
self.setMotionBlurFactor(id: item.id, factor: self.motionBlurFactor, transition: .immediate)
}
private func setMotionBlurFactor(id: AnyHashable, factor: CGFloat, transition: ComponentTransition) {
guard let component = self.component, component.motionBlur, let itemView = self.itemViews[id]?.view else {
return
}
if factor != 0.0 {
let motionBlurLayer: SimpleLayer
if let current = self.motionBlurLayers[id] {
motionBlurLayer = current
} else {
motionBlurLayer = SimpleLayer()
let image = generateImage(itemView.bounds.size, rotatedContext: { size, context in
UIGraphicsPushContext(context)
defer { UIGraphicsPopContext() }
context.clear(CGRect(origin: CGPoint(), size: size))
itemView.layer.render(in: context)
})
if let image {
motionBlurLayer.contents = verticalBlurredImage(image, radius: 8.0)?.cgImage
}
motionBlurLayer.contentsScale = itemView.layer.contentsScale
self.motionBlurLayers[id] = motionBlurLayer
itemView.layer.addSublayer(motionBlurLayer)
motionBlurLayer.position = CGPoint(x: itemView.bounds.size.width * 0.5, y: itemView.bounds.size.height * 0.5)
motionBlurLayer.bounds = CGRect(origin: CGPoint(), size: itemView.bounds.size)
if let tintColor = component.tintColor {
motionBlurLayer.layerTintColor = tintColor.cgColor
}
}
let scaleFactor = 1.0 * (1.0 - factor) + 1.25 * factor
let opacityFactor = 1.0 * (1.0 - factor) + 0.6 * factor
transition.setTransform(layer: motionBlurLayer, transform: CATransform3DMakeScale(1.0, scaleFactor, 1.0))
transition.setAlpha(layer: itemView.layer, alpha: opacityFactor)
} else if let motionBlurLayer = self.motionBlurLayers[id] {
self.motionBlurLayers.removeValue(forKey: id)
transition.setAlpha(layer: motionBlurLayer, alpha: 0.0, completion: { [weak motionBlurLayer] _ in
motionBlurLayer?.removeFromSuperlayer()
})
transition.setTransform(layer: motionBlurLayer, transform: CATransform3DIdentity)
}
}
private func beginSpinning() {
self.spinState = .spinning
self.isAnimating = true
self.motionBlurFactor = 1.0
self.currentInterval = 0.1
self.ensureDisplayLink()
}
private func beginDeceleration() {
guard let component = self.component, self.spinState == .spinning || self.spinState == .decelerating else { return }
self.spinState = .decelerating
self.isAnimating = false
var queue: [AnyComponentWithIdentity<ChildEnvironment>] = []
if !component.items.isEmpty {
let shuffled = Array(component.items.shuffled().prefix(3))
queue.append(contentsOf: shuffled)
}
queue.append(AnyComponentWithIdentity(id: "final", component: component.item))
self.decelQueue = queue
self.decelTotalSteps = queue.count
self.decelStepIndex = 0
self.motionBlurFactor = max(self.motionBlurFactor, 0.0001)
self.ensureDisplayLink()
}
private func ensureDisplayLink() {
guard self.animationLink == nil else {
return
}
self.animationLink = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] _ in
self?.tick()
})
}
private func invalidateDisplayLinkIfIdle() {
if self.spinState == .idle || self.spinState == .settled {
self.animationLink?.invalidate()
self.animationLink = nil
}
}
private func applyEdge3DSquish() {
let H = self.containerView.bounds.height
guard H > 0 else { return }
CATransaction.begin()
CATransaction.setDisableActions(true)
for (_, cv) in self.itemViews {
guard let v = cv.view, v.superview === self.containerView else { continue }
let midY = liveMidY(in: self.containerView, of: v)
let scaleY = edgeScaleY(for: midY, containerHeight: H)
var t = CATransform3DIdentity
t = CATransform3DScale(t, 1.0, scaleY, 1.0)
v.layer.transform = t
}
CATransaction.commit()
}
private func tick() {
guard let availableSize = self.availableSize else {
return
}
let now = CACurrentMediaTime()
switch self.spinState {
case .spinning:
if let last = self.lastSpawnTime, now - last >= self.currentInterval || self.lastSpawnTime == nil {
self.spawnRandomSlot(availableSize: availableSize)
}
case .decelerating:
let t = clamp01(self.decelTotalSteps > 1 ? Double(self.decelStepIndex) / Double(self.decelTotalSteps - 1) : 1.0)
if let last = self.lastSpawnTime, now - last >= self.currentInterval {
if !self.decelQueue.isEmpty {
let next = self.decelQueue.removeFirst()
let isFinal = self.decelQueue.isEmpty
let animDuration = isFinal ? nil : lerp(baseAnimDuration, maxAnimDuration, t)
self.spawnSlot(item: next, isFinal: isFinal, availableSize: availableSize, animDuration: animDuration, completion: {})
self.motionBlurFactor = CGFloat(1.0 - t)
self.decelStepIndex += 1
}
} else if self.lastSpawnTime == nil {
if !self.decelQueue.isEmpty {
let next = self.decelQueue.removeFirst()
self.spawnSlot(item: next, availableSize: availableSize) {}
}
}
case .settled, .idle:
self.invalidateDisplayLinkIfIdle()
}
self.applyEdge3DSquish()
}
private func finishSettled() {
for (id, _) in self.motionBlurLayers {
self.setMotionBlurFactor(id: id, factor: 0.0, transition: .easeInOut(duration: 0.2))
}
self.motionBlurLayers.removeAll()
self.spinState = .settled
self.decelQueue.removeAll()
self.invalidateDisplayLinkIfIdle()
}
func update(
component: SlotsComponent,
availableSize: CGSize,
state: EmptyComponentState,
environment: Environment<ChildEnvironment>,
transition: ComponentTransition
) -> CGSize {
self.component = component
self.environment = environment
self.availableSize = availableSize
let size = component.size
self.containerView.frame = CGRect(origin: CGPoint(x: 0.0, y: component.verticalOffset), size: size).insetBy(dx: 0.0, dy: -maskInset)
self.maskLayer.frame = CGRect(origin: .zero, size: self.containerView.bounds.size)
let wasAnimating = self.isAnimating
let nowAnimating = component.isAnimating
if nowAnimating && !wasAnimating {
self.beginSpinning()
self.spawnRandomSlot(availableSize: availableSize)
} else if !nowAnimating && wasAnimating {
self.beginDeceleration()
} else if nowAnimating && self.spinState == .settled {
self.beginSpinning()
self.spawnRandomSlot(availableSize: availableSize)
}
if let tintColor = component.tintColor {
for (id, itemView) in self.itemViews {
if let itemLayer = itemView.view?.layer {
transition.setTintColor(layer: itemLayer, color: tintColor)
}
if let blurLayer = self.motionBlurLayers[id] {
transition.setTintColor(layer: blurLayer, color: tintColor)
}
}
}
return size
}
}
public func makeView() -> View {
return View()
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private let minScaleYAtEdge: CGFloat = 0.7
private let squishFalloff: CGFloat = 0.12
private func smoothstep(_ x: CGFloat) -> CGFloat {
let t = max(0.0, min(1.0, x))
return t * t * (3.0 - 2.0 * t)
}
private func liveMidY(in container: UIView, of view: UIView) -> CGFloat {
if let pres = view.layer.presentation() {
let p = container.layer.convert(pres.position, from: view.layer.superlayer)
return p.y
}
return view.center.y
}
private func edgeScaleY(for midY: CGFloat, containerHeight H: CGFloat) -> CGFloat {
guard H > 0 else { return 1.0 }
let d = abs((midY - H * 0.5) / (H * 0.5))
let uRaw = (d - squishFalloff) / (1.0 - squishFalloff)
let u = smoothstep(max(0.0, min(1.0, uRaw)))
return (1.0 - u) + minScaleYAtEdge * u
}
final class SpacerComponent: Component {
let size: CGSize
init(
size: CGSize
) {
self.size = size
}
static func ==(lhs: SpacerComponent, rhs: SpacerComponent) -> Bool {
return lhs.size == rhs.size
}
final class View: UIView {
private var component: SpacerComponent?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: SpacerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
return component.size
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}