diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 639cbb16d3..619c1b6b99 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1213,6 +1213,7 @@ public protocol SharedAccountContext: AnyObject { func makeStoryStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peerId: EnginePeer.Id, storyId: Int32, storyItem: EngineStoryItem, fromStory: Bool) -> ViewController func makeStarsTransactionsScreen(context: AccountContext, starsContext: StarsContext) -> ViewController + func makeTonTransactionsScreen(context: AccountContext, tonContext: StarsContext) -> ViewController func makeStarsPurchaseScreen(context: AccountContext, starsContext: StarsContext, options: [Any], purpose: StarsPurchasePurpose, completion: @escaping (Int64) -> Void) -> ViewController func makeStarsTransferScreen(context: AccountContext, starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, extendedMedia: [TelegramExtendedMedia], inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?, EnginePeer?)?, NoError>, completion: @escaping (Bool) -> Void) -> ViewController func makeStarsSubscriptionTransferScreen(context: AccountContext, starsContext: StarsContext, invoice: TelegramMediaInvoice, link: String, inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?, EnginePeer?)?, NoError>, navigateToPeer: @escaping (EnginePeer) -> Void) -> ViewController diff --git a/submodules/ComponentFlow/Source/Base/ChildComponentTransitions.swift b/submodules/ComponentFlow/Source/Base/ChildComponentTransitions.swift index 75752bea75..960121a751 100644 --- a/submodules/ComponentFlow/Source/Base/ChildComponentTransitions.swift +++ b/submodules/ComponentFlow/Source/Base/ChildComponentTransitions.swift @@ -88,12 +88,19 @@ public extension ComponentTransition.Update { transition.setPosition(view: view, position: position) transition.setScale(view: view, scale: scale) } else { - if component._anchorPoint != nil { - view.bounds = CGRect(origin: CGPoint(), size: size) + if view is UIScrollView { + let frame = component.size.centered(around: component._position ?? CGPoint()) + if view.frame != frame { + transition.setFrame(view: view, frame: frame) + } } else { - transition.setBounds(view: view, bounds: CGRect(origin: CGPoint(), size: size)) + if component._anchorPoint != nil { + view.bounds = CGRect(origin: CGPoint(), size: size) + } else { + transition.setBounds(view: view, bounds: CGRect(origin: CGPoint(), size: size)) + } + transition.setPosition(view: view, position: position) } - transition.setPosition(view: view, position: position) } let opacity = component._opacity ?? 1.0 if view.alpha != opacity { diff --git a/submodules/ComponentFlow/Source/Base/CombinedComponent.swift b/submodules/ComponentFlow/Source/Base/CombinedComponent.swift index 95ea2ebda7..5f019bc5ff 100644 --- a/submodules/ComponentFlow/Source/Base/CombinedComponent.swift +++ b/submodules/ComponentFlow/Source/Base/CombinedComponent.swift @@ -724,11 +724,15 @@ public extension CombinedComponent { updatedChild.view.center = updatedChild._position ?? CGPoint() updatedChild.view.transform = CGAffineTransform(scaleX: scale, y: scale) } else { - updatedChild.view.bounds = CGRect(origin: CGPoint(), size: updatedChild.size) - if updatedChild.view.layer.anchorPoint != CGPoint(x: 0.5, y: 0.5) { - updatedChild.view.layer.position = updatedChild._position ?? CGPoint() + if updatedChild.view is UIScrollView { + updatedChild.view.frame = updatedChild.size.centered(around: updatedChild._position ?? CGPoint()) } else { - updatedChild.view.center = updatedChild._position ?? CGPoint() + updatedChild.view.bounds = CGRect(origin: CGPoint(), size: updatedChild.size) + if updatedChild.view.layer.anchorPoint != CGPoint(x: 0.5, y: 0.5) { + updatedChild.view.layer.position = updatedChild._position ?? CGPoint() + } else { + updatedChild.view.center = updatedChild._position ?? CGPoint() + } } } diff --git a/submodules/PremiumUI/Resources/diamond.scn b/submodules/PremiumUI/Resources/diamond.scn new file mode 100644 index 0000000000..882a662d15 Binary files /dev/null and b/submodules/PremiumUI/Resources/diamond.scn differ diff --git a/submodules/StatisticsUI/Sources/StarsTransactionItem.swift b/submodules/StatisticsUI/Sources/StarsTransactionItem.swift index 538ce1b80d..b706c235b1 100644 --- a/submodules/StatisticsUI/Sources/StarsTransactionItem.swift +++ b/submodules/StatisticsUI/Sources/StarsTransactionItem.swift @@ -280,9 +280,9 @@ final class StarsTransactionItemNode: ListViewItemNode, ItemListItemNode { let itemLabel: NSAttributedString let labelString: String - let absCount = StarsAmount(value: abs(item.transaction.count.value), nanos: abs(item.transaction.count.nanos)) + let absCount = StarsAmount(value: abs(item.transaction.count.amount.value), nanos: abs(item.transaction.count.amount.nanos)) let formattedLabel = presentationStringsFormattedNumber(absCount, item.presentationData.dateTimeFormat.groupingSeparator) - if item.transaction.count < StarsAmount.zero { + if item.transaction.count.amount < StarsAmount.zero { labelString = "- \(formattedLabel)" } else { labelString = "+ \(formattedLabel)" diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift index 90b3c169ec..afd57ea134 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift @@ -618,9 +618,9 @@ private final class StarsContextImpl { } var transactions = state.transactions if addTransaction { - transactions.insert(.init(flags: [.isLocal], id: "\(arc4random())", count: balance, currency: self.ton ? .ton : .stars, date: Int32(Date().timeIntervalSince1970), peer: .appStore, title: nil, description: nil, photo: nil, transactionDate: nil, transactionUrl: nil, paidMessageId: nil, giveawayMessageId: nil, media: [], subscriptionPeriod: nil, starGift: nil, floodskipNumber: nil, starrefCommissionPermille: nil, starrefPeerId: nil, starrefAmount: nil, paidMessageCount: nil, premiumGiftMonths: nil), at: 0) + let count = CurrencyAmount(amount: balance, currency: self.ton ? .ton : .stars) + transactions.insert(.init(flags: [.isLocal], id: "\(arc4random())", count: count, date: Int32(Date().timeIntervalSince1970), peer: .appStore, title: nil, description: nil, photo: nil, transactionDate: nil, transactionUrl: nil, paidMessageId: nil, giveawayMessageId: nil, media: [], subscriptionPeriod: nil, starGift: nil, floodskipNumber: nil, starrefCommissionPermille: nil, starrefPeerId: nil, starrefAmount: nil, paidMessageCount: nil, premiumGiftMonths: nil), at: 0) } - self.updateState(StarsContext.State(flags: [.isPendingBalance], balance: max(StarsAmount(value: 0, nanos: 0), state.balance + balance), subscriptions: state.subscriptions, canLoadMoreSubscriptions: state.canLoadMoreSubscriptions, transactions: transactions, canLoadMoreTransactions: state.canLoadMoreTransactions, isLoading: state.isLoading)) } @@ -723,10 +723,8 @@ private extension StarsContext.State.Transaction { let media = extendedMedia.flatMap({ $0.compactMap { textMediaAndExpirationTimerFromApiMedia($0, PeerId(0)).media } }) ?? [] let _ = subscriptionPeriod - - let amount = CurrencyAmount(apiAmount: stars) - self.init(flags: flags, id: id, count: amount.amount, currency: amount.currency, date: date, peer: parsedPeer, title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), transactionDate: transactionDate, transactionUrl: transactionUrl, paidMessageId: paidMessageId, giveawayMessageId: giveawayMessageId, media: media, subscriptionPeriod: subscriptionPeriod, starGift: starGift.flatMap { StarGift(apiStarGift: $0) }, floodskipNumber: floodskipNumber, starrefCommissionPermille: starrefCommissionPermille, starrefPeerId: starrefPeer?.peerId, starrefAmount: starrefAmount.flatMap(StarsAmount.init(apiAmount:)), paidMessageCount: paidMessageCount, premiumGiftMonths: premiumGiftMonths) + self.init(flags: flags, id: id, count: CurrencyAmount(apiAmount: stars), date: date, peer: parsedPeer, title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), transactionDate: transactionDate, transactionUrl: transactionUrl, paidMessageId: paidMessageId, giveawayMessageId: giveawayMessageId, media: media, subscriptionPeriod: subscriptionPeriod, starGift: starGift.flatMap { StarGift(apiStarGift: $0) }, floodskipNumber: floodskipNumber, starrefCommissionPermille: starrefCommissionPermille, starrefPeerId: starrefPeer?.peerId, starrefAmount: starrefAmount.flatMap(StarsAmount.init(apiAmount:)), paidMessageCount: paidMessageCount, premiumGiftMonths: premiumGiftMonths) } } } @@ -791,8 +789,7 @@ public final class StarsContext { public let flags: Flags public let id: String - public let count: StarsAmount - public let currency: CurrencyAmount.Currency + public let count: CurrencyAmount public let date: Int32 public let peer: Peer public let title: String? @@ -815,8 +812,7 @@ public final class StarsContext { public init( flags: Flags, id: String, - count: StarsAmount, - currency: CurrencyAmount.Currency, + count: CurrencyAmount, date: Int32, peer: Peer, title: String?, @@ -839,7 +835,6 @@ public final class StarsContext { self.flags = flags self.id = id self.count = count - self.currency = currency self.date = date self.peer = peer self.title = title @@ -1079,7 +1074,7 @@ public final class StarsContext { return peerId! } - public let ton: Bool + let ton: Bool public var currentState: StarsContext.State? { var state: StarsContext.State? @@ -1173,9 +1168,9 @@ private final class StarsTransactionsContextImpl { case .all: initialTransactions = currentTransactions case .incoming: - initialTransactions = currentTransactions.filter { $0.count > StarsAmount.zero } + initialTransactions = currentTransactions.filter { $0.count.amount > StarsAmount.zero } case .outgoing: - initialTransactions = currentTransactions.filter { $0.count < StarsAmount.zero } + initialTransactions = currentTransactions.filter { $0.count.amount < StarsAmount.zero } } self._state = StarsTransactionsContext.State(transactions: initialTransactions, canLoadMore: true, isLoading: false) @@ -1193,9 +1188,9 @@ private final class StarsTransactionsContextImpl { case .all: filteredTransactions = currentTransactions case .incoming: - filteredTransactions = currentTransactions.filter { $0.count > StarsAmount.zero } + filteredTransactions = currentTransactions.filter { $0.count.amount > StarsAmount.zero } case .outgoing: - filteredTransactions = currentTransactions.filter { $0.count < StarsAmount.zero } + filteredTransactions = currentTransactions.filter { $0.count.amount < StarsAmount.zero } } if !filteredTransactions.isEmpty && self._state.transactions.isEmpty && filteredTransactions != initialTransactions { @@ -1220,9 +1215,9 @@ private final class StarsTransactionsContextImpl { case .all: filteredTransactions = currentTransactions case .incoming: - filteredTransactions = currentTransactions.filter { $0.count > StarsAmount.zero } + filteredTransactions = currentTransactions.filter { $0.count.amount > StarsAmount.zero } case .outgoing: - filteredTransactions = currentTransactions.filter { $0.count < StarsAmount.zero } + filteredTransactions = currentTransactions.filter { $0.count.amount < StarsAmount.zero } } if filteredTransactions != initialTransactions { diff --git a/submodules/TelegramStringFormatting/Sources/TonFormat.swift b/submodules/TelegramStringFormatting/Sources/TonFormat.swift index c237580f34..1b40073f81 100644 --- a/submodules/TelegramStringFormatting/Sources/TonFormat.swift +++ b/submodules/TelegramStringFormatting/Sources/TonFormat.swift @@ -93,6 +93,15 @@ public func formatStarsAmountText(_ amount: StarsAmount, dateTimeFormat: Present return balanceText } +public func formatCurrencyAmountText(_ amount: CurrencyAmount, dateTimeFormat: PresentationDateTimeFormat, showPlus: Bool = false) -> String { + switch amount.currency { + case .stars: + return formatStarsAmountText(amount.amount, dateTimeFormat: dateTimeFormat, showPlus: showPlus) + case .ton: + return formatTonAmountText(amount.amount.value, dateTimeFormat: dateTimeFormat, showPlus: showPlus) + } +} + private let invalidAddressCharacters = CharacterSet(charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_=").inverted public func isValidTonAddress(_ address: String, exactLength: Bool = false) -> Bool { if address.count > walletAddressLength || address.rangeOfCharacter(from: invalidAddressCharacters) != nil { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift index cb03111948..fe4168a2ef 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift @@ -848,7 +848,7 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id, |> distinctUntilChanged } } - + let starsState: Signal if let starsContext { starsState = starsContext.state diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 08e1341526..0f88b26a3e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -1037,9 +1037,9 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p } } if let tonState = data.tonState { - if !isPremiumDisabled || tonState.balance != .zero { + if abs(tonState.balance.value) > 0 { let balanceText: NSAttributedString - if tonState.balance != .zero { + if abs(tonState.balance.value) > 0 { let formattedLabel = formatTonAmountText(tonState.balance.value, dateTimeFormat: presentationData.dateTimeFormat) let smallLabelFont = Font.regular(floor(presentationData.listsFontSize.itemListBaseFontSize / 17.0 * 13.0)) let labelFont = Font.regular(presentationData.listsFontSize.itemListBaseFontSize) @@ -1049,19 +1049,19 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p balanceText = NSAttributedString() } //TODO:localize - items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 105, label: .attributedText(balanceText), text: "TON", icon: PresentationResourcesSettings.ton, action: { + items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 103, label: .attributedText(balanceText), text: "My TON", icon: PresentationResourcesSettings.ton, action: { interaction.openSettings(.ton) })) } } if !isPremiumDisabled || context.isPremium { - items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 103, label: .text(""), additionalBadgeLabel: nil, text: presentationData.strings.Settings_Business, icon: PresentationResourcesSettings.business, action: { + items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 104, label: .text(""), additionalBadgeLabel: nil, text: presentationData.strings.Settings_Business, icon: PresentationResourcesSettings.business, action: { interaction.openSettings(.businessSetup) })) } if let starsState = data.starsState { if !isPremiumDisabled || starsState.balance > StarsAmount.zero { - items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 104, label: .text(""), text: presentationData.strings.Settings_SendGift, icon: PresentationResourcesSettings.premiumGift, action: { + items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 105, label: .text(""), text: presentationData.strings.Settings_SendGift, icon: PresentationResourcesSettings.premiumGift, action: { interaction.openSettings(.premiumGift) })) } @@ -10670,7 +10670,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } case .ton: if let tonContext = self.controller?.tonContext { - push(self.context.sharedContext.makeStarsTransactionsScreen(context: self.context, starsContext: tonContext)) + push(self.context.sharedContext.makeTonTransactionsScreen(context: self.context, tonContext: tonContext)) } } } @@ -12971,16 +12971,21 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc self.chatLocation = .peer(id: peerId) } - if isSettings, let starsContext = context.starsContext { - self.starsContext = starsContext - starsContext.load(force: true) + if isSettings { + if let starsContext = context.starsContext { + self.starsContext = starsContext + starsContext.load(force: true) + } else { + self.starsContext = nil + } + if let tonContext = context.tonContext { + self.tonContext = tonContext + tonContext.load(force: true) + } else { + self.tonContext = nil + } } else { self.starsContext = nil - } - if isSettings, let tonContext = context.tonContext { - self.tonContext = tonContext - tonContext.load(force: true) - } else { self.tonContext = nil } diff --git a/submodules/TelegramUI/Components/Premium/PremiumDiamondComponent/BUILD b/submodules/TelegramUI/Components/Premium/PremiumDiamondComponent/BUILD new file mode 100644 index 0000000000..bb9178dd5f --- /dev/null +++ b/submodules/TelegramUI/Components/Premium/PremiumDiamondComponent/BUILD @@ -0,0 +1,27 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "PremiumDiamondComponent", + module_name = "PremiumDiamondComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/ComponentFlow", + "//submodules/AccountContext", + "//submodules/AppBundle", + "//submodules/GZip", + "//submodules/LegacyComponents", + "//submodules/Components/MultilineTextComponent:MultilineTextComponent", + "//submodules/TelegramUI/Components/Premium/PremiumStarComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Premium/PremiumDiamondComponent/Sources/PremiumDiamondComponent.swift b/submodules/TelegramUI/Components/Premium/PremiumDiamondComponent/Sources/PremiumDiamondComponent.swift new file mode 100644 index 0000000000..0aeb9e849f --- /dev/null +++ b/submodules/TelegramUI/Components/Premium/PremiumDiamondComponent/Sources/PremiumDiamondComponent.swift @@ -0,0 +1,309 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import SwiftSignalKit +import SceneKit +import GZip +import AppBundle +import LegacyComponents +import PremiumStarComponent + +private let sceneVersion: Int = 5 + +private func deg2rad(_ number: Float) -> Float { + return number * .pi / 180 +} + +private func rad2deg(_ number: Float) -> Float { + return number * 180.0 / .pi +} + +public final class PremiumDiamondComponent: Component { + public init() { + } + + public static func ==(lhs: PremiumDiamondComponent, rhs: PremiumDiamondComponent) -> Bool { + return true + } + + public final class View: UIView, SCNSceneRendererDelegate, ComponentTaggedView { + public final class Tag { + public init() { + + } + } + + public func matches(tag: Any) -> Bool { + if let _ = tag as? Tag { + return true + } + return false + } + + private var _ready = Promise() + public var ready: Signal { + return self._ready.get() + } + + weak var animateFrom: UIView? + weak var containerView: UIView? + + private let sceneView: SCNView + + private var timer: SwiftSignalKit.Timer? + + private var component: PremiumDiamondComponent? + + override init(frame: CGRect) { + self.sceneView = SCNView(frame: CGRect(origin: .zero, size: CGSize(width: 64.0, height: 64.0))) + self.sceneView.backgroundColor = .clear + self.sceneView.transform = CGAffineTransform(scaleX: 0.5, y: 0.5) + self.sceneView.isUserInteractionEnabled = false + self.sceneView.preferredFramesPerSecond = 60 + self.sceneView.isJitteringEnabled = true + + super.init(frame: frame) + + self.addSubview(self.sceneView) + + self.setup() + + let panGestureRecoginzer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:))) + self.addGestureRecognizer(panGestureRecoginzer) + + self.disablesInteractiveModalDismiss = true + self.disablesInteractiveTransitionGestureRecognizer = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.timer?.invalidate() + } + + private var previousYaw: Float = 0.0 + @objc private func handlePan(_ gesture: UIPanGestureRecognizer) { + guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else { + return + } + + let keys = [ + "rotate", + "tapRotate", + "continuousRotation" + ] + + for key in keys { + node.removeAnimation(forKey: key) + } + + switch gesture.state { + case .began: + self.previousYaw = 0.0 + case .changed: + let translation = gesture.translation(in: gesture.view) + let yawPan = deg2rad(Float(translation.x)) + + func rubberBandingOffset(offset: CGFloat, bandingStart: CGFloat) -> CGFloat { + let bandedOffset = offset - bandingStart + let range: CGFloat = 60.0 + let coefficient: CGFloat = 0.4 + return bandingStart + (1.0 - (1.0 / ((bandedOffset * coefficient / range) + 1.0))) * range + } + + var pitchTranslation = rubberBandingOffset(offset: abs(translation.y), bandingStart: 0.0) + if translation.y < 0.0 { + pitchTranslation *= -1.0 + } + let pitchPan = deg2rad(Float(pitchTranslation)) + + self.previousYaw = yawPan + // Maintain the initial tilt while adding pan gestures + let initialTiltX: Float = deg2rad(-15.0) + let initialTiltZ: Float = deg2rad(5.0) + node.eulerAngles = SCNVector3(initialTiltX + pitchPan, yawPan, initialTiltZ) + case .ended: + let velocity = gesture.velocity(in: gesture.view) + + var smallAngle = false + if (self.previousYaw < .pi / 2 && self.previousYaw > -.pi / 2) && abs(velocity.x) < 200 { + smallAngle = true + } + + self.playAppearanceAnimation(velocity: velocity.x, smallAngle: smallAngle, explode: !smallAngle && abs(velocity.x) > 600) + default: + break + } + } + + private func setup() { + guard let scene = loadCompressedScene(name: "diamond", version: sceneVersion) else { + return + } + + self.sceneView.scene = scene + self.sceneView.delegate = self + + let _ = self.sceneView.snapshot() + } + + private var didSetReady = false + public func renderer(_ renderer: SCNSceneRenderer, didRenderScene scene: SCNScene, atTime time: TimeInterval) { + if !self.didSetReady { + self.didSetReady = true + + Queue.mainQueue().justDispatch { + self._ready.set(.single(true)) + self.onReady() + } + } + } + + private func onReady() { + self.playAppearanceAnimation(mirror: true, explode: true) + } + + private func playAppearanceAnimation(velocity: CGFloat? = nil, smallAngle: Bool = false, mirror: Bool = false, explode: Bool = false) { + guard let scene = self.sceneView.scene else { + return + } + + if explode, let node = scene.rootNode.childNode(withName: "swirl", recursively: false), let particlesLeft = scene.rootNode.childNode(withName: "particles_left", recursively: false), let particlesRight = scene.rootNode.childNode(withName: "particles_right", recursively: false), let particlesBottomLeft = scene.rootNode.childNode(withName: "particles_left_bottom", recursively: false), let particlesBottomRight = scene.rootNode.childNode(withName: "particles_right_bottom", recursively: false) { + if let leftParticleSystem = particlesLeft.particleSystems?.first, let rightParticleSystem = particlesRight.particleSystems?.first, let leftBottomParticleSystem = particlesBottomLeft.particleSystems?.first, let rightBottomParticleSystem = particlesBottomRight.particleSystems?.first { + leftParticleSystem.speedFactor = 2.0 + leftParticleSystem.particleVelocity = 1.6 + leftParticleSystem.birthRate = 60.0 + leftParticleSystem.particleLifeSpan = 4.0 + + rightParticleSystem.speedFactor = 2.0 + rightParticleSystem.particleVelocity = 1.6 + rightParticleSystem.birthRate = 60.0 + rightParticleSystem.particleLifeSpan = 4.0 + + leftBottomParticleSystem.particleVelocity = 1.6 + leftBottomParticleSystem.birthRate = 24.0 + leftBottomParticleSystem.particleLifeSpan = 7.0 + + rightBottomParticleSystem.particleVelocity = 1.6 + rightBottomParticleSystem.birthRate = 24.0 + rightBottomParticleSystem.particleLifeSpan = 7.0 + + node.physicsField?.isActive = true + Queue.mainQueue().after(1.0) { + node.physicsField?.isActive = false + + leftParticleSystem.birthRate = 15.0 + leftParticleSystem.particleVelocity = 1.0 + leftParticleSystem.particleLifeSpan = 3.0 + + rightParticleSystem.birthRate = 15.0 + rightParticleSystem.particleVelocity = 1.0 + rightParticleSystem.particleLifeSpan = 3.0 + + leftBottomParticleSystem.particleVelocity = 1.0 + leftBottomParticleSystem.birthRate = 10.0 + leftBottomParticleSystem.particleLifeSpan = 5.0 + + rightBottomParticleSystem.particleVelocity = 1.0 + rightBottomParticleSystem.birthRate = 10.0 + rightBottomParticleSystem.particleLifeSpan = 5.0 + + let leftAnimation = POPBasicAnimation() + leftAnimation.property = (POPAnimatableProperty.property(withName: "speedFactor", initializer: { property in + property?.readBlock = { particleSystem, values in + values?.pointee = (particleSystem as! SCNParticleSystem).speedFactor + } + property?.writeBlock = { particleSystem, values in + (particleSystem as! SCNParticleSystem).speedFactor = values!.pointee + } + property?.threshold = 0.01 + }) as! POPAnimatableProperty) + leftAnimation.fromValue = 1.2 as NSNumber + leftAnimation.toValue = 0.85 as NSNumber + leftAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear) + leftAnimation.duration = 0.5 + leftParticleSystem.pop_add(leftAnimation, forKey: "speedFactor") + + let rightAnimation = POPBasicAnimation() + rightAnimation.property = (POPAnimatableProperty.property(withName: "speedFactor", initializer: { property in + property?.readBlock = { particleSystem, values in + values?.pointee = (particleSystem as! SCNParticleSystem).speedFactor + } + property?.writeBlock = { particleSystem, values in + (particleSystem as! SCNParticleSystem).speedFactor = values!.pointee + } + property?.threshold = 0.01 + }) as! POPAnimatableProperty) + rightAnimation.fromValue = 1.2 as NSNumber + rightAnimation.toValue = 0.85 as NSNumber + rightAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear) + rightAnimation.duration = 0.5 + rightParticleSystem.pop_add(rightAnimation, forKey: "speedFactor") + } + } + } + +// var from = node.presentation.eulerAngles +// if abs(from.y - .pi * 2.0) < 0.001 { +// from.y = 0.0 +// } +// node.removeAnimation(forKey: "tapRotate") +// +// var toValue: Float = smallAngle ? 0.0 : .pi * 2.0 +// if let velocity = velocity, !smallAngle && abs(velocity) > 200 && velocity < 0.0 { +// toValue *= -1 +// } +// if mirror { +// toValue *= -1 +// } +// +// +// let to = SCNVector3(x: from.x, y: toValue, z: from.z) +// let distance = rad2deg(to.y - from.y) +// +// guard !distance.isZero else { +// Queue.mainQueue().after(0.1) { [weak self] in +// self?.setupContinuousRotation() +// } +// return +// } +// +// let springAnimation = CASpringAnimation(keyPath: "eulerAngles") +// springAnimation.fromValue = NSValue(scnVector3: from) +// springAnimation.toValue = NSValue(scnVector3: to) +// springAnimation.mass = 1.0 +// springAnimation.stiffness = 21.0 +// springAnimation.damping = 5.8 +// springAnimation.duration = springAnimation.settlingDuration * 0.75 +// springAnimation.initialVelocity = velocity.flatMap { abs($0 / CGFloat(distance)) } ?? 1.7 +// springAnimation.completion = { [weak self] finished in +// if finished { +// self?.setupContinuousRotation() +// } +// } +// node.addAnimation(springAnimation, forKey: "rotate") + } + + func update(component: PremiumDiamondComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { + self.component = component + + self.sceneView.bounds = CGRect(origin: .zero, size: CGSize(width: availableSize.width * 2.0, height: availableSize.height * 2.0)) + if self.sceneView.superview == self { + self.sceneView.center = CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0) + } + + return availableSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift b/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift index 5711ab370d..0d53714a56 100644 --- a/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift @@ -372,42 +372,36 @@ public final class StarsAvatarComponent: Component { } public final class StarsLabelComponent: CombinedComponent { - let theme: PresentationTheme - let currency: CurrencyAmount.Currency - let textColor: UIColor let text: NSAttributedString let subtext: NSAttributedString? + let iconName: String + let iconColor: UIColor? public init( - theme: PresentationTheme, - currency: CurrencyAmount.Currency, - textColor: UIColor, text: NSAttributedString, - subtext: NSAttributedString? = nil + subtext: NSAttributedString? = nil, + iconName: String = "Premium/Stars/StarMedium", + iconColor: UIColor? = nil ) { - self.currency = currency - self.theme = theme - self.textColor = textColor self.text = text self.subtext = subtext + self.iconName = iconName + self.iconColor = iconColor } public static func ==(lhs: StarsLabelComponent, rhs: StarsLabelComponent) -> Bool { - if lhs.currency != rhs.currency { - return false - } - if lhs.theme !== rhs.theme { - return false - } - if lhs.textColor != rhs.textColor { - return false - } if lhs.text != rhs.text { return false } if lhs.subtext != rhs.subtext { return false } + if lhs.iconName != rhs.iconName { + return false + } + if lhs.iconColor != rhs.iconColor { + return false + } return true } @@ -424,6 +418,7 @@ public final class StarsLabelComponent: CombinedComponent { availableSize: CGSize(width: 140.0, height: 40.0), transition: context.transition ) + var subtext: _UpdatedChildComponent? = nil if let sublabel = component.subtext { @@ -434,12 +429,11 @@ public final class StarsLabelComponent: CombinedComponent { ) } - let iconSize: CGSize = component.currency == .ton ? CGSize(width: 16.0, height: 16.0) : CGSize(width: 20.0, height: 20.0) + let iconSize = CGSize(width: 20.0, height: 20.0) let icon = icon.update( component: BundleIconComponent( - name: component.currency == .ton ? "Ads/TonBig" : "Premium/Stars/StarMedium", - tintColor: component.currency == .ton ? component.textColor : nil, - maxSize: iconSize + name: component.iconName, + tintColor: component.iconColor ), availableSize: iconSize, transition: context.transition diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift index 865ebaccdd..8da430af80 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift @@ -217,7 +217,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { var statusText: String? var statusIsDestructive = false - let count: CurrencyAmount + let count: StarsAmount var countIsGeneric = false var countOnTop = false var transactionId: String? @@ -257,7 +257,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { titleText = strings.Stars_Transaction_Giveaway_Boost_Stars(Int32(stars)) descriptionText = "" boostsText = strings.Stars_Transaction_Giveaway_Boost_Boosts(boosts) - count = CurrencyAmount(amount: StarsAmount(value: stars, nanos: 0), currency: .stars) + count = StarsAmount(value: stars, nanos: 0) date = boost.date toPeer = state.peerMap[peerId] giveawayMessageId = boost.giveawayMessageId @@ -266,7 +266,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { let usdValue = formatTonUsdValue(pricing.amount.value, divide: false, rate: usdRate, dateTimeFormat: environment.dateTimeFormat) titleText = strings.Stars_Transaction_Subscription_Title descriptionText = strings.Stars_Transaction_Subscription_PerMonthUsd(usdValue).string - count = CurrencyAmount(amount: pricing.amount, currency: .stars) + count = pricing.amount countOnTop = true date = importer.date toPeer = importer.peer.peer.flatMap(EnginePeer.init) @@ -288,7 +288,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { photo = subscription.photo descriptionText = "" - count = CurrencyAmount(amount: subscription.pricing.amount, currency: .stars) + count = subscription.pricing.amount date = subscription.untilDate if let creationDate = (subscription.peer._asPeer() as? TelegramChannel)?.creationDate, creationDate > 0 { additionalDate = creationDate @@ -376,7 +376,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { titleText = gift.title descriptionText = "\(strings.Gift_Unique_Collectible) #\(presentationStringsFormattedNumber(gift.number, dateTimeFormat.groupingSeparator))" } - count = CurrencyAmount(amount: transaction.count, currency: transaction.currency) + count = transaction.count.amount transactionId = transaction.id date = transaction.date if case let .peer(peer) = transaction.peer { @@ -395,7 +395,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { } else if let giveawayMessageIdValue = transaction.giveawayMessageId { titleText = strings.Stars_Transaction_Giveaway_Title descriptionText = "" - count = CurrencyAmount(amount: transaction.count, currency: transaction.currency) + count = transaction.count.amount transactionId = transaction.id date = transaction.date giveawayMessageId = giveawayMessageIdValue @@ -406,7 +406,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { } else if let _ = transaction.subscriptionPeriod { titleText = strings.Stars_Transaction_SubscriptionFee descriptionText = "" - count = CurrencyAmount(amount: transaction.count, currency: transaction.currency) + count = transaction.count.amount transactionId = transaction.id date = transaction.date if case let .peer(peer) = transaction.peer { @@ -417,7 +417,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { } else if transaction.flags.contains(.isGift) { titleText = strings.Stars_Gift_Received_Title descriptionText = strings.Stars_Gift_Received_Text - count = CurrencyAmount(amount: transaction.count, currency: transaction.currency) + count = transaction.count.amount countOnTop = true transactionId = transaction.id date = transaction.date @@ -446,7 +446,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { countOnTop = false descriptionText = "" } - count = CurrencyAmount(amount: transaction.count, currency: transaction.currency) + count = transaction.count.amount transactionId = transaction.id date = transaction.date transactionPeer = transaction.peer @@ -457,7 +457,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { titleText = strings.Stars_Transaction_Reaction_Title descriptionText = "" messageId = transaction.paidMessageId - count = CurrencyAmount(amount: transaction.count, currency: transaction.currency) + count = transaction.count.amount transactionId = transaction.id date = transaction.date if case let .peer(peer) = transaction.peer { @@ -490,7 +490,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { via = strings.Stars_Transaction_PremiumBotTopUp_Subtitle case .fragment: if parentPeer.id == component.context.account.peerId { - if (transaction.count.value < 0 && !transaction.flags.contains(.isRefund)) || (transaction.count.value > 0 && transaction.flags.contains(.isRefund)) { + if (transaction.count.amount.value < 0 && !transaction.flags.contains(.isRefund)) || (transaction.count.amount.value > 0 && transaction.flags.contains(.isRefund)) { titleText = strings.Stars_Transaction_FragmentWithdrawal_Title via = strings.Stars_Transaction_FragmentWithdrawal_Subtitle } else { @@ -545,7 +545,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { messageId = transaction.paidMessageId - count = CurrencyAmount(amount: transaction.count, currency: transaction.currency) + count = transaction.count.amount transactionId = transaction.id date = transaction.date if case let .peer(peer) = transaction.peer { @@ -564,7 +564,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { case let .receipt(receipt): titleText = receipt.invoiceMedia.title descriptionText = receipt.invoiceMedia.description - count = CurrencyAmount(amount: StarsAmount(value: (receipt.invoice.prices.first?.amount ?? receipt.invoiceMedia.totalAmount) * -1, nanos: 0), currency: .stars) + count = StarsAmount(value: (receipt.invoice.prices.first?.amount ?? receipt.invoiceMedia.totalAmount) * -1, nanos: 0) transactionId = receipt.transactionId date = receipt.date if let peer = state.peerMap[receipt.botPaymentId] { @@ -581,7 +581,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { if case let .giftStars(_, _, countValue, _, _, _) = action.action { titleText = incoming ? strings.Stars_Gift_Received_Title : strings.Stars_Gift_Sent_Title - count = CurrencyAmount(amount: StarsAmount(value: countValue, nanos: 0), currency: .stars) + count = StarsAmount(value: countValue, nanos: 0) if !incoming { countIsGeneric = true } @@ -595,7 +595,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { } else if case let .prizeStars(countValue, _, boostPeerId, _, giveawayMessageIdValue) = action.action { titleText = strings.Stars_Transaction_Giveaway_Title - count = CurrencyAmount(amount: StarsAmount(value: countValue, nanos: 0), currency: .stars) + count = StarsAmount(value: countValue, nanos: 0) countOnTop = true transactionId = nil giveawayMessageId = giveawayMessageIdValue @@ -648,14 +648,8 @@ private final class StarsTransactionSheetContent: CombinedComponent { headerTextColor = theme.actionSheet.primaryTextColor } - let absCount = StarsAmount(value: abs(count.amount.value), nanos: abs(count.amount.nanos)) - let formattedAmount: String - switch count.currency { - case .stars: - formattedAmount = formatStarsAmountText(absCount, dateTimeFormat: dateTimeFormat) - case .ton: - formattedAmount = formatTonAmountText(absCount.value, dateTimeFormat: dateTimeFormat) - } + let absCount = StarsAmount(value: abs(count.value), nanos: abs(count.nanos)) + let formattedAmount = formatStarsAmountText(absCount, dateTimeFormat: dateTimeFormat) let countColor: UIColor var countFont: UIFont = isSubscription || isSubscriber ? Font.regular(17.0) : Font.semibold(17.0) var countBackgroundColor: UIColor? @@ -670,7 +664,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { } else if countIsGeneric { amountText = "\(formattedAmount)" countColor = theme.list.itemPrimaryTextColor - } else if count.amount < StarsAmount.zero { + } else if count < StarsAmount.zero { amountText = "- \(formattedAmount)" if case .unique = giftAnimationSubject { countColor = .white @@ -712,9 +706,9 @@ private final class StarsTransactionSheetContent: CombinedComponent { imageSubject = .gift(premiumGiftMonths) } else if isGift { var value: Int32 = 3 - if count.amount.value <= 1000 { + if count.value <= 1000 { value = 3 - } else if count.amount.value < 2500 { + } else if count.value < 2500 { value = 6 } else { value = 12 @@ -732,9 +726,9 @@ private final class StarsTransactionSheetContent: CombinedComponent { imageSubject = .none } if isSubscription || isSubscriber || isSubscriptionFee || giveawayMessageId != nil { - imageIcon = count.currency == .ton ? .ton : .star + imageIcon = .star } else { - imageIcon = count.currency == .ton ? .ton : nil + imageIcon = nil } if isSubscription && "".isEmpty { @@ -817,26 +811,10 @@ private final class StarsTransactionSheetContent: CombinedComponent { transition: .immediate ) - let amountStarIconName: String - var amountStarTintColor: UIColor? - var amountStarMaxSize: CGSize? - var amountOffset = CGPoint() - if boostsText != nil { - amountStarIconName = "Premium/BoostButtonIcon" - } else if case .ton = count.currency { - amountStarIconName = "Ads/TonBig" - amountStarTintColor = countColor - amountStarMaxSize = CGSize(width: 14.0, height: 14.0) - amountOffset.y += 3.0 - } else { - amountStarIconName = "Premium/Stars/StarMedium" - } - let amountStar = amountStar.update( component: BundleIconComponent( - name: amountStarIconName, - tintColor: amountStarTintColor, - maxSize: amountStarMaxSize + name: boostsText != nil ? "Premium/BoostButtonIcon" : "Premium/Stars/StarMedium", + tintColor: nil ), availableSize: context.availableSize, transition: .immediate @@ -858,7 +836,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { )) } else if case .unique = giftAnimationSubject { let reason: String - if count.amount < StarsAmount.zero, case let .transaction(transaction, _) = subject { + if count < StarsAmount.zero, case let .transaction(transaction, _) = subject { if transaction.flags.contains(.isStarGiftResale) { reason = strings.Stars_Transaction_GiftPurchase } else { @@ -914,7 +892,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { } else if isSubscriber { title = strings.Stars_Transaction_Subscription_Subscriber } else { - title = count.amount < StarsAmount.zero || countIsGeneric ? strings.Stars_Transaction_To : strings.Stars_Transaction_From + title = count < StarsAmount.zero || countIsGeneric ? strings.Stars_Transaction_To : strings.Stars_Transaction_From } let toComponent: AnyComponent @@ -1019,7 +997,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { id: "prize", title: strings.Stars_Transaction_Giveaway_Prize, component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Stars_Transaction_Giveaway_Stars(Int32(count.amount.value)), font: tableFont, textColor: tableTextColor))) + MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Stars_Transaction_Giveaway_Stars(Int32(count.value)), font: tableFont, textColor: tableTextColor))) ) )) @@ -1195,7 +1173,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { } if let starrefCommissionPermille = transaction.starrefCommissionPermille, transaction.starrefPeerId != nil { if transaction.flags.contains(.isPaidMessage) || transaction.flags.contains(.isStarGiftResale) { - var totalStars = transaction.count + var totalStars = transaction.count.amount if let starrefCount = transaction.starrefAmount { totalStars = totalStars + starrefCount } @@ -1521,7 +1499,6 @@ private final class StarsTransactionSheetContent: CombinedComponent { amountLabelOffsetY = 2.0 amountStarOffsetY = 5.0 } - amountStarOffsetY += amountOffset.y context.add(amount .position(CGPoint(x: amountLabelOriginX, y: amountOrigin + amount.size.height / 2.0 + amountLabelOffsetY)) diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/BUILD b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/BUILD index 393e97bd63..d71aef4f9c 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/BUILD +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/BUILD @@ -50,6 +50,8 @@ swift_library( "//submodules/TelegramUI/Components/LottieComponent", "//submodules/TelegramUI/Components/LottieComponentResourceContent", "//submodules/TelegramUI/Components/Gifts/GiftAnimationComponent", + "//submodules/TelegramUI/Components/Premium/PremiumCoinComponent", + "//submodules/TelegramUI/Components/Premium/PremiumDiamondComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift index ffea44a602..8753da0912 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift @@ -16,8 +16,8 @@ final class StarsBalanceComponent: Component { let theme: PresentationTheme let strings: PresentationStrings let dateTimeFormat: PresentationDateTimeFormat - let currency: CurrencyAmount.Currency let count: StarsAmount + let isTon: Bool let rate: Double? let actionTitle: String let actionAvailable: Bool @@ -35,8 +35,8 @@ final class StarsBalanceComponent: Component { theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, - currency: CurrencyAmount.Currency, count: StarsAmount, + isTon: Bool = false, rate: Double?, actionTitle: String, actionAvailable: Bool, @@ -53,8 +53,8 @@ final class StarsBalanceComponent: Component { self.theme = theme self.strings = strings self.dateTimeFormat = dateTimeFormat - self.currency = currency self.count = count + self.isTon = isTon self.rate = rate self.actionTitle = actionTitle self.actionAvailable = actionAvailable @@ -100,6 +100,9 @@ final class StarsBalanceComponent: Component { if lhs.count != rhs.count { return false } + if lhs.isTon != rhs.isTon { + return false + } if lhs.rate != rhs.rate { return false } @@ -123,8 +126,6 @@ final class StarsBalanceComponent: Component { override init(frame: CGRect) { super.init(frame: frame) - self.icon.image = UIImage(bundleImageName: "Premium/Stars/BalanceStar") - self.addSubview(self.icon) } @@ -133,6 +134,14 @@ final class StarsBalanceComponent: Component { } func update(component: StarsBalanceComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + if self.component == nil { + if component.isTon { + self.icon.image = generateTintedImage(image: UIImage(bundleImageName: "Ads/TonBig"), color: component.theme.list.itemAccentColor) + } else { + self.icon.image = UIImage(bundleImageName: "Premium/Stars/BalanceStar") + } + } + self.component = component self.state = state @@ -168,11 +177,10 @@ final class StarsBalanceComponent: Component { var contentHeight: CGFloat = sideInset let formattedLabel: String - switch component.currency { - case .stars: - formattedLabel = formatStarsAmountText(component.count, dateTimeFormat: component.dateTimeFormat) - case .ton: + if component.isTon { formattedLabel = formatTonAmountText(component.count.value, dateTimeFormat: component.dateTimeFormat) + } else { + formattedLabel = formatStarsAmountText(component.count, dateTimeFormat: component.dateTimeFormat) } let labelFont: UIFont if formattedLabel.contains(component.dateTimeFormat.decimalSeparator) { @@ -197,13 +205,13 @@ final class StarsBalanceComponent: Component { self.addSubview(titleView) } if let icon = self.icon.image { - let spacing: CGFloat = 3.0 + let spacing: CGFloat = 4.0 let totalWidth = titleSize.width + icon.size.width + spacing let origin = floorToScreenPixels((availableSize.width - totalWidth) / 2.0) let titleFrame = CGRect(origin: CGPoint(x: origin + icon.size.width + spacing, y: contentHeight - 3.0), size: titleSize) titleView.frame = titleFrame - - self.icon.frame = CGRect(origin: CGPoint(x: origin, y: contentHeight), size: icon.size) + + self.icon.frame = CGRect(origin: CGPoint(x: origin, y: floorToScreenPixels(titleFrame.midY - icon.size.height / 2.0)), size: icon.size) } } contentHeight += titleSize.height diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift index c70cf69b24..768dfd30c5 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift @@ -21,7 +21,7 @@ import TelegramStringFormatting private extension StarsContext.State.Transaction { var extendedId: String { - if self.count > StarsAmount.zero { + if self.count.amount > StarsAmount.zero { return "\(id)_in" } else { return "\(id)_out" @@ -320,7 +320,7 @@ final class StarsTransactionsListPanelComponent: Component { switch starGift { case let .generic(gift): itemFile = gift.file - itemSubtitle = item.count > StarsAmount.zero ? environment.strings.Stars_Intro_Transaction_ConvertedGift : environment.strings.Stars_Intro_Transaction_Gift + itemSubtitle = item.count.amount > StarsAmount.zero ? environment.strings.Stars_Intro_Transaction_ConvertedGift : environment.strings.Stars_Intro_Transaction_Gift case let .unique(gift): for attribute in gift.attributes { if case let .model(_, file, _) = attribute { @@ -328,7 +328,7 @@ final class StarsTransactionsListPanelComponent: Component { break } } - if item.count > StarsAmount.zero { + if item.count.amount > StarsAmount.zero { itemSubtitle = environment.strings.Stars_Intro_Transaction_GiftSale } else { if item.flags.contains(.isStarGiftResale) { @@ -373,7 +373,7 @@ final class StarsTransactionsListPanelComponent: Component { itemSubtitle = environment.strings.Stars_Intro_Transaction_Gift_Title itemPeer = .fragment } else { - if (item.count.value < 0 && !item.flags.contains(.isRefund)) || (item.count.value > 0 && item.flags.contains(.isRefund)) { + if (item.count.amount.value < 0 && !item.flags.contains(.isRefund)) || (item.count.amount.value > 0 && item.flags.contains(.isRefund)) { itemTitle = environment.strings.Stars_Intro_Transaction_FragmentWithdrawal_Title itemSubtitle = environment.strings.Stars_Intro_Transaction_FragmentWithdrawal_Subtitle } else { @@ -382,7 +382,7 @@ final class StarsTransactionsListPanelComponent: Component { } } } else { - if item.count > StarsAmount.zero && !item.flags.contains(.isRefund) { + if item.count.amount > StarsAmount.zero && !item.flags.contains(.isRefund) { itemTitle = environment.strings.Stars_Intro_Transaction_FragmentTopUp_Title itemSubtitle = environment.strings.Stars_Intro_Transaction_FragmentTopUp_Subtitle } else { @@ -409,19 +409,24 @@ final class StarsTransactionsListPanelComponent: Component { } let itemLabel: NSAttributedString - let formattedLabel: String - switch item.currency { - case .stars: - formattedLabel = formatStarsAmountText(item.count, dateTimeFormat: environment.dateTimeFormat, showPlus: true) - case .ton: - formattedLabel = formatTonAmountText(item.count.value, dateTimeFormat: environment.dateTimeFormat, showPlus: true) - } + let formattedLabel = formatCurrencyAmountText(item.count, dateTimeFormat: environment.dateTimeFormat, showPlus: true) let smallLabelFont = Font.with(size: floor(fontBaseDisplaySize / 17.0 * 13.0)) let labelFont = Font.medium(fontBaseDisplaySize) let labelColor = formattedLabel.hasPrefix("-") ? environment.theme.list.itemDestructiveColor : environment.theme.list.itemDisclosureActions.constructive.fillColor itemLabel = tonAmountAttributedString(formattedLabel, integralFont: labelFont, fractionalFont: smallLabelFont, color: labelColor, decimalSeparator: environment.dateTimeFormat.decimalSeparator) + let itemIconName: String + let itemIconColor: UIColor? + switch item.count.currency { + case .stars: + itemIconName = "Premium/Stars/StarMedium" + itemIconColor = nil + case .ton: + itemIconName = "Ads/TonAbout" + itemIconColor = labelColor + } + var itemDateColor = environment.theme.list.itemSecondaryTextColor itemDate = stringForMediumCompactDate(timestamp: item.date, strings: environment.strings, dateTimeFormat: environment.dateTimeFormat) if item.flags.contains(.isRefund) { @@ -502,7 +507,7 @@ final class StarsTransactionsListPanelComponent: Component { contentInsets: UIEdgeInsets(top: 9.0, left: environment.containerInsets.left, bottom: 8.0, right: environment.containerInsets.right), leftIcon: .custom(AnyComponentWithIdentity(id: "avatar", component: AnyComponent(StarsAvatarComponent(context: component.context, theme: environment.theme, peer: itemPeer, photo: item.photo, media: item.media, uniqueGift: uniqueGift, backgroundColor: environment.theme.list.plainBackgroundColor))), false), icon: nil, - accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: "label", component: AnyComponent(StarsLabelComponent(theme: environment.theme, currency: item.currency, textColor: labelColor, text: itemLabel))), insets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 16.0))), + accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: "label", component: AnyComponent(StarsLabelComponent(text: itemLabel, iconName: itemIconName, iconColor: itemIconColor))), insets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 16.0))), action: { [weak self] _ in guard let self, let component = self.component else { return diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/TonTransactionsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/TonTransactionsScreen.swift new file mode 100644 index 0000000000..c2da4898b0 --- /dev/null +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/TonTransactionsScreen.swift @@ -0,0 +1,1055 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import SwiftSignalKit +import ViewControllerComponent +import ComponentDisplayAdapters +import TelegramPresentationData +import AccountContext +import TelegramCore +import Postbox +import MultilineTextComponent +import BalancedTextComponent +import Markdown +import PremiumStarComponent +import ListSectionComponent +import BundleIconComponent +import TextFormat +import UndoUI +import ListActionItemComponent +import StarsAvatarComponent +import TelegramStringFormatting +import ListItemComponentAdaptor +import ItemListUI +import StarsWithdrawalScreen +import PremiumDiamondComponent + +final class TonTransactionsScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let tonContext: StarsContext + let openTransaction: (StarsContext.State.Transaction) -> Void + let buy: () -> Void + let withdraw: () -> Void + let showTimeoutTooltip: (Int32) -> Void + let gift: () -> Void + + init( + context: AccountContext, + starsContext: StarsContext, + openTransaction: @escaping (StarsContext.State.Transaction) -> Void, + buy: @escaping () -> Void, + withdraw: @escaping () -> Void, + showTimeoutTooltip: @escaping (Int32) -> Void, + gift: @escaping () -> Void + ) { + self.context = context + self.tonContext = starsContext + self.openTransaction = openTransaction + self.buy = buy + self.withdraw = withdraw + self.showTimeoutTooltip = showTimeoutTooltip + self.gift = gift + } + + static func ==(lhs: TonTransactionsScreenComponent, rhs: TonTransactionsScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.tonContext !== rhs.tonContext { + return false + } + return true + } + + private final class ScrollViewImpl: UIScrollView { + override func touchesShouldCancel(in view: UIView) -> Bool { + return true + } + + override var contentOffset: CGPoint { + set(value) { + var value = value + if value.y > self.contentSize.height - self.bounds.height { + value.y = max(0.0, self.contentSize.height - self.bounds.height) + self.bounces = false + } else { + self.bounces = true + } + super.contentOffset = value + } get { + return super.contentOffset + } + } + } + + class View: UIView, UIScrollViewDelegate { + private let scrollView: ScrollViewImpl + + private var currentSelectedPanelId: AnyHashable? + + private let navigationBackgroundView: BlurredBackgroundView + private let navigationSeparatorLayer: SimpleLayer + private let navigationSeparatorLayerContainer: SimpleLayer + + private let scrollContainerView: UIView + + private let overscroll = ComponentView() + private let fade = ComponentView() + private let starView = ComponentView() + private let titleView = ComponentView() + private let descriptionView = ComponentView() + + private let balanceView = ComponentView() + private let earnStarsSection = ComponentView() + + private let topBalanceTitleView = ComponentView() + private let topBalanceValueView = ComponentView() + private let topBalanceIconView = ComponentView() + + private let panelContainer = ComponentView() + + private var component: TonTransactionsScreenComponent? + private weak var state: EmptyComponentState? + private var navigationMetrics: (navigationHeight: CGFloat, statusBarHeight: CGFloat)? + private var controller: (() -> ViewController?)? + + private var enableVelocityTracking: Bool = false + private var previousVelocityM1: CGFloat = 0.0 + private var previousVelocity: CGFloat = 0.0 + + private var listIsExpanded = false + + private var ignoreScrolling: Bool = false + + private var stateDisposable: Disposable? + private var starsState: StarsContext.State? + + private var previousBalance: StarsAmount? + + private var allTransactionsContext: StarsTransactionsContext? + private var incomingTransactionsContext: StarsTransactionsContext? + private var outgoingTransactionsContext: StarsTransactionsContext? + + override init(frame: CGRect) { + self.navigationBackgroundView = BlurredBackgroundView(color: nil, enableBlur: true) + self.navigationBackgroundView.alpha = 0.0 + + self.navigationSeparatorLayer = SimpleLayer() + self.navigationSeparatorLayer.opacity = 0.0 + self.navigationSeparatorLayerContainer = SimpleLayer() + self.navigationSeparatorLayerContainer.opacity = 0.0 + + self.scrollContainerView = UIView() + self.scrollView = ScrollViewImpl() + + super.init(frame: frame) + + 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.scrollsToTop = false + self.scrollView.delegate = self + self.scrollView.clipsToBounds = true + self.addSubview(self.scrollView) + + self.scrollView.addSubview(self.scrollContainerView) + + self.addSubview(self.navigationBackgroundView) + + self.navigationSeparatorLayerContainer.addSublayer(self.navigationSeparatorLayer) + self.layer.addSublayer(self.navigationSeparatorLayerContainer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.stateDisposable?.dispose() + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + guard let result = super.hitTest(point, with: event) else { + return nil + } + var currentParent: UIView? = result + while true { + if currentParent == nil || currentParent === self { + break + } + if let scrollView = currentParent as? UIScrollView { + if scrollView === self.scrollView { + break + } + if scrollView.isDecelerating && scrollView.contentOffset.y < -scrollView.contentInset.top { + return self.scrollView + } + } + currentParent = currentParent?.superview + } + return result + } + + func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + self.enableVelocityTracking = true + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + guard !self.ignoreScrolling else { + return + } + if self.enableVelocityTracking { + self.previousVelocityM1 = self.previousVelocity + if let value = (scrollView.value(forKey: (["_", "verticalVelocity"] as [String]).joined()) as? NSNumber)?.doubleValue { + self.previousVelocity = CGFloat(value) + } + } + + self.updateScrolling(transition: .immediate) + } + + func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + guard let navigationMetrics = self.navigationMetrics else { + return + } + + if let panelContainerView = self.panelContainer.view as? StarsTransactionsPanelContainerComponent.View { + let paneAreaExpansionFinalPoint: CGFloat = panelContainerView.frame.minY - navigationMetrics.navigationHeight + if abs(scrollView.contentOffset.y - paneAreaExpansionFinalPoint) < .ulpOfOne { + panelContainerView.transferVelocity(self.previousVelocityM1) + } + } + } + + func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { + guard let _ = self.navigationMetrics else { + return + } + + let paneAreaExpansionDistance: CGFloat = 32.0 + let paneAreaExpansionFinalPoint: CGFloat = scrollView.contentSize.height - scrollView.bounds.height + if targetContentOffset.pointee.y > paneAreaExpansionFinalPoint - paneAreaExpansionDistance && targetContentOffset.pointee.y < paneAreaExpansionFinalPoint { + targetContentOffset.pointee.y = paneAreaExpansionFinalPoint + self.enableVelocityTracking = false + self.previousVelocity = 0.0 + self.previousVelocityM1 = 0.0 + } + } + + func scrollToTop() { + if let panelContainerView = self.panelContainer.view as? StarsTransactionsPanelContainerComponent.View, !panelContainerView.scrollToTop() { + self.scrollView.setContentOffset(.zero, animated: true) + } + } + + private func updateScrolling(transition: ComponentTransition) { + let scrollBounds = self.scrollView.bounds + + let isLockedAtPanels = scrollBounds.maxY == self.scrollView.contentSize.height + + if let navigationMetrics = self.navigationMetrics { + let topInset: CGFloat = navigationMetrics.navigationHeight - 56.0 + + let titleOffset: CGFloat + let titleScale: CGFloat + let titleOffsetDelta = (topInset + 160.0) - (navigationMetrics.statusBarHeight + (navigationMetrics.navigationHeight - navigationMetrics.statusBarHeight) / 2.0) + + var topContentOffset = self.scrollView.contentOffset.y + + let navigationBackgroundAlpha = min(20.0, max(0.0, topContentOffset - 95.0)) / 20.0 + topContentOffset = topContentOffset + max(0.0, min(1.0, topContentOffset / titleOffsetDelta)) * 10.0 + titleOffset = topContentOffset + let fraction = max(0.0, min(1.0, titleOffset / titleOffsetDelta)) + titleScale = 1.0 - fraction * 0.36 + + let headerTransition: ComponentTransition = .immediate + + if let starView = self.starView.view { + let starPosition = CGPoint(x: self.scrollView.frame.width / 2.0, y: topInset + starView.bounds.height / 2.0 - 30.0 - titleOffset * titleScale) + headerTransition.setPosition(view: starView, position: starPosition) + headerTransition.setScale(view: starView, scale: titleScale) + } + + if let titleView = self.titleView.view { + let titlePosition = CGPoint(x: scrollBounds.width / 2.0, y: max(topInset + 160.0 - titleOffset, navigationMetrics.statusBarHeight + (navigationMetrics.navigationHeight - navigationMetrics.statusBarHeight) / 2.0)) + + headerTransition.setPosition(view: titleView, position: titlePosition) + headerTransition.setScale(view: titleView, scale: titleScale) + } + + let animatedTransition = ComponentTransition(animation: .curve(duration: 0.18, curve: .easeInOut)) + animatedTransition.setAlpha(view: self.navigationBackgroundView, alpha: navigationBackgroundAlpha) + animatedTransition.setAlpha(layer: self.navigationSeparatorLayerContainer, alpha: navigationBackgroundAlpha) + + let expansionDistance: CGFloat = 32.0 + var expansionDistanceFactor: CGFloat = abs(scrollBounds.maxY - self.scrollView.contentSize.height) / expansionDistance + expansionDistanceFactor = max(0.0, min(1.0, expansionDistanceFactor)) + + transition.setAlpha(layer: self.navigationSeparatorLayer, alpha: expansionDistanceFactor) + if let panelContainerView = self.panelContainer.view as? StarsTransactionsPanelContainerComponent.View { + panelContainerView.updateNavigationMergeFactor(value: 1.0 - expansionDistanceFactor, transition: transition) + } + + let topBalanceAlpha = 1.0 - expansionDistanceFactor + if let view = self.topBalanceTitleView.view { + view.alpha = topBalanceAlpha + } + if let view = self.topBalanceValueView.view { + view.alpha = topBalanceAlpha + } + if let view = self.topBalanceIconView.view { + view.alpha = topBalanceAlpha + } + + let listIsExpanded = expansionDistanceFactor == 0.0 + if listIsExpanded != self.listIsExpanded { + self.listIsExpanded = listIsExpanded + if !self.isUpdating { + self.state?.updated(transition: .init(animation: .curve(duration: 0.25, curve: .slide))) + } + } + } + + let _ = self.panelContainer.updateEnvironment( + transition: transition, + environment: { + StarsTransactionsPanelContainerEnvironment(isScrollable: isLockedAtPanels) + } + ) + } + + private var isUpdating = false + func update(component: TonTransactionsScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + self.component = component + self.state = state + + var balanceUpdated = false + if let starsState = self.starsState { + if let previousBalance = self.previousBalance, starsState.balance != previousBalance { + balanceUpdated = true + } + self.previousBalance = starsState.balance + } + + let environment = environment[ViewControllerComponentContainer.Environment.self].value + + if self.stateDisposable == nil { + self.stateDisposable = (component.tonContext.state + |> deliverOnMainQueue).start(next: { [weak self] state in + guard let self else { + return + } + self.starsState = state + + if !self.isUpdating { + self.state?.updated() + } + }) + } + + var wasLockedAtPanels = false + if let panelContainerView = self.panelContainer.view, let navigationMetrics = self.navigationMetrics { + if self.scrollView.bounds.minY > 0.0 && abs(self.scrollView.bounds.minY - (panelContainerView.frame.minY - navigationMetrics.navigationHeight)) <= UIScreenPixel { + wasLockedAtPanels = true + } + } + + self.controller = environment.controller + + self.navigationMetrics = (environment.navigationHeight, environment.statusBarHeight) + + self.navigationSeparatorLayer.backgroundColor = environment.theme.rootController.navigationBar.separatorColor.cgColor + + let navigationFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: environment.navigationHeight)) + self.navigationBackgroundView.updateColor(color: environment.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) + self.navigationBackgroundView.update(size: navigationFrame.size, transition: transition.containedViewLayoutTransition) + transition.setFrame(view: self.navigationBackgroundView, frame: navigationFrame) + + let navigationSeparatorFrame = CGRect(origin: CGPoint(x: 0.0, y: navigationFrame.maxY), size: CGSize(width: availableSize.width, height: UIScreenPixel)) + + transition.setFrame(layer: self.navigationSeparatorLayerContainer, frame: navigationSeparatorFrame) + transition.setFrame(layer: self.navigationSeparatorLayer, frame: CGRect(origin: CGPoint(), size: navigationSeparatorFrame.size)) + + self.backgroundColor = environment.theme.list.blocksBackgroundColor + + var contentHeight: CGFloat = 0.0 + + let sideInsets: CGFloat = environment.safeInsets.left + environment.safeInsets.right + 16.0 * 2.0 + let bottomInset: CGFloat = environment.safeInsets.bottom + + if environment.statusBarHeight > 0.0 { + contentHeight += environment.statusBarHeight + } else { + contentHeight += 12.0 + } + + let starTransition: ComponentTransition = .immediate + + var topBackgroundColor = environment.theme.list.plainBackgroundColor + let bottomBackgroundColor = environment.theme.list.blocksBackgroundColor + if environment.theme.overallDarkAppearance { + topBackgroundColor = bottomBackgroundColor + } + + let overscrollSize = self.overscroll.update( + transition: .immediate, + component: AnyComponent(Rectangle(color: topBackgroundColor)), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 1000.0) + ) + let overscrollFrame = CGRect(origin: CGPoint(x: 0.0, y: -overscrollSize.height), size: overscrollSize) + if let overscrollView = self.overscroll.view { + if overscrollView.superview == nil { + self.scrollView.addSubview(overscrollView) + } + starTransition.setFrame(view: overscrollView, frame: overscrollFrame) + } + + let fadeSize = self.fade.update( + transition: .immediate, + component: AnyComponent(RoundedRectangle( + colors: [ + topBackgroundColor, + bottomBackgroundColor + ], + cornerRadius: 0.0, + gradientDirection: .vertical + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 1000.0) + ) + let fadeFrame = CGRect(origin: CGPoint(x: 0.0, y: -fadeSize.height), size: fadeSize) + if let fadeView = self.fade.view { + if fadeView.superview == nil { + self.scrollView.addSubview(fadeView) + } + starTransition.setFrame(view: fadeView, frame: fadeFrame) + } + + let starSize = self.starView.update( + transition: .immediate, + component: AnyComponent(PremiumDiamondComponent()), + environment: {}, + containerSize: CGSize(width: min(414.0, availableSize.width), height: 220.0) + ) + let starFrame = CGRect(origin: .zero, size: starSize) + if let starView = self.starView.view { + if starView.superview == nil { + self.insertSubview(starView, aboveSubview: self.scrollView) + } + starTransition.setBounds(view: starView, bounds: starFrame) + } + + //TODO:localize + let titleSize = self.titleView.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: "TON", font: Font.bold(28.0), textColor: environment.theme.list.itemPrimaryTextColor)), + horizontalAlignment: .center, + truncationType: .end, + maximumNumberOfLines: 1 + ) + ), + environment: {}, + containerSize: availableSize + ) + if let titleView = self.titleView.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + starTransition.setBounds(view: titleView, bounds: CGRect(origin: .zero, size: titleSize)) + } + + let topBalanceTitleSize = self.topBalanceTitleView.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.Stars_Intro_Balance, + font: Font.regular(14.0), + textColor: environment.theme.actionSheet.primaryTextColor + )), + maximumNumberOfLines: 1 + )), + environment: {}, + containerSize: CGSize(width: 120.0, height: 100.0) + ) + + let formattedBalance = formatStarsAmountText(self.starsState?.balance ?? StarsAmount.zero, dateTimeFormat: environment.dateTimeFormat) + let smallLabelFont = Font.regular(11.0) + let labelFont = Font.semibold(14.0) + let balanceText = tonAmountAttributedString(formattedBalance, integralFont: labelFont, fractionalFont: smallLabelFont, color: environment.theme.actionSheet.primaryTextColor, decimalSeparator: environment.dateTimeFormat.decimalSeparator) + + let topBalanceValueSize = self.topBalanceValueView.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(balanceText), + maximumNumberOfLines: 1 + )), + environment: {}, + containerSize: CGSize(width: 120.0, height: 100.0) + ) + let topBalanceIconSize = self.topBalanceIconView.update( + transition: .immediate, + component: AnyComponent(BundleIconComponent(name: "Ads/TonAbout", tintColor: nil)), + environment: {}, + containerSize: availableSize + ) + + let navigationHeight = environment.navigationHeight - environment.statusBarHeight + let topBalanceOriginY = environment.statusBarHeight + (navigationHeight - topBalanceTitleSize.height - topBalanceValueSize.height) / 2.0 + let topBalanceTitleFrame = CGRect(origin: CGPoint(x: availableSize.width - topBalanceTitleSize.width - 16.0 - environment.safeInsets.right, y: topBalanceOriginY), size: topBalanceTitleSize) + if let topBalanceTitleView = self.topBalanceTitleView.view { + if topBalanceTitleView.superview == nil { + topBalanceTitleView.alpha = 0.0 + self.addSubview(topBalanceTitleView) + } + starTransition.setFrame(view: topBalanceTitleView, frame: topBalanceTitleFrame) + } + + let topBalanceValueFrame = CGRect(origin: CGPoint(x: availableSize.width - topBalanceValueSize.width - 16.0 - environment.safeInsets.right, y: topBalanceTitleFrame.maxY), size: topBalanceValueSize) + if let topBalanceValueView = self.topBalanceValueView.view { + if topBalanceValueView.superview == nil { + topBalanceValueView.alpha = 0.0 + self.addSubview(topBalanceValueView) + } + starTransition.setFrame(view: topBalanceValueView, frame: topBalanceValueFrame) + } + + let topBalanceIconFrame = CGRect(origin: CGPoint(x: topBalanceValueFrame.minX - topBalanceIconSize.width - 2.0, y: floorToScreenPixels(topBalanceValueFrame.midY - topBalanceIconSize.height / 2.0) - UIScreenPixel), size: topBalanceIconSize) + if let topBalanceIconView = self.topBalanceIconView.view { + if topBalanceIconView.superview == nil { + topBalanceIconView.alpha = 0.0 + self.addSubview(topBalanceIconView) + } + starTransition.setFrame(view: topBalanceIconView, frame: topBalanceIconFrame) + } + + contentHeight += 181.0 + + //TODO:localize + let descriptionSize = self.descriptionView.update( + transition: .immediate, + component: AnyComponent( + BalancedTextComponent( + text: .plain(NSAttributedString(string: "Offer TON to submit post suggestions to channels on Telegram.", font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.2 + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInsets - 8.0, height: 240.0) + ) + let descriptionFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - descriptionSize.width) / 2.0), y: contentHeight + 20.0 - floor(descriptionSize.height / 2.0)), size: descriptionSize) + if let descriptionView = self.descriptionView.view { + if descriptionView.superview == nil { + self.scrollView.addSubview(descriptionView) + } + + starTransition.setFrame(view: descriptionView, frame: descriptionFrame) + } + + contentHeight += descriptionSize.height + contentHeight += 29.0 + + let withdrawAvailable = "".isEmpty //(self.revenueState?.balances.overallRevenue.value ?? 0) > 0 + + let balanceSize = self.balanceView.update( + transition: .immediate, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: nil, + footer: nil, + items: [AnyComponentWithIdentity(id: 0, component: AnyComponent( + StarsBalanceComponent( + theme: environment.theme, + strings: environment.strings, + dateTimeFormat: environment.dateTimeFormat, + count: self.starsState?.balance ?? StarsAmount.zero, + isTon: true, + rate: 2.99 * 1e-9, + actionTitle: "Withdraw via Fragment", + actionAvailable: withdrawAvailable, + actionIsEnabled: true, + actionIcon: nil, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.withdraw() + }, + secondaryActionTitle: nil, + secondaryActionIcon: nil, + secondaryAction: nil, + additionalAction: nil + ) + ))] + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInsets, height: availableSize.height) + ) + let balanceFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - balanceSize.width) / 2.0), y: contentHeight), size: balanceSize) + if let balanceView = self.balanceView.view { + if balanceView.superview == nil { + self.scrollView.addSubview(balanceView) + } + starTransition.setFrame(view: balanceView, frame: balanceFrame) + } + contentHeight += balanceSize.height + contentHeight += 34.0 + + let initialTransactions = self.starsState?.transactions ?? [] + var panelItems: [StarsTransactionsPanelContainerComponent.Item] = [] + if !initialTransactions.isEmpty { + let allTransactionsContext: StarsTransactionsContext + if let current = self.allTransactionsContext { + allTransactionsContext = current + } else { + allTransactionsContext = component.context.engine.payments.peerStarsTransactionsContext(subject: .starsContext(component.tonContext), mode: .all) + self.allTransactionsContext = allTransactionsContext + } + + let incomingTransactionsContext: StarsTransactionsContext + if let current = self.incomingTransactionsContext { + incomingTransactionsContext = current + } else { + incomingTransactionsContext = component.context.engine.payments.peerStarsTransactionsContext(subject: .starsContext(component.tonContext), mode: .incoming) + self.incomingTransactionsContext = incomingTransactionsContext + } + + let outgoingTransactionsContext: StarsTransactionsContext + if let current = self.outgoingTransactionsContext { + outgoingTransactionsContext = current + } else { + outgoingTransactionsContext = component.context.engine.payments.peerStarsTransactionsContext(subject: .starsContext(component.tonContext), mode: .outgoing) + self.outgoingTransactionsContext = outgoingTransactionsContext + } + + panelItems.append(StarsTransactionsPanelContainerComponent.Item( + id: "all", + title: environment.strings.Stars_Intro_AllTransactions, + panel: AnyComponent(StarsTransactionsListPanelComponent( + context: component.context, + transactionsContext: allTransactionsContext, + isAccount: true, + action: { transaction in + component.openTransaction(transaction) + } + )) + )) + + panelItems.append(StarsTransactionsPanelContainerComponent.Item( + id: "incoming", + title: environment.strings.Stars_Intro_Incoming, + panel: AnyComponent(StarsTransactionsListPanelComponent( + context: component.context, + transactionsContext: incomingTransactionsContext, + isAccount: true, + action: { transaction in + component.openTransaction(transaction) + } + )) + )) + + panelItems.append(StarsTransactionsPanelContainerComponent.Item( + id: "outgoing", + title: environment.strings.Stars_Intro_Outgoing, + panel: AnyComponent(StarsTransactionsListPanelComponent( + context: component.context, + transactionsContext: outgoingTransactionsContext, + isAccount: true, + action: { transaction in + component.openTransaction(transaction) + } + )) + )) + } + + var panelTransition = transition + if balanceUpdated { + panelTransition = .easeInOut(duration: 0.25) + } + + if !panelItems.isEmpty { + let panelContainerInset: CGFloat = self.listIsExpanded ? 0.0 : 16.0 + let panelContainerCornerRadius: CGFloat = self.listIsExpanded ? 0.0 : 11.0 + + let panelContainerSize = self.panelContainer.update( + transition: panelTransition, + component: AnyComponent(StarsTransactionsPanelContainerComponent( + theme: environment.theme, + strings: environment.strings, + dateTimeFormat: environment.dateTimeFormat, + insets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left + panelContainerInset, bottom: bottomInset, right: environment.safeInsets.right + panelContainerInset), + items: panelItems, + currentPanelUpdated: { [weak self] id, transition in + guard let self else { + return + } + self.currentSelectedPanelId = id + self.state?.updated(transition: transition) + } + )), + environment: { + StarsTransactionsPanelContainerEnvironment(isScrollable: wasLockedAtPanels) + }, + containerSize: CGSize(width: availableSize.width, height: availableSize.height - environment.navigationHeight) + ) + if let panelContainerView = self.panelContainer.view { + if panelContainerView.superview == nil { + self.scrollContainerView.addSubview(panelContainerView) + } + transition.setFrame(view: panelContainerView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - panelContainerSize.width) / 2.0), y: contentHeight), size: panelContainerSize)) + transition.setCornerRadius(layer: panelContainerView.layer, cornerRadius: panelContainerCornerRadius) + } + contentHeight += panelContainerSize.height + } else { + self.panelContainer.view?.removeFromSuperview() + } + + self.ignoreScrolling = true + + let contentOffset = self.scrollView.bounds.minY + transition.setPosition(view: self.scrollView, position: CGRect(origin: CGPoint(), size: availableSize).center) + let contentSize = CGSize(width: availableSize.width, height: contentHeight) + if self.scrollView.contentSize != contentSize { + self.scrollView.contentSize = contentSize + } + transition.setFrame(view: self.scrollContainerView, frame: CGRect(origin: CGPoint(), size: contentSize)) + + var scrollViewBounds = self.scrollView.bounds + scrollViewBounds.size = availableSize + if wasLockedAtPanels, let panelContainerView = self.panelContainer.view { + scrollViewBounds.origin.y = panelContainerView.frame.minY - environment.navigationHeight + } + transition.setBounds(view: self.scrollView, bounds: scrollViewBounds) + + if !wasLockedAtPanels && !transition.animation.isImmediate && self.scrollView.bounds.minY != contentOffset { + let deltaOffset = self.scrollView.bounds.minY - contentOffset + transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: -deltaOffset), to: CGPoint(), additive: true) + } + + self.ignoreScrolling = false + + self.updateScrolling(transition: transition) + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +public final class TonTransactionsScreen: ViewControllerComponentContainer { + private let context: AccountContext + private let tonContext: StarsContext + + private let options = Promise<[StarsTopUpOption]>() + + private let navigateDisposable = MetaDisposable() + + private weak var tooltipScreen: UndoOverlayController? + private var timer: Foundation.Timer? + + public init(context: AccountContext, tonContext: StarsContext, forceDark: Bool = false) { + self.context = context + self.tonContext = tonContext + + var buyImpl: (() -> Void)? + var withdrawImpl: (() -> Void)? + var showTimeoutTooltipImpl: ((Int32) -> Void)? + var giftImpl: (() -> Void)? + var openTransactionImpl: ((StarsContext.State.Transaction) -> Void)? + super.init(context: context, component: TonTransactionsScreenComponent( + context: self.context, + starsContext: self.tonContext, + openTransaction: { transaction in + openTransactionImpl?(transaction) + }, + buy: { + buyImpl?() + }, + withdraw: { + withdrawImpl?() + }, + showTimeoutTooltip: { timestamp in + showTimeoutTooltipImpl?(timestamp) + }, + gift: { + giftImpl?() + } + ), navigationBarAppearance: .transparent) + + self.navigationPresentation = .modalInLargeLayout + + self.options.set(.single([]) |> then(context.engine.payments.starsTopUpOptions())) + + openTransactionImpl = { [weak self] transaction in + guard let self else { + return + } + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + let controller = context.sharedContext.makeStarsTransactionScreen(context: context, transaction: transaction, peer: peer) + self.push(controller) + }) + } + + buyImpl = { [weak self] in + guard let self else { + return + } + let _ = (self.options.get() + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] options in + guard let self else { + return + } + let controller = context.sharedContext.makeStarsPurchaseScreen(context: context, starsContext: tonContext, options: options, purpose: .generic, completion: { [weak self] stars in + guard let self else { + return + } + self.tonContext.add(balance: StarsAmount(value: stars, nanos: 0)) + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let resultController = UndoOverlayController( + presentationData: presentationData, + content: .universal( + animation: "StarsBuy", + scale: 0.066, + colors: [:], + title: presentationData.strings.Stars_Intro_PurchasedTitle, + text: presentationData.strings.Stars_Intro_PurchasedText(presentationData.strings.Stars_Intro_PurchasedText_Stars(Int32(stars))).string, + customUndoText: nil, + timeout: nil + ), + elevatedLayout: false, + action: { _ in return true}) + self.present(resultController, in: .window(.root)) + }) + self.push(controller) + }) + } + + withdrawImpl = { [weak self] in + guard let _ = self else { + return + } + +// let _ = (context.engine.peers.checkStarsRevenueWithdrawalAvailability() +// |> deliverOnMainQueue).start(error: { [weak self] error in +// guard let self else { +// return +// } +// switch error { +// case .serverProvided: +// return +// case .requestPassword: +// let _ = (self.starsRevenueStatsContext.state +// |> take(1) +// |> deliverOnMainQueue).start(next: { [weak self] state in +// guard let self, let stats = state.stats else { +// return +// } +// let controller = self.context.sharedContext.makeStarsWithdrawalScreen(context: context, stats: stats, completion: { [weak self] amount in +// guard let self else { +// return +// } +// let controller = confirmStarsRevenueWithdrawalController(context: context, peerId: context.account.peerId, amount: amount, present: { [weak self] c, a in +// self?.present(c, in: .window(.root)) +// }, completion: { [weak self] url in +// let presentationData = context.sharedContext.currentPresentationData.with { $0 } +// context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {}) +// +// Queue.mainQueue().after(2.0) { +// self?.starsRevenueStatsContext.reload() +// } +// }) +// self.present(controller, in: .window(.root)) +// }) +// self.push(controller) +// }) +// default: +// let controller = starsRevenueWithdrawalController(context: context, peerId: context.account.peerId, amount: 0, initialError: error, present: { [weak self] c, a in +// self?.present(c, in: .window(.root)) +// }, completion: { _ in +// +// }) +// self.present(controller, in: .window(.root)) +// } +// }) + } + + showTimeoutTooltipImpl = { [weak self] cooldownUntilTimestamp in + guard let self, self.tooltipScreen == nil else { + return + } + + let remainingCooldownSeconds = cooldownUntilTimestamp - Int32(Date().timeIntervalSince1970) + + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + let content: UndoOverlayContent = .universal( + animation: "anim_clock", + scale: 0.058, + colors: [:], + title: nil, + text: presentationData.strings.Stars_Withdraw_Withdraw_ErrorTimeout(stringForRemainingTime(remainingCooldownSeconds)).string, + customUndoText: nil, + timeout: nil + ) + let controller = UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { _ in + return true + }) + self.tooltipScreen = controller + self.present(controller, in: .window(.root)) + + if remainingCooldownSeconds < 3600 { + if self.timer == nil { + self.timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { [weak self] _ in + guard let self else { + return + } + + if let tooltipScreen = self.tooltipScreen { + let remainingCooldownSeconds = cooldownUntilTimestamp - Int32(Date().timeIntervalSince1970) + let content: UndoOverlayContent = .universal( + animation: "anim_clock", + scale: 0.058, + colors: [:], + title: nil, + text: presentationData.strings.Stars_Withdraw_Withdraw_ErrorTimeout(stringForRemainingTime(remainingCooldownSeconds)).string, + customUndoText: nil, + timeout: nil + ) + tooltipScreen.content = content + } else { + if let timer = self.timer { + self.timer = nil + timer.invalidate() + } + } + }) + } + } + } + + giftImpl = { [weak self] in + guard let self else { + return + } + let _ = combineLatest(queue: Queue.mainQueue(), + self.options.get() |> take(1), + self.context.account.stateManager.contactBirthdays |> take(1) + ).start(next: { [weak self] options, birthdays in + guard let self else { + return + } + let controller = self.context.sharedContext.makeStarsGiftController(context: self.context, birthdays: birthdays, completion: { [weak self] peerIds in + guard let self, let peerId = peerIds.first else { + return + } + let purchaseController = self.context.sharedContext.makeStarsPurchaseScreen( + context: self.context, + starsContext: tonContext, + options: options, + purpose: .gift(peerId: peerId), + completion: { [weak self] stars in + guard let self else { + return + } + + if let navigationController = self.navigationController as? NavigationController { + var controllers = navigationController.viewControllers + controllers = controllers.filter { !($0 is ContactSelectionController) } + navigationController.setViewControllers(controllers, animated: true) + } + + Queue.mainQueue().after(2.0) { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let resultController = UndoOverlayController( + presentationData: presentationData, + content: .universal( + animation: "StarsSend", + scale: 0.066, + colors: [:], + title: nil, + text: presentationData.strings.Stars_Intro_StarsSent(Int32(stars)), + customUndoText: presentationData.strings.Stars_Intro_StarsSent_ViewChat, + timeout: nil + ), + elevatedLayout: false, + action: { [weak self] action in + if case .undo = action, let navigationController = self?.navigationController as? NavigationController { + let _ = (context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) + ) + |> deliverOnMainQueue).start(next: { peer in + guard let peer else { + return + } + 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)) + }) + } + return true + }) + self.present(resultController, in: .window(.root)) + } + } + ) + self.push(purchaseController) + }) + self.push(controller) + }) + } + + self.tonContext.load(force: false) + + self.scrollToTop = { [weak self] in + guard let self else { + return + } + if let componentView = self.node.hostView.componentView as? TonTransactionsScreenComponent.View { + componentView.scrollToTop() + } + } + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.navigateDisposable.dispose() + } + + public func update() { + } +} diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/BUILD b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/BUILD index 07d4ac7084..7355c6c637 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/BUILD +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/BUILD @@ -49,7 +49,7 @@ swift_library( copts = [ "-warnings-as-errors", ], - data = [ + data = [ ":StoryPeerListBundle", ], deps = [ diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift index 4fd515139d..312ce0b606 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift @@ -29,6 +29,11 @@ func openWebAppImpl( skipTermsOfService: Bool, payload: String? ) { + if context.isFrozen { + parentController.push(context.sharedContext.makeAccountFreezeInfoScreen(context: context)) + return + } + let presentationData: PresentationData if let parentController = parentController as? ChatControllerImpl { presentationData = parentController.presentationData diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 5998aec3b4..ee7e2a1b93 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -3687,6 +3687,10 @@ public final class SharedAccountContextImpl: SharedAccountContext { return StarsTransactionsScreen(context: context, starsContext: starsContext) } + public func makeTonTransactionsScreen(context: AccountContext, tonContext: StarsContext) -> ViewController { + return TonTransactionsScreen(context: context, tonContext: tonContext) + } + public func makeStarsPurchaseScreen(context: AccountContext, starsContext: StarsContext, options: [Any], purpose: StarsPurchasePurpose, completion: @escaping (Int64) -> Void) -> ViewController { return StarsPurchaseScreen(context: context, starsContext: starsContext, options: options, purpose: purpose, completion: completion) }