mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2026-02-23 02:44:01 +00:00
443 lines
20 KiB
Swift
443 lines
20 KiB
Swift
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 {
|
|
}
|
|
}
|