Various improvements

This commit is contained in:
Ilya Laktyushin 2025-06-24 01:24:14 +02:00
parent 9a14b076c6
commit d166e32b3e
16 changed files with 1536 additions and 46 deletions

View File

@ -1213,6 +1213,7 @@ public protocol SharedAccountContext: AnyObject {
func makeStoryStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, 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

Binary file not shown.

View File

@ -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)"

View File

@ -618,9 +618,9 @@ private final class StarsContextImpl {
}
var transactions = state.transactions
if addTransaction {
transactions.insert(.init(flags: [.isLocal], id: "\(arc4random())", count: balance, 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))
}
@ -724,7 +724,7 @@ private extension StarsContext.State.Transaction {
let media = extendedMedia.flatMap({ $0.compactMap { textMediaAndExpirationTimerFromApiMedia($0, PeerId(0)).media } }) ?? []
let _ = subscriptionPeriod
self.init(flags: flags, id: id, count: StarsAmount(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)
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)
}
}
}
@ -789,7 +789,7 @@ public final class StarsContext {
public let flags: Flags
public let id: String
public let count: StarsAmount
public let count: CurrencyAmount
public let date: Int32
public let peer: Peer
public let title: String?
@ -812,7 +812,7 @@ public final class StarsContext {
public init(
flags: Flags,
id: String,
count: StarsAmount,
count: CurrencyAmount,
date: Int32,
peer: Peer,
title: String?,
@ -1173,9 +1173,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 +1193,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 +1220,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 {

View File

@ -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 {

View File

@ -381,6 +381,7 @@ final class PeerInfoScreenData {
let hasBotPreviewItems: Bool
let isPremiumRequiredForStoryPosting: Bool
let personalChannel: PeerInfoPersonalChannelData?
let tonState: StarsContext.State?
let starsState: StarsContext.State?
let starsRevenueStatsState: StarsRevenueStats?
let starsRevenueStatsContext: StarsRevenueStatsContext?
@ -432,6 +433,7 @@ final class PeerInfoScreenData {
hasBotPreviewItems: Bool,
isPremiumRequiredForStoryPosting: Bool,
personalChannel: PeerInfoPersonalChannelData?,
tonState: StarsContext.State?,
starsState: StarsContext.State?,
starsRevenueStatsState: StarsRevenueStats?,
starsRevenueStatsContext: StarsRevenueStatsContext?,
@ -471,6 +473,7 @@ final class PeerInfoScreenData {
self.hasBotPreviewItems = hasBotPreviewItems
self.isPremiumRequiredForStoryPosting = isPremiumRequiredForStoryPosting
self.personalChannel = personalChannel
self.tonState = tonState
self.starsState = starsState
self.starsRevenueStatsState = starsRevenueStatsState
self.starsRevenueStatsContext = starsRevenueStatsContext
@ -752,7 +755,7 @@ private func peerInfoPersonalOrLinkedChannel(context: AccountContext, peerId: En
|> distinctUntilChanged
}
func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id, accountsAndPeers: Signal<[(AccountContext, EnginePeer, Int32)], NoError>, activeSessionsContextAndCount: Signal<(ActiveSessionsContext, Int, WebSessionsContext)?, NoError>, notificationExceptions: Signal<NotificationExceptionsList?, NoError>, privacySettings: Signal<AccountPrivacySettings?, NoError>, archivedStickerPacks: Signal<[ArchivedStickerPackItem]?, NoError>, hasPassport: Signal<Bool, NoError>, starsContext: StarsContext?) -> Signal<PeerInfoScreenData, NoError> {
func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id, accountsAndPeers: Signal<[(AccountContext, EnginePeer, Int32)], NoError>, activeSessionsContextAndCount: Signal<(ActiveSessionsContext, Int, WebSessionsContext)?, NoError>, notificationExceptions: Signal<NotificationExceptionsList?, NoError>, privacySettings: Signal<AccountPrivacySettings?, NoError>, archivedStickerPacks: Signal<[ArchivedStickerPackItem]?, NoError>, hasPassport: Signal<Bool, NoError>, starsContext: StarsContext?, tonContext: StarsContext?) -> Signal<PeerInfoScreenData, NoError> {
let preferences = context.sharedContext.accountManager.sharedData(keys: [
SharedDataKeys.proxySettings,
ApplicationSpecificSharedDataKeys.inAppNotificationSettings,
@ -845,6 +848,13 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id,
|> distinctUntilChanged
}
}
let tonState: Signal<StarsContext.State?, NoError>
if let tonContext {
tonState = tonContext.state
} else {
tonState = .single(nil)
}
let starsState: Signal<StarsContext.State?, NoError>
if let starsContext {
@ -879,9 +889,10 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id,
hasStories,
bots,
peerInfoPersonalOrLinkedChannel(context: context, peerId: peerId, isSettings: true),
starsState
starsState,
tonState
)
|> map { peerView, accountsAndPeers, accountSessions, privacySettings, sharedPreferences, notifications, stickerPacks, hasPassport, accountPreferences, suggestions, limits, hasPassword, isPowerSavingEnabled, hasStories, bots, personalChannel, starsState -> PeerInfoScreenData in
|> map { peerView, accountsAndPeers, accountSessions, privacySettings, sharedPreferences, notifications, stickerPacks, hasPassport, accountPreferences, suggestions, limits, hasPassword, isPowerSavingEnabled, hasStories, bots, personalChannel, starsState, tonState -> PeerInfoScreenData in
let (notificationExceptions, notificationsAuthorizationStatus, notificationsWarningSuppressed) = notifications
let (featuredStickerPacks, archivedStickerPacks) = stickerPacks
@ -959,6 +970,7 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id,
hasBotPreviewItems: false,
isPremiumRequiredForStoryPosting: true,
personalChannel: personalChannel,
tonState: tonState,
starsState: starsState,
starsRevenueStatsState: nil,
starsRevenueStatsContext: nil,
@ -1009,6 +1021,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
hasBotPreviewItems: false,
isPremiumRequiredForStoryPosting: true,
personalChannel: nil,
tonState: nil,
starsState: nil,
starsRevenueStatsState: nil,
starsRevenueStatsContext: nil,
@ -1468,6 +1481,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
hasBotPreviewItems: hasBotPreviewItems,
isPremiumRequiredForStoryPosting: false,
personalChannel: personalChannel,
tonState: nil,
starsState: nil,
starsRevenueStatsState: starsRevenueContextAndState.1,
starsRevenueStatsContext: starsRevenueContextAndState.0,
@ -1699,6 +1713,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
hasBotPreviewItems: false,
isPremiumRequiredForStoryPosting: isPremiumRequiredForStoryPosting,
personalChannel: personalChannel,
tonState: nil,
starsState: nil,
starsRevenueStatsState: starsRevenueContextAndState.1,
starsRevenueStatsContext: starsRevenueContextAndState.0,
@ -2031,6 +2046,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
hasBotPreviewItems: false,
isPremiumRequiredForStoryPosting: isPremiumRequiredForStoryPosting,
personalChannel: nil,
tonState: nil,
starsState: nil,
starsRevenueStatsState: starsRevenueContextAndState.1,
starsRevenueStatsContext: starsRevenueContextAndState.0,

View File

@ -538,6 +538,7 @@ private enum PeerInfoSettingsSection {
case profile
case premiumManagement
case stars
case ton
}
private enum PeerInfoReportType {
@ -1035,14 +1036,31 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p
}))
}
}
if let tonState = data.tonState {
if abs(tonState.balance.value) > 0 {
let balanceText: NSAttributedString
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)
let labelColor = presentationData.theme.list.itemSecondaryTextColor
balanceText = tonAmountAttributedString(formattedLabel, integralFont: labelFont, fractionalFont: smallLabelFont, color: labelColor, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)
} else {
balanceText = NSAttributedString()
}
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)
}))
}
@ -3011,7 +3029,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}
private var didSetReady = false
init(controller: PeerInfoScreenImpl, context: AccountContext, peerId: PeerId, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, nearbyPeerDistance: Int32?, reactionSourceMessageId: MessageId?, callMessages: [Message], isSettings: Bool, isMyProfile: Bool, hintGroupInCommon: PeerId?, requestsContext: PeerInvitationImportersContext?, profileGiftsContext: ProfileGiftsContext?, starsContext: StarsContext?, chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>, initialPaneKey: PeerInfoPaneKey?) {
init(controller: PeerInfoScreenImpl, context: AccountContext, peerId: PeerId, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, nearbyPeerDistance: Int32?, reactionSourceMessageId: MessageId?, callMessages: [Message], isSettings: Bool, isMyProfile: Bool, hintGroupInCommon: PeerId?, requestsContext: PeerInvitationImportersContext?, profileGiftsContext: ProfileGiftsContext?, starsContext: StarsContext?, tonContext: StarsContext?, chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>, initialPaneKey: PeerInfoPaneKey?) {
self.controller = controller
self.context = context
self.peerId = peerId
@ -4676,7 +4694,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
self.cachedFaq.set(.single(nil) |> then(cachedFaqInstantPage(context: self.context) |> map(Optional.init)))
screenData = peerInfoScreenSettingsData(context: context, peerId: peerId, accountsAndPeers: self.accountsAndPeers.get(), activeSessionsContextAndCount: self.activeSessionsContextAndCount.get(), notificationExceptions: self.notificationExceptions.get(), privacySettings: self.privacySettings.get(), archivedStickerPacks: self.archivedPacks.get(), hasPassport: hasPassport, starsContext: starsContext)
screenData = peerInfoScreenSettingsData(context: context, peerId: peerId, accountsAndPeers: self.accountsAndPeers.get(), activeSessionsContextAndCount: self.activeSessionsContextAndCount.get(), notificationExceptions: self.notificationExceptions.get(), privacySettings: self.privacySettings.get(), archivedStickerPacks: self.archivedPacks.get(), hasPassport: hasPassport, starsContext: starsContext, tonContext: tonContext)
self.headerNode.displayCopyContextMenu = { [weak self] node, copyPhone, copyUsername in
@ -10649,6 +10667,10 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
if let starsContext = self.controller?.starsContext {
push(self.context.sharedContext.makeStarsTransactionsScreen(context: self.context, starsContext: starsContext))
}
case .ton:
if let tonContext = self.controller?.tonContext {
push(self.context.sharedContext.makeTonTransactionsScreen(context: self.context, tonContext: tonContext))
}
}
}
@ -12846,6 +12868,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc
private weak var requestsContext: PeerInvitationImportersContext?
private weak var profileGiftsContext: ProfileGiftsContext?
fileprivate let starsContext: StarsContext?
fileprivate let tonContext: StarsContext?
private let switchToRecommendedChannels: Bool
private let switchToGifts: Bool
private let switchToGroupsInCommon: Bool
@ -12947,11 +12970,22 @@ 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
self.tonContext = nil
}
if isMyProfile, let profileGiftsContext {
@ -13294,7 +13328,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc
} else if self.switchToGroupsInCommon {
initialPaneKey = .groupsInCommon
}
self.displayNode = PeerInfoScreenNode(controller: self, context: self.context, peerId: self.peerId, avatarInitiallyExpanded: self.avatarInitiallyExpanded, isOpenedFromChat: self.isOpenedFromChat, nearbyPeerDistance: self.nearbyPeerDistance, reactionSourceMessageId: self.reactionSourceMessageId, callMessages: self.callMessages, isSettings: self.isSettings, isMyProfile: self.isMyProfile, hintGroupInCommon: self.hintGroupInCommon, requestsContext: self.requestsContext, profileGiftsContext: self.profileGiftsContext, starsContext: self.starsContext, chatLocation: self.chatLocation, chatLocationContextHolder: self.chatLocationContextHolder, initialPaneKey: initialPaneKey)
self.displayNode = PeerInfoScreenNode(controller: self, context: self.context, peerId: self.peerId, avatarInitiallyExpanded: self.avatarInitiallyExpanded, isOpenedFromChat: self.isOpenedFromChat, nearbyPeerDistance: self.nearbyPeerDistance, reactionSourceMessageId: self.reactionSourceMessageId, callMessages: self.callMessages, isSettings: self.isSettings, isMyProfile: self.isMyProfile, hintGroupInCommon: self.hintGroupInCommon, requestsContext: self.requestsContext, profileGiftsContext: self.profileGiftsContext, starsContext: self.starsContext, tonContext: self.tonContext, chatLocation: self.chatLocation, chatLocationContextHolder: self.chatLocationContextHolder, initialPaneKey: initialPaneKey)
self.controllerNode.accountsAndPeers.set(self.accountsAndPeers.get() |> map { $0.1 })
self.controllerNode.activeSessionsContextAndCount.set(self.activeSessionsContextAndCount.get())
self.cachedDataPromise.set(self.controllerNode.cachedDataPromise.get())

View File

@ -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",
],
)

View File

@ -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<Bool>()
public var ready: Signal<Bool, NoError> {
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<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}

View File

@ -376,7 +376,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
titleText = gift.title
descriptionText = "\(strings.Gift_Unique_Collectible) #\(presentationStringsFormattedNumber(gift.number, dateTimeFormat.groupingSeparator))"
}
count = transaction.count
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 = transaction.count
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 = transaction.count
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 = transaction.count
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 = transaction.count
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 = transaction.count
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 = transaction.count
count = transaction.count.amount
transactionId = transaction.id
date = transaction.date
if case let .peer(peer) = transaction.peer {
@ -1173,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
}

View File

@ -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",

View File

@ -17,6 +17,7 @@ final class StarsBalanceComponent: Component {
let strings: PresentationStrings
let dateTimeFormat: PresentationDateTimeFormat
let count: StarsAmount
let isTon: Bool
let rate: Double?
let actionTitle: String
let actionAvailable: Bool
@ -35,6 +36,7 @@ final class StarsBalanceComponent: Component {
strings: PresentationStrings,
dateTimeFormat: PresentationDateTimeFormat,
count: StarsAmount,
isTon: Bool = false,
rate: Double?,
actionTitle: String,
actionAvailable: Bool,
@ -52,6 +54,7 @@ final class StarsBalanceComponent: Component {
self.strings = strings
self.dateTimeFormat = dateTimeFormat
self.count = count
self.isTon = isTon
self.rate = rate
self.actionTitle = actionTitle
self.actionAvailable = actionAvailable
@ -97,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
}
@ -120,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)
}
@ -130,6 +134,14 @@ final class StarsBalanceComponent: Component {
}
func update(component: StarsBalanceComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, 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
@ -164,7 +176,12 @@ final class StarsBalanceComponent: Component {
let sideInset: CGFloat = 16.0
var contentHeight: CGFloat = sideInset
let formattedLabel = formatStarsAmountText(component.count, dateTimeFormat: component.dateTimeFormat)
let formattedLabel: String
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) {
labelFont = Font.with(size: 48.0, design: .round, weight: .semibold)
@ -188,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

View File

@ -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,13 +409,24 @@ final class StarsTransactionsListPanelComponent: Component {
}
let itemLabel: NSAttributedString
let formattedLabel = formatStarsAmountText(item.count, 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) {
@ -496,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(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

View File

@ -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

View File

@ -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)
}