From d166e32b3ec2b8156adc05b94d246230c07aa009 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 24 Jun 2025 01:24:14 +0200 Subject: [PATCH] Various improvements --- .../Sources/AccountContext.swift | 1 + submodules/PremiumUI/Resources/diamond.scn | Bin 0 -> 70359 bytes .../Sources/StarsTransactionItem.swift | 4 +- .../TelegramEngine/Payments/Stars.swift | 22 +- .../Sources/TonFormat.swift | 9 + .../PeerInfoScreen/Sources/PeerInfoData.swift | 22 +- .../Sources/PeerInfoScreen.swift | 50 +- .../Premium/PremiumDiamondComponent/BUILD | 27 + .../Sources/PremiumDiamondComponent.swift | 309 +++++ .../Sources/StarsTransactionScreen.swift | 18 +- .../Stars/StarsTransactionsScreen/BUILD | 2 + .../Sources/StarsBalanceComponent.swift | 29 +- .../StarsTransactionsListPanelComponent.swift | 25 +- .../Sources/TonTransactionsScreen.swift | 1055 +++++++++++++++++ .../Chat/ChatControllerOpenWebApp.swift | 5 + .../Sources/SharedAccountContext.swift | 4 + 16 files changed, 1536 insertions(+), 46 deletions(-) create mode 100644 submodules/PremiumUI/Resources/diamond.scn create mode 100644 submodules/TelegramUI/Components/Premium/PremiumDiamondComponent/BUILD create mode 100644 submodules/TelegramUI/Components/Premium/PremiumDiamondComponent/Sources/PremiumDiamondComponent.swift create mode 100644 submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/TonTransactionsScreen.swift 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/PremiumUI/Resources/diamond.scn b/submodules/PremiumUI/Resources/diamond.scn new file mode 100644 index 0000000000000000000000000000000000000000..882a662d157db5aa7769866853a648a3743a9171 GIT binary patch literal 70359 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 C3`oHM literal 0 HcmV?d00001 diff --git a/submodules/StatisticsUI/Sources/StarsTransactionItem.swift b/submodules/StatisticsUI/Sources/StarsTransactionItem.swift index 1de6bd5ae1..6135061c9a 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 967cfe9ac8..e984471494 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, 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 { 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 a09cd2413b..69e012d2c9 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift @@ -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, privacySettings: Signal, archivedStickerPacks: Signal<[ArchivedStickerPackItem]?, NoError>, hasPassport: Signal, starsContext: StarsContext?) -> Signal { +func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id, accountsAndPeers: Signal<[(AccountContext, EnginePeer, Int32)], NoError>, activeSessionsContextAndCount: Signal<(ActiveSessionsContext, Int, WebSessionsContext)?, NoError>, notificationExceptions: Signal, privacySettings: Signal, archivedStickerPacks: Signal<[ArchivedStickerPackItem]?, NoError>, hasPassport: Signal, starsContext: StarsContext?, tonContext: StarsContext?) -> Signal { 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 + if let tonContext { + tonState = tonContext.state + } else { + tonState = .single(nil) + } let starsState: Signal 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, diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 26a7cd4b7d..d1345cd60f 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -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, 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, 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()) 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/StarsTransactionScreen/Sources/StarsTransactionScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift index a54f61bdb7..8da430af80 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift @@ -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 } 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 872bca586c..8753da0912 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift @@ -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, 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 diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift index d4dd8e3d61..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,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 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/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) }