From b56a0143f3c399a4c8c7ed6a9bb25e413bb142bb Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 24 Jun 2025 13:02:37 +0200 Subject: [PATCH] Stars --- .../Sources/StarsTransactionItem.swift | 5 +- .../ApiUtils/StoreMessage_Telegram.swift | 4 +- .../SuggestedPostMessageAttribute.swift | 10 +- .../TelegramEngine/Payments/Stars.swift | 11 +- .../Sources/CurrencyFormat.swift | 11 + .../Sources/ServiceMessageStrings.swift | 11 +- .../ChatMessageGiftBubbleContentNode.swift | 39 +- .../Sources/EmojiTextAttachmentView.swift | 23 +- .../Sources/PeerInfoScreen.swift | 4 +- .../Sources/StarsAvatarComponent.swift | 26 +- .../Sources/StarsImageComponent.swift | 23 +- .../Sources/StarsTransactionScreen.swift | 75 +- .../Sources/StarsBalanceComponent.swift | 11 +- .../Sources/StarsStatisticsScreen.swift | 2 + .../StarsTransactionsListPanelComponent.swift | 10 +- .../Sources/StarsTransactionsScreen.swift | 41 +- .../Sources/StarsWithdrawalScreen.swift | 675 ++++++++++++++++-- .../Chat/ChatControllerLoadDisplayNode.swift | 16 +- .../Sources/ChatTextInputAttributes.swift | 2 +- 19 files changed, 851 insertions(+), 148 deletions(-) diff --git a/submodules/StatisticsUI/Sources/StarsTransactionItem.swift b/submodules/StatisticsUI/Sources/StarsTransactionItem.swift index 1de6bd5ae1..538ce1b80d 100644 --- a/submodules/StatisticsUI/Sources/StarsTransactionItem.swift +++ b/submodules/StatisticsUI/Sources/StarsTransactionItem.swift @@ -287,7 +287,8 @@ final class StarsTransactionItemNode: ListViewItemNode, ItemListItemNode { } else { labelString = "+ \(formattedLabel)" } - itemLabel = NSAttributedString(string: labelString, font: Font.medium(fontBaseDisplaySize), textColor: labelString.hasPrefix("-") ? item.presentationData.theme.list.itemDestructiveColor : item.presentationData.theme.list.itemDisclosureActions.constructive.fillColor) + let itemLabelColor = labelString.hasPrefix("-") ? item.presentationData.theme.list.itemDestructiveColor : item.presentationData.theme.list.itemDisclosureActions.constructive.fillColor + itemLabel = NSAttributedString(string: labelString, font: Font.medium(fontBaseDisplaySize), textColor: itemLabelColor) var itemDateColor = item.presentationData.theme.list.itemSecondaryTextColor itemDate = stringForMediumCompactDate(timestamp: item.transaction.date, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat) @@ -341,7 +342,7 @@ final class StarsTransactionItemNode: ListViewItemNode, ItemListItemNode { contentInsets: UIEdgeInsets(top: 9.0, left: 0.0, bottom: 8.0, right: 0.0), leftIcon: .custom(AnyComponentWithIdentity(id: "avatar", component: AnyComponent(StarsAvatarComponent(context: item.context, theme: item.presentationData.theme, peer: item.transaction.peer, photo: nil, media: [], uniqueGift: nil, backgroundColor: item.presentationData.theme.list.itemBlocksBackgroundColor))), 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(theme: item.presentationData.theme, currency: item.transaction.currency, textColor: itemLabelColor, text: itemLabel))), insets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 16.0))), action: { [weak self] _ in guard let self, let item = self.item else { return diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index b19ed63f58..eb7de8d13b 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -1016,8 +1016,8 @@ extension StoreMessage { attributes.append(SuggestedPostMessageAttribute(apiSuggestedPost: suggestedPost)) } - if (flags2 & (1 << 8)) != 0 { - attributes.append(PublishedSuggestedPostMessageAttribute()) + if (flags2 & (1 << 8)) != 0 || (flags2 & (1 << 9)) != 0 { + attributes.append(PublishedSuggestedPostMessageAttribute(currency: (flags2 & (1 << 8)) != 0 ? .stars : .ton)) } var storeFlags = StoreMessageFlags() diff --git a/submodules/TelegramCore/Sources/SyncCore/SuggestedPostMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SuggestedPostMessageAttribute.swift index 559e558bd7..390710fb04 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SuggestedPostMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SuggestedPostMessageAttribute.swift @@ -93,16 +93,24 @@ extension SuggestedPostMessageAttribute { } public final class PublishedSuggestedPostMessageAttribute: Equatable, MessageAttribute { - public init() { + public let currency: CurrencyAmount.Currency + + public init(currency: CurrencyAmount.Currency) { + self.currency = currency } public init(decoder: PostboxDecoder) { + self.currency = CurrencyAmount.Currency(rawValue: decoder.decodeInt32ForKey("c", orElse: 0)) ?? .stars } public func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt32(self.currency.rawValue, forKey: "c") } public static func == (lhs: PublishedSuggestedPostMessageAttribute, rhs: PublishedSuggestedPostMessageAttribute) -> Bool { + if lhs.currency != rhs.currency { + return false + } return true } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift index a0c94370ed..90b3c169ec 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift @@ -618,7 +618,7 @@ 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) + transactions.insert(.init(flags: [.isLocal], id: "\(arc4random())", count: balance, currency: self.ton ? .ton : .stars, date: Int32(Date().timeIntervalSince1970), peer: .appStore, title: nil, description: nil, photo: nil, transactionDate: nil, transactionUrl: nil, paidMessageId: nil, giveawayMessageId: nil, media: [], subscriptionPeriod: nil, starGift: nil, floodskipNumber: nil, starrefCommissionPermille: nil, starrefPeerId: nil, starrefAmount: nil, paidMessageCount: nil, premiumGiftMonths: nil), at: 0) } self.updateState(StarsContext.State(flags: [.isPendingBalance], balance: max(StarsAmount(value: 0, nanos: 0), state.balance + balance), subscriptions: state.subscriptions, canLoadMoreSubscriptions: state.canLoadMoreSubscriptions, transactions: transactions, canLoadMoreTransactions: state.canLoadMoreTransactions, isLoading: state.isLoading)) @@ -723,8 +723,10 @@ private extension StarsContext.State.Transaction { let media = extendedMedia.flatMap({ $0.compactMap { textMediaAndExpirationTimerFromApiMedia($0, PeerId(0)).media } }) ?? [] let _ = subscriptionPeriod + + let amount = CurrencyAmount(apiAmount: stars) - self.init(flags: flags, id: id, count: 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: amount.amount, currency: amount.currency, date: date, peer: parsedPeer, title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), transactionDate: transactionDate, transactionUrl: transactionUrl, paidMessageId: paidMessageId, giveawayMessageId: giveawayMessageId, media: media, subscriptionPeriod: subscriptionPeriod, starGift: starGift.flatMap { StarGift(apiStarGift: $0) }, floodskipNumber: floodskipNumber, starrefCommissionPermille: starrefCommissionPermille, starrefPeerId: starrefPeer?.peerId, starrefAmount: starrefAmount.flatMap(StarsAmount.init(apiAmount:)), paidMessageCount: paidMessageCount, premiumGiftMonths: premiumGiftMonths) } } } @@ -790,6 +792,7 @@ public final class StarsContext { public let flags: Flags public let id: String public let count: StarsAmount + public let currency: CurrencyAmount.Currency public let date: Int32 public let peer: Peer public let title: String? @@ -813,6 +816,7 @@ public final class StarsContext { flags: Flags, id: String, count: StarsAmount, + currency: CurrencyAmount.Currency, date: Int32, peer: Peer, title: String?, @@ -835,6 +839,7 @@ public final class StarsContext { self.flags = flags self.id = id self.count = count + self.currency = currency self.date = date self.peer = peer self.title = title @@ -1074,7 +1079,7 @@ public final class StarsContext { return peerId! } - let ton: Bool + public let ton: Bool public var currentState: StarsContext.State? { var state: StarsContext.State? diff --git a/submodules/TelegramStringFormatting/Sources/CurrencyFormat.swift b/submodules/TelegramStringFormatting/Sources/CurrencyFormat.swift index faf6979c99..3733889b57 100644 --- a/submodules/TelegramStringFormatting/Sources/CurrencyFormat.swift +++ b/submodules/TelegramStringFormatting/Sources/CurrencyFormat.swift @@ -63,6 +63,17 @@ private func loadCurrencyFormatterEntries() -> [String: CurrencyFormatterEntry] } } + let tonEntry = CurrencyFormatterEntry( + symbol: "TON", + thousandsSeparator: ".", + decimalSeparator: ",", + symbolOnLeft: true, + spaceBetweenAmountAndSymbol: false, + decimalDigits: 9 + ) + result["TON"] = tonEntry + result["ton"] = tonEntry + return result } diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index 459dde1f40..4f24755979 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -1106,10 +1106,17 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, var range = NSRange(location: NSNotFound, length: 0) range = (mutableString.string as NSString).range(of: "{amount}") if range.location != NSNotFound { - if currency == "XTR" { + if currency == "TON" { + let amountAttributedString = NSMutableAttributedString(string: "#\(formatTonAmountText(totalAmount, dateTimeFormat: dateTimeFormat))", font: titleBoldFont, textColor: primaryTextColor) + if let range = amountAttributedString.string.range(of: "#") { + amountAttributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .ton(tinted: true)), range: NSRange(range, in: amountAttributedString.string)) + amountAttributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: amountAttributedString.string)) + } + mutableString.replaceCharacters(in: range, with: amountAttributedString) + } else if currency == "XTR" { let amountAttributedString = NSMutableAttributedString(string: "#\(totalAmount)", font: titleBoldFont, textColor: primaryTextColor) if let range = amountAttributedString.string.range(of: "#") { - amountAttributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: true)), range: NSRange(range, in: amountAttributedString.string)) + amountAttributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: amountAttributedString.string)) amountAttributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: amountAttributedString.string)) } mutableString.replaceCharacters(in: range, with: amountAttributedString) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift index 14cdf5eb20..52e44d335a 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift @@ -112,6 +112,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { private var fetchDisposable: Disposable? private var setupTimestamp: Double? + private var cachedTonImage: (UIImage, UIColor)? + required public init() { self.labelNode = TextNode() self.labelNode.isUserInteractionEnabled = false @@ -339,6 +341,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { let currentIsExpanded = self.isExpanded + let cachedTonImage = self.cachedTonImage + return { item, layoutConstants, _, _, _, _ in let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .always, forceFullCorners: false, forceAlignment: .center) @@ -425,8 +429,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { let cryptoAmount = cryptoAmount ?? 0 - title = item.presentationData.strings.Notification_StarsGift_Title(Int32(cryptoAmount)) - text = incoming ? item.presentationData.strings.Notification_StarsGift_Subtitle : item.presentationData.strings.Notification_StarsGift_SubtitleYou(peerName).string + title = "$ \(formatTonAmountText(cryptoAmount, dateTimeFormat: item.presentationData.dateTimeFormat))" + text = incoming ? "Use TON to unlock content and services on Telegram." : "With TON, \(peerName) will be able to unlock content and services on Telegram." case let .prizeStars(count, _, channelId, _, _): if count <= 1000 { months = 3 @@ -596,7 +600,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { } else { title = isStoryEntity ? uniqueGift.title : item.presentationData.strings.Notification_StarGift_Title(authorName).string } - text = isStoryEntity ? "**\(item.presentationData.strings.Notification_StarGift_Collectible) #\(presentationStringsFormattedNumber(uniqueGift.number, item.presentationData.dateTimeFormat.groupingSeparator))**" : "**\(uniqueGift.title) #\(presentationStringsFormattedNumber(uniqueGift.number, item.presentationData.dateTimeFormat.groupingSeparator))**" + text = isStoryEntity ? "**\(item.presentationData.strings.Notification_StarGift_Collectible) #\(presentationStringsFormattedNumber(uniqueGift.number, item.presentationData.dateTimeFormat.groupingSeparator))**" : "**\(uniqueGift.title) #\(presentationStringsFormattedNumber(uniqueGift.number, item.presentationData.dateTimeFormat.groupingSeparator))**" ribbonTitle = isStoryEntity ? "" : item.presentationData.strings.Notification_StarGift_Gift buttonTitle = isStoryEntity ? "" : item.presentationData.strings.Notification_StarGift_View modelTitle = item.presentationData.strings.Notification_StarGift_Model @@ -648,7 +652,32 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: attributedString, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: constrainedSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: Font.semibold(15.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + let titleAttributedString = NSMutableAttributedString(attributedString: NSAttributedString(string: title, font: Font.semibold(15.0), textColor: primaryTextColor, paragraphAlignment: .center)) + var updatedCachedTonImage: (UIImage, UIColor)? = cachedTonImage + if let range = titleAttributedString.string.range(of: "$") { + if updatedCachedTonImage == nil || updatedCachedTonImage?.1 != primaryTextColor { + if let image = generateTintedImage(image: UIImage(bundleImageName: "Ads/TonAbout"), color: primaryTextColor) { + let imageScale: CGFloat = 0.8 + let imageSize = CGSize(width: floor(image.size.width * imageScale), height: floor(image.size.height * imageScale)) + updatedCachedTonImage = (generateImage(CGSize(width: imageSize.width + 2.0, height: imageSize.height), opaque: false, scale: nil, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + UIGraphicsPushContext(context) + defer { + UIGraphicsPopContext() + } + image.draw(in: CGRect(origin: CGPoint(x: 2.0, y: 0.0), size: imageSize)) + })!, primaryTextColor) + } + } + if let tonImage = updatedCachedTonImage?.0 { + titleAttributedString.addAttribute(.attachment, value: tonImage, range: NSRange(range, in: titleAttributedString.string)) + titleAttributedString.addAttribute(.foregroundColor, value: primaryTextColor, range: NSRange(range, in: titleAttributedString.string)) + titleAttributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: titleAttributedString.string)) + titleAttributedString.addAttribute(.kern, value: 2.0, range: NSRange(range, in: titleAttributedString.string)) + } + } + + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) let (moreLayout, moreApply) = makeMoreTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Notification_PremiumGift_More, font: Font.semibold(13.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) @@ -853,6 +882,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.animationNode.updateLayout(size: iconSize) strongSelf.placeholderNode.frame = animationFrame + strongSelf.cachedTonImage = updatedCachedTonImage + let _ = labelApply() let _ = titleApply() let _ = subtitleApply(TextNodeWithEntities.Arguments( diff --git a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift index 5d0490c400..e86a2fef1c 100644 --- a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift +++ b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift @@ -487,8 +487,11 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { if tinted { self.updateTintColor() } - case .ton: - self.updateTon() + case let .ton(tinted): + self.updateTon(tinted: tinted) + if tinted { + self.updateTintColor() + } case let .animation(name): self.updateLocalAnimation(name: name, attemptSynchronousLoad: attemptSynchronousLoad) case .verification: @@ -581,7 +584,7 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { } } else if let emoji = self.arguments?.emoji, let custom = emoji.custom { switch custom { - case .stars(true), .verification: + case .stars(true), .ton(true), .verification: customColor = self.dynamicColor default: break @@ -687,8 +690,8 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { self.contents = tinted ? tintedStarImage?.cgImage : starImage?.cgImage } - private func updateTon() { - self.contents = tonImage?.cgImage + private func updateTon(tinted: Bool) { + self.contents = tinted ? tintedTonImage?.cgImage : tonImage?.cgImage } private func updateVerification() { @@ -1053,6 +1056,16 @@ private let tonImage: UIImage? = { })?.withRenderingMode(.alwaysTemplate) }() +private let tintedTonImage: UIImage? = { + generateImage(CGSize(width: 32.0, height: 32.0), contextGenerator: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + + if let image = generateTintedImage(image: UIImage(bundleImageName: "Ads/TonBig"), color: .white), let cgImage = image.cgImage { + context.draw(cgImage, in: CGRect(origin: .zero, size: size).insetBy(dx: 4.0, dy: 4.0), byTiling: false) + } + })?.withRenderingMode(.alwaysTemplate) +}() + private let verificationImage: UIImage? = { if let backgroundImage = UIImage(bundleImageName: "Peer Info/VerifiedIconBackground"), let foregroundImage = UIImage(bundleImageName: "Peer Info/VerifiedIconForeground") { return generateImage(backgroundImage.size, contextGenerator: { size, context in diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index ffd4026643..08e1341526 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -1609,7 +1609,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese let string = "*\(formatTonAmountText(revenueBalance, dateTimeFormat: presentationData.dateTimeFormat))" let attributedString = NSMutableAttributedString(string: string, font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize), textColor: presentationData.theme.list.itemSecondaryTextColor) if let range = attributedString.string.range(of: "*") { - attributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .ton), range: NSRange(range, in: attributedString.string)) + attributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .ton(tinted: false)), range: NSRange(range, in: attributedString.string)) attributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: attributedString.string)) } items[.balances]!.append(PeerInfoScreenDisclosureItem(id: 21, label: .attributedText(attributedString), text: presentationData.strings.PeerInfo_BotBalance_Ton, icon: PresentationResourcesSettings.ton, action: { @@ -1933,7 +1933,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese attributedString.append(starsAttributedString) } if let range = attributedString.string.range(of: "#") { - attributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .ton), range: NSRange(range, in: attributedString.string)) + attributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .ton(tinted: false)), range: NSRange(range, in: attributedString.string)) attributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: attributedString.string)) } if let range = attributedString.string.range(of: "*") { diff --git a/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift b/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift index b372222740..5711ab370d 100644 --- a/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift @@ -372,18 +372,36 @@ public final class StarsAvatarComponent: Component { } public final class StarsLabelComponent: CombinedComponent { + let theme: PresentationTheme + let currency: CurrencyAmount.Currency + let textColor: UIColor let text: NSAttributedString let subtext: NSAttributedString? public init( + theme: PresentationTheme, + currency: CurrencyAmount.Currency, + textColor: UIColor, text: NSAttributedString, subtext: NSAttributedString? = nil ) { + self.currency = currency + self.theme = theme + self.textColor = textColor self.text = text self.subtext = subtext } public static func ==(lhs: StarsLabelComponent, rhs: StarsLabelComponent) -> Bool { + if lhs.currency != rhs.currency { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.textColor != rhs.textColor { + return false + } if lhs.text != rhs.text { return false } @@ -406,7 +424,6 @@ public final class StarsLabelComponent: CombinedComponent { availableSize: CGSize(width: 140.0, height: 40.0), transition: context.transition ) - var subtext: _UpdatedChildComponent? = nil if let sublabel = component.subtext { @@ -417,11 +434,12 @@ public final class StarsLabelComponent: CombinedComponent { ) } - let iconSize = CGSize(width: 20.0, height: 20.0) + let iconSize: CGSize = component.currency == .ton ? CGSize(width: 16.0, height: 16.0) : CGSize(width: 20.0, height: 20.0) let icon = icon.update( component: BundleIconComponent( - name: "Premium/Stars/StarMedium", - tintColor: nil + name: component.currency == .ton ? "Ads/TonBig" : "Premium/Stars/StarMedium", + tintColor: component.currency == .ton ? component.textColor : nil, + maxSize: iconSize ), availableSize: iconSize, transition: context.transition diff --git a/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift b/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift index 3ad8a621a9..36380871b8 100644 --- a/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift @@ -306,6 +306,7 @@ public final class StarsImageComponent: Component { public enum Icon { case star + case ton } public let context: AccountContext @@ -865,7 +866,7 @@ public final class StarsImageComponent: Component { animationNode.updateLayout(size: animationFrame.size) } - if let _ = component.icon { + if let icon = component.icon { let smallIconView: UIImageView let smallIconOutlineView: UIImageView if let current = self.smallIconView, let currentOutline = self.smallIconOutlineView { @@ -880,15 +881,27 @@ public final class StarsImageComponent: Component { containerNode.view.addSubview(smallIconView) self.smallIconView = smallIconView - smallIconOutlineView.image = UIImage(bundleImageName: "Premium/Stars/TransactionStarOutline")?.withRenderingMode(.alwaysTemplate) - smallIconView.image = UIImage(bundleImageName: "Premium/Stars/TransactionStar") + switch icon { + case .star: + smallIconOutlineView.image = UIImage(bundleImageName: "Premium/Stars/TransactionStarOutline")?.withRenderingMode(.alwaysTemplate) + smallIconView.image = UIImage(bundleImageName: "Premium/Stars/TransactionStar") + case .ton: + smallIconOutlineView.image = UIImage(bundleImageName: "Ads/TonMedium")?.withRenderingMode(.alwaysTemplate) + smallIconView.image = UIImage(bundleImageName: "Ads/TonMedium")?.withRenderingMode(.alwaysTemplate) + } } smallIconOutlineView.tintColor = component.backgroundColor - if let icon = smallIconView.image { - let smallIconFrame = CGRect(origin: CGPoint(x: imageFrame.maxX - icon.size.width, y: imageFrame.maxY - icon.size.height), size: icon.size) + if let iconImage = smallIconView.image { + let smallIconFrame = CGRect(origin: CGPoint(x: imageFrame.maxX - iconImage.size.width, y: imageFrame.maxY - iconImage.size.height), size: iconImage.size) smallIconView.frame = smallIconFrame + switch icon { + case .star: + smallIconView.tintColor = nil + case .ton: + smallIconView.tintColor = component.theme.list.itemAccentColor + } smallIconOutlineView.frame = smallIconFrame } } else if let smallIconView = self.smallIconView, let smallIconOutlineView = self.smallIconOutlineView { diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift index a54f61bdb7..865ebaccdd 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift @@ -217,7 +217,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { var statusText: String? var statusIsDestructive = false - let count: StarsAmount + let count: CurrencyAmount var countIsGeneric = false var countOnTop = false var transactionId: String? @@ -257,7 +257,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { titleText = strings.Stars_Transaction_Giveaway_Boost_Stars(Int32(stars)) descriptionText = "" boostsText = strings.Stars_Transaction_Giveaway_Boost_Boosts(boosts) - count = StarsAmount(value: stars, nanos: 0) + count = CurrencyAmount(amount: StarsAmount(value: stars, nanos: 0), currency: .stars) date = boost.date toPeer = state.peerMap[peerId] giveawayMessageId = boost.giveawayMessageId @@ -266,7 +266,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { let usdValue = formatTonUsdValue(pricing.amount.value, divide: false, rate: usdRate, dateTimeFormat: environment.dateTimeFormat) titleText = strings.Stars_Transaction_Subscription_Title descriptionText = strings.Stars_Transaction_Subscription_PerMonthUsd(usdValue).string - count = pricing.amount + count = CurrencyAmount(amount: pricing.amount, currency: .stars) countOnTop = true date = importer.date toPeer = importer.peer.peer.flatMap(EnginePeer.init) @@ -288,7 +288,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { photo = subscription.photo descriptionText = "" - count = subscription.pricing.amount + count = CurrencyAmount(amount: subscription.pricing.amount, currency: .stars) date = subscription.untilDate if let creationDate = (subscription.peer._asPeer() as? TelegramChannel)?.creationDate, creationDate > 0 { additionalDate = creationDate @@ -376,7 +376,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { titleText = gift.title descriptionText = "\(strings.Gift_Unique_Collectible) #\(presentationStringsFormattedNumber(gift.number, dateTimeFormat.groupingSeparator))" } - count = transaction.count + count = CurrencyAmount(amount: transaction.count, currency: transaction.currency) 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 = CurrencyAmount(amount: transaction.count, currency: transaction.currency) 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 = CurrencyAmount(amount: transaction.count, currency: transaction.currency) 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 = CurrencyAmount(amount: transaction.count, currency: transaction.currency) countOnTop = true transactionId = transaction.id date = transaction.date @@ -446,7 +446,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { countOnTop = false descriptionText = "" } - count = transaction.count + count = CurrencyAmount(amount: transaction.count, currency: transaction.currency) 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 = CurrencyAmount(amount: transaction.count, currency: transaction.currency) transactionId = transaction.id date = transaction.date if case let .peer(peer) = transaction.peer { @@ -545,7 +545,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { messageId = transaction.paidMessageId - count = transaction.count + count = CurrencyAmount(amount: transaction.count, currency: transaction.currency) transactionId = transaction.id date = transaction.date if case let .peer(peer) = transaction.peer { @@ -564,7 +564,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { case let .receipt(receipt): titleText = receipt.invoiceMedia.title descriptionText = receipt.invoiceMedia.description - count = StarsAmount(value: (receipt.invoice.prices.first?.amount ?? receipt.invoiceMedia.totalAmount) * -1, nanos: 0) + count = CurrencyAmount(amount: StarsAmount(value: (receipt.invoice.prices.first?.amount ?? receipt.invoiceMedia.totalAmount) * -1, nanos: 0), currency: .stars) transactionId = receipt.transactionId date = receipt.date if let peer = state.peerMap[receipt.botPaymentId] { @@ -581,7 +581,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { if case let .giftStars(_, _, countValue, _, _, _) = action.action { titleText = incoming ? strings.Stars_Gift_Received_Title : strings.Stars_Gift_Sent_Title - count = StarsAmount(value: countValue, nanos: 0) + count = CurrencyAmount(amount: StarsAmount(value: countValue, nanos: 0), currency: .stars) if !incoming { countIsGeneric = true } @@ -595,7 +595,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { } else if case let .prizeStars(countValue, _, boostPeerId, _, giveawayMessageIdValue) = action.action { titleText = strings.Stars_Transaction_Giveaway_Title - count = StarsAmount(value: countValue, nanos: 0) + count = CurrencyAmount(amount: StarsAmount(value: countValue, nanos: 0), currency: .stars) countOnTop = true transactionId = nil giveawayMessageId = giveawayMessageIdValue @@ -648,8 +648,14 @@ private final class StarsTransactionSheetContent: CombinedComponent { headerTextColor = theme.actionSheet.primaryTextColor } - let absCount = StarsAmount(value: abs(count.value), nanos: abs(count.nanos)) - let formattedAmount = formatStarsAmountText(absCount, dateTimeFormat: dateTimeFormat) + let absCount = StarsAmount(value: abs(count.amount.value), nanos: abs(count.amount.nanos)) + let formattedAmount: String + switch count.currency { + case .stars: + formattedAmount = formatStarsAmountText(absCount, dateTimeFormat: dateTimeFormat) + case .ton: + formattedAmount = formatTonAmountText(absCount.value, dateTimeFormat: dateTimeFormat) + } let countColor: UIColor var countFont: UIFont = isSubscription || isSubscriber ? Font.regular(17.0) : Font.semibold(17.0) var countBackgroundColor: UIColor? @@ -664,7 +670,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { } else if countIsGeneric { amountText = "\(formattedAmount)" countColor = theme.list.itemPrimaryTextColor - } else if count < StarsAmount.zero { + } else if count.amount < StarsAmount.zero { amountText = "- \(formattedAmount)" if case .unique = giftAnimationSubject { countColor = .white @@ -706,9 +712,9 @@ private final class StarsTransactionSheetContent: CombinedComponent { imageSubject = .gift(premiumGiftMonths) } else if isGift { var value: Int32 = 3 - if count.value <= 1000 { + if count.amount.value <= 1000 { value = 3 - } else if count.value < 2500 { + } else if count.amount.value < 2500 { value = 6 } else { value = 12 @@ -726,9 +732,9 @@ private final class StarsTransactionSheetContent: CombinedComponent { imageSubject = .none } if isSubscription || isSubscriber || isSubscriptionFee || giveawayMessageId != nil { - imageIcon = .star + imageIcon = count.currency == .ton ? .ton : .star } else { - imageIcon = nil + imageIcon = count.currency == .ton ? .ton : nil } if isSubscription && "".isEmpty { @@ -811,10 +817,26 @@ private final class StarsTransactionSheetContent: CombinedComponent { transition: .immediate ) + let amountStarIconName: String + var amountStarTintColor: UIColor? + var amountStarMaxSize: CGSize? + var amountOffset = CGPoint() + if boostsText != nil { + amountStarIconName = "Premium/BoostButtonIcon" + } else if case .ton = count.currency { + amountStarIconName = "Ads/TonBig" + amountStarTintColor = countColor + amountStarMaxSize = CGSize(width: 14.0, height: 14.0) + amountOffset.y += 3.0 + } else { + amountStarIconName = "Premium/Stars/StarMedium" + } + let amountStar = amountStar.update( component: BundleIconComponent( - name: boostsText != nil ? "Premium/BoostButtonIcon" : "Premium/Stars/StarMedium", - tintColor: nil + name: amountStarIconName, + tintColor: amountStarTintColor, + maxSize: amountStarMaxSize ), availableSize: context.availableSize, transition: .immediate @@ -836,7 +858,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { )) } else if case .unique = giftAnimationSubject { let reason: String - if count < StarsAmount.zero, case let .transaction(transaction, _) = subject { + if count.amount < StarsAmount.zero, case let .transaction(transaction, _) = subject { if transaction.flags.contains(.isStarGiftResale) { reason = strings.Stars_Transaction_GiftPurchase } else { @@ -892,7 +914,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { } else if isSubscriber { title = strings.Stars_Transaction_Subscription_Subscriber } else { - title = count < StarsAmount.zero || countIsGeneric ? strings.Stars_Transaction_To : strings.Stars_Transaction_From + title = count.amount < StarsAmount.zero || countIsGeneric ? strings.Stars_Transaction_To : strings.Stars_Transaction_From } let toComponent: AnyComponent @@ -997,7 +1019,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { id: "prize", title: strings.Stars_Transaction_Giveaway_Prize, component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Stars_Transaction_Giveaway_Stars(Int32(count.value)), font: tableFont, textColor: tableTextColor))) + MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Stars_Transaction_Giveaway_Stars(Int32(count.amount.value)), font: tableFont, textColor: tableTextColor))) ) )) @@ -1499,6 +1521,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { amountLabelOffsetY = 2.0 amountStarOffsetY = 5.0 } + amountStarOffsetY += amountOffset.y context.add(amount .position(CGPoint(x: amountLabelOriginX, y: amountOrigin + amount.size.height / 2.0 + amountLabelOffsetY)) diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift index 872bca586c..ffea44a602 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift @@ -16,6 +16,7 @@ final class StarsBalanceComponent: Component { let theme: PresentationTheme let strings: PresentationStrings let dateTimeFormat: PresentationDateTimeFormat + let currency: CurrencyAmount.Currency let count: StarsAmount let rate: Double? let actionTitle: String @@ -34,6 +35,7 @@ final class StarsBalanceComponent: Component { theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, + currency: CurrencyAmount.Currency, count: StarsAmount, rate: Double?, actionTitle: String, @@ -51,6 +53,7 @@ final class StarsBalanceComponent: Component { self.theme = theme self.strings = strings self.dateTimeFormat = dateTimeFormat + self.currency = currency self.count = count self.rate = rate self.actionTitle = actionTitle @@ -164,7 +167,13 @@ final class StarsBalanceComponent: Component { let sideInset: CGFloat = 16.0 var contentHeight: CGFloat = sideInset - let formattedLabel = formatStarsAmountText(component.count, dateTimeFormat: component.dateTimeFormat) + let formattedLabel: String + switch component.currency { + case .stars: + formattedLabel = formatStarsAmountText(component.count, dateTimeFormat: component.dateTimeFormat) + case .ton: + formattedLabel = formatTonAmountText(component.count.value, dateTimeFormat: component.dateTimeFormat) + } let labelFont: UIFont if formattedLabel.contains(component.dateTimeFormat.decimalSeparator) { labelFont = Font.with(size: 48.0, design: .round, weight: .semibold) diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsStatisticsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsStatisticsScreen.swift index 904e99a6e8..e29f5a3506 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsStatisticsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsStatisticsScreen.swift @@ -579,6 +579,7 @@ final class StarsStatisticsScreenComponent: Component { theme: environment.theme, strings: strings, dateTimeFormat: environment.dateTimeFormat, + currency: .stars, count: self.starsState?.balances.availableBalance ?? StarsAmount.zero, rate: self.starsState?.usdRate ?? 0, actionTitle: strings.Stars_Intro_BuyShort, @@ -622,6 +623,7 @@ final class StarsStatisticsScreenComponent: Component { theme: environment.theme, strings: strings, dateTimeFormat: environment.dateTimeFormat, + currency: .stars, count: self.starsState?.balances.availableBalance ?? StarsAmount.zero, rate: self.starsState?.usdRate ?? 0, actionTitle: strings.Stars_BotRevenue_Withdraw_WithdrawShort, diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift index d4dd8e3d61..c70cf69b24 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift @@ -409,7 +409,13 @@ final class StarsTransactionsListPanelComponent: Component { } let itemLabel: NSAttributedString - let formattedLabel = formatStarsAmountText(item.count, dateTimeFormat: environment.dateTimeFormat, showPlus: true) + let formattedLabel: String + switch item.currency { + case .stars: + formattedLabel = formatStarsAmountText(item.count, dateTimeFormat: environment.dateTimeFormat, showPlus: true) + case .ton: + formattedLabel = formatTonAmountText(item.count.value, dateTimeFormat: environment.dateTimeFormat, showPlus: true) + } let smallLabelFont = Font.with(size: floor(fontBaseDisplaySize / 17.0 * 13.0)) let labelFont = Font.medium(fontBaseDisplaySize) @@ -496,7 +502,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(theme: environment.theme, currency: item.currency, textColor: labelColor, text: itemLabel))), 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/StarsTransactionsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift index c5f800167d..ee4289cdd4 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift @@ -522,12 +522,23 @@ final class StarsTransactionsScreenComponent: Component { } starTransition.setBounds(view: starView, bounds: starFrame) } + + let titleString: String + let descriptionString: String + if component.starsContext.ton { + //TODO:localize + titleString = "TON" + descriptionString = "Use TON to unlock content and services on Telegram" + } else { + titleString = environment.strings.Stars_Intro_Title + descriptionString = environment.strings.Stars_Intro_Description + } let titleSize = self.titleView.update( transition: .immediate, component: AnyComponent( MultilineTextComponent( - text: .plain(NSAttributedString(string: environment.strings.Stars_Intro_Title, font: Font.bold(28.0), textColor: environment.theme.list.itemPrimaryTextColor)), + text: .plain(NSAttributedString(string: titleString, font: Font.bold(28.0), textColor: environment.theme.list.itemPrimaryTextColor)), horizontalAlignment: .center, truncationType: .end, maximumNumberOfLines: 1 @@ -557,7 +568,12 @@ final class StarsTransactionsScreenComponent: Component { containerSize: CGSize(width: 120.0, height: 100.0) ) - let formattedBalance = formatStarsAmountText(self.starsState?.balance ?? StarsAmount.zero, dateTimeFormat: environment.dateTimeFormat) + let formattedBalance: String + if component.starsContext.ton { + formattedBalance = formatTonAmountText(self.starsState?.balance.value ?? 0, dateTimeFormat: environment.dateTimeFormat) + } else { + 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) @@ -573,7 +589,11 @@ final class StarsTransactionsScreenComponent: Component { ) let topBalanceIconSize = self.topBalanceIconView.update( transition: .immediate, - component: AnyComponent(BundleIconComponent(name: "Premium/Stars/StarSmall", tintColor: nil)), + component: AnyComponent(BundleIconComponent( + name: component.starsContext.ton ? "Ads/TonBig" : "Premium/Stars/StarSmall", + tintColor: component.starsContext.ton ? environment.theme.list.itemAccentColor : nil, + maxSize: component.starsContext.ton ? CGSize(width: 12.0, height: 12.0) : nil + )), environment: {}, containerSize: availableSize ) @@ -598,7 +618,10 @@ final class StarsTransactionsScreenComponent: Component { 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) + var topBalanceIconFrame = CGRect(origin: CGPoint(x: topBalanceValueFrame.minX - topBalanceIconSize.width - 2.0, y: floorToScreenPixels(topBalanceValueFrame.midY - topBalanceIconSize.height / 2.0) - UIScreenPixel), size: topBalanceIconSize) + if component.starsContext.ton { + topBalanceIconFrame.origin.y += 1.0 - UIScreenPixel + } if let topBalanceIconView = self.topBalanceIconView.view { if topBalanceIconView.superview == nil { topBalanceIconView.alpha = 0.0 @@ -613,7 +636,7 @@ final class StarsTransactionsScreenComponent: Component { transition: .immediate, component: AnyComponent( BalancedTextComponent( - text: .plain(NSAttributedString(string: environment.strings.Stars_Intro_Description, font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor)), + text: .plain(NSAttributedString(string: descriptionString, font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor)), horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.2 @@ -648,6 +671,7 @@ final class StarsTransactionsScreenComponent: Component { theme: environment.theme, strings: environment.strings, dateTimeFormat: environment.dateTimeFormat, + currency: component.starsContext.ton ? .ton : .stars, count: self.starsState?.balance ?? StarsAmount.zero, rate: nil, actionTitle: withdrawAvailable ? environment.strings.Stars_Intro_BuyShort : environment.strings.Stars_Intro_Buy, @@ -705,7 +729,7 @@ final class StarsTransactionsScreenComponent: Component { contentHeight += 34.0 var canJoinRefProgram = false - if let data = component.context.currentAppConfiguration.with({ $0 }).data, let value = data["starref_connect_allowed"] { + if !component.starsContext.ton, let data = component.context.currentAppConfiguration.with({ $0 }).data, let value = data["starref_connect_allowed"] { if let value = value as? Double { canJoinRefProgram = value != 0.0 } else if let value = value as? Bool { @@ -835,10 +859,11 @@ final class StarsTransactionsScreenComponent: Component { MultilineTextComponent(text: .plain(NSAttributedString(string: isExpired ? environment.strings.Stars_Intro_Subscriptions_ExpiredStatus : environment.strings.Stars_Intro_Subscriptions_Cancelled, font: Font.regular(floor(fontBaseDisplaySize * 13.0 / 17.0)), textColor: environment.theme.list.itemDestructiveColor))) )) } else { - let itemLabel = NSAttributedString(string: "\(subscription.pricing.amount)", font: Font.medium(fontBaseDisplaySize), textColor: environment.theme.list.itemPrimaryTextColor) + let itemLabelColor = environment.theme.list.itemPrimaryTextColor + let itemLabel = NSAttributedString(string: "\(subscription.pricing.amount)", font: Font.medium(fontBaseDisplaySize), textColor: itemLabelColor) let itemSublabel = NSAttributedString(string: environment.strings.Stars_Intro_Subscriptions_PerMonth, font: Font.regular(floor(fontBaseDisplaySize * 13.0 / 17.0)), textColor: environment.theme.list.itemSecondaryTextColor) - labelComponent = AnyComponentWithIdentity(id: "label", component: AnyComponent(StarsLabelComponent(text: itemLabel, subtext: itemSublabel))) + labelComponent = AnyComponentWithIdentity(id: "label", component: AnyComponent(StarsLabelComponent(theme: environment.theme, currency: component.starsContext.ton ? .ton : .stars, textColor: itemLabelColor, text: itemLabel, subtext: itemSublabel))) } subscriptionsItems.append(AnyComponentWithIdentity( diff --git a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift index 231d091348..5f6153be25 100644 --- a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift @@ -91,7 +91,7 @@ private final class SheetContent: CombinedComponent { theme: environment.theme, strings: environment.strings, currency: state.currency, - balance: state.balance, + balance: state.currency == .stars ? state.starsBalance : state.tonBalance, alignment: .right ), availableSize: CGSize(width: 200.0, height: 200.0), @@ -169,7 +169,7 @@ private final class SheetContent: CombinedComponent { amountPlaceholder = environment.strings.Stars_Withdraw_AmountPlaceholder minAmount = withdrawConfiguration.minWithdrawAmount.flatMap { StarsAmount(value: $0, nanos: 0) } - maxAmount = state.balance + maxAmount = state.starsBalance case .paidMedia: titleString = environment.strings.Stars_PaidContent_Title amountTitle = environment.strings.Stars_PaidContent_AmountTitle @@ -237,9 +237,9 @@ private final class SheetContent: CombinedComponent { let balance: StarsAmount? if case .accountWithdraw = component.mode { - balance = state.balance + balance = state.starsBalance } else if case .reaction = component.mode { - balance = state.balance + balance = state.starsBalance } else if case let .withdraw(starsState, _) = component.mode { balance = starsState.balances.availableBalance } else { @@ -329,10 +329,16 @@ private final class SheetContent: CombinedComponent { guard let state else { return } + + let currency: CurrencyAmount.Currency if id == AnyHashable(0) { - state.currency = .stars + currency = .stars } else { - state.currency = .ton + currency = .ton + } + if state.currency != currency { + state.currency = currency + state.amount = nil } state.updated(transition: .spring(duration: 0.4)) } @@ -485,6 +491,7 @@ private final class SheetContent: CombinedComponent { placeholderText: amountPlaceholder, labelText: amountLabel, currency: state.currency, + dateTimeFormat: presentationData.dateTimeFormat, amountUpdated: { [weak state] amount in state?.amount = amount.flatMap { StarsAmount(value: $0, nanos: 0) } state?.updated() @@ -632,13 +639,16 @@ private final class SheetContent: CombinedComponent { case .sender: if let amount = state.amount { let currencySymbol: String + let currencyAmount: String switch state.currency { case .stars: currencySymbol = "#" + currencyAmount = presentationStringsFormattedNumber(amount, environment.dateTimeFormat.groupingSeparator) case .ton: currencySymbol = "$" + currencyAmount = formatTonAmountText(amount.value, dateTimeFormat: environment.dateTimeFormat) } - buttonString = "Offer \(currencySymbol) \(presentationStringsFormattedNumber(amount, environment.dateTimeFormat.groupingSeparator))" + buttonString = "Offer \(currencySymbol) \(currencyAmount)" } else { buttonString = "Offer for Free" } @@ -756,8 +766,10 @@ private final class SheetContent: CombinedComponent { fileprivate var currency: CurrencyAmount.Currency = .stars fileprivate var timestamp: Int32? - fileprivate var balance: StarsAmount? - private var stateDisposable: Disposable? + fileprivate var starsBalance: StarsAmount? + private var starsStateDisposable: Disposable? + fileprivate var tonBalance: StarsAmount? + private var tonStateDisposable: Disposable? var cachedCloseImage: (UIImage, PresentationTheme)? var cachedStarImage: (UIImage, PresentationTheme)? @@ -809,14 +821,25 @@ private final class SheetContent: CombinedComponent { default: break } - if needsBalance, let starsContext = component.context.starsContext { - self.stateDisposable = (starsContext.state - |> deliverOnMainQueue).startStrict(next: { [weak self] state in - if let self, let balance = state?.balance { - self.balance = balance - self.updated() - } - }) + if needsBalance { + if let starsContext = component.context.starsContext { + self.starsStateDisposable = (starsContext.state + |> deliverOnMainQueue).startStrict(next: { [weak self] state in + if let self, let balance = state?.balance { + self.starsBalance = balance + self.updated() + } + }) + } + if let tonContext = component.context.tonContext { + self.tonStateDisposable = (tonContext.state + |> deliverOnMainQueue).startStrict(next: { [weak self] state in + if let self, let balance = state?.balance { + self.tonBalance = balance + self.updated() + } + }) + } } if case let .starGiftResell(giftToMatch, update, _) = self.mode { @@ -851,7 +874,8 @@ private final class SheetContent: CombinedComponent { } deinit { - self.stateDisposable?.dispose() + self.starsStateDisposable?.dispose() + self.tonStateDisposable?.dispose() } } @@ -1035,6 +1059,453 @@ public final class StarsWithdrawScreen: ViewControllerComponentContainer { private let invalidAmountCharacters = CharacterSet.decimalDigits.inverted +private final class AmountFieldTonFormatter: NSObject, UITextFieldDelegate { + private struct Representation { + private let format: CurrencyFormat + private var caretIndex: Int = 0 + private var wholePart: [Int] = [] + private var decimalPart: [Int] = [] + + init(string: String, format: CurrencyFormat) { + self.format = format + + var isDecimalPart = false + for c in string { + if c.isNumber { + if let value = Int(String(c)) { + if isDecimalPart { + self.decimalPart.append(value) + } else { + self.wholePart.append(value) + } + } + } else if String(c) == format.decimalSeparator { + isDecimalPart = true + } + } + + while self.wholePart.count > 1 { + if self.wholePart[0] != 0 { + break + } else { + self.wholePart.removeFirst() + } + } + if self.wholePart.isEmpty { + self.wholePart = [0] + } + + while self.decimalPart.count > 1 { + if self.decimalPart[self.decimalPart.count - 1] != 0 { + break + } else { + self.decimalPart.removeLast() + } + } + while self.decimalPart.count < format.decimalDigits { + self.decimalPart.append(0) + } + + self.caretIndex = self.wholePart.count + } + + var minCaretIndex: Int { + for i in 0 ..< self.wholePart.count { + if self.wholePart[i] != 0 { + return i + } + } + return self.wholePart.count + } + + mutating func moveCaret(offset: Int) { + self.caretIndex = max(self.minCaretIndex, min(self.caretIndex + offset, self.wholePart.count + self.decimalPart.count)) + } + + mutating func normalize() { + while self.wholePart.count > 1 { + if self.wholePart[0] != 0 { + break + } else { + self.wholePart.removeFirst() + self.moveCaret(offset: -1) + } + } + if self.wholePart.isEmpty { + self.wholePart = [0] + } + + while self.decimalPart.count < format.decimalDigits { + self.decimalPart.append(0) + } + while self.decimalPart.count > format.decimalDigits { + self.decimalPart.removeLast() + } + + self.caretIndex = max(self.minCaretIndex, min(self.caretIndex, self.wholePart.count + self.decimalPart.count)) + } + + mutating func backspace() { + if self.caretIndex > self.wholePart.count { + let decimalIndex = self.caretIndex - self.wholePart.count + if decimalIndex > 0 { + self.decimalPart.remove(at: decimalIndex - 1) + + self.moveCaret(offset: -1) + self.normalize() + } + } else { + if self.caretIndex > 0 { + self.wholePart.remove(at: self.caretIndex - 1) + + self.moveCaret(offset: -1) + self.normalize() + } + } + } + + mutating func insert(letter: String) { + if letter == "." || letter == "," { + if self.caretIndex == self.wholePart.count { + return + } else if self.caretIndex < self.wholePart.count { + for i in (self.caretIndex ..< self.wholePart.count).reversed() { + self.decimalPart.insert(self.wholePart[i], at: 0) + self.wholePart.remove(at: i) + } + } + + self.normalize() + } else if letter.count == 1 && letter[letter.startIndex].isNumber { + if let value = Int(letter) { + if self.caretIndex <= self.wholePart.count { + self.wholePart.insert(value, at: self.caretIndex) + } else { + let decimalIndex = self.caretIndex - self.wholePart.count + self.decimalPart.insert(value, at: decimalIndex) + } + self.moveCaret(offset: 1) + self.normalize() + } + } + } + + var string: String { + var result = "" + + for digit in self.wholePart { + result.append("\(digit)") + } + result.append(self.format.decimalSeparator) + for digit in self.decimalPart { + result.append("\(digit)") + } + + return result + } + + var stringCaretIndex: Int { + var logicalIndex = 0 + var resolvedIndex = 0 + + if logicalIndex == self.caretIndex { + return resolvedIndex + } + + for _ in self.wholePart { + logicalIndex += 1 + resolvedIndex += 1 + + if logicalIndex == self.caretIndex { + return resolvedIndex + } + } + + resolvedIndex += 1 + + for _ in self.decimalPart { + logicalIndex += 1 + resolvedIndex += 1 + + if logicalIndex == self.caretIndex { + return resolvedIndex + } + } + + return resolvedIndex + } + + var numericalValue: Int64 { + var result: Int64 = 0 + + for digit in self.wholePart { + result *= 10 + result += Int64(digit) + } + for digit in self.decimalPart { + result *= 10 + result += Int64(digit) + } + + return result + } + } + + private let format: CurrencyFormat + private let currency: String + private let maxNumericalValue: Int64 + private let updated: (Int64) -> Void + private let isEmptyUpdated: (Bool) -> Void + private let focusUpdated: (Bool) -> Void + + private var representation: Representation + + private var previousResolvedCaretIndex: Int = 0 + private var ignoreTextSelection: Bool = false + private var enableTextSelectionProcessing: Bool = false + + init?(textField: UITextField, currency: String, maxNumericalValue: Int64, initialValue: String, updated: @escaping (Int64) -> Void, isEmptyUpdated: @escaping (Bool) -> Void, focusUpdated: @escaping (Bool) -> Void) { + guard let format = CurrencyFormat(currency: currency) else { + return nil + } + self.format = format + self.currency = currency + self.maxNumericalValue = maxNumericalValue + self.updated = updated + self.isEmptyUpdated = isEmptyUpdated + self.focusUpdated = focusUpdated + + self.representation = Representation(string: initialValue, format: format) + + super.init() + + textField.text = self.representation.string + self.previousResolvedCaretIndex = self.representation.stringCaretIndex + + self.isEmptyUpdated(false) + } + + func reset(textField: UITextField, initialValue: String) { + self.representation = Representation(string: initialValue, format: self.format) + self.resetFromRepresentation(textField: textField, notifyUpdated: false) + } + + private func resetFromRepresentation(textField: UITextField, notifyUpdated: Bool) { + self.ignoreTextSelection = true + + if self.representation.numericalValue > self.maxNumericalValue { + self.representation = Representation(string: formatCurrencyAmountCustom(self.maxNumericalValue, currency: self.currency).0, format: self.format) + } + + textField.text = self.representation.string + self.previousResolvedCaretIndex = self.representation.stringCaretIndex + + if self.enableTextSelectionProcessing { + let stringCaretIndex = self.representation.stringCaretIndex + if let caretPosition = textField.position(from: textField.beginningOfDocument, offset: stringCaretIndex) { + textField.selectedTextRange = textField.textRange(from: caretPosition, to: caretPosition) + } + } + self.ignoreTextSelection = false + + if notifyUpdated { + self.updated(self.representation.numericalValue) + } + } + + @objc public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + if string.count == 1 { + self.representation.insert(letter: string) + self.resetFromRepresentation(textField: textField, notifyUpdated: true) + } else if string.count == 0 { + self.representation.backspace() + self.resetFromRepresentation(textField: textField, notifyUpdated: true) + } + + return false + } + + @objc public func textFieldShouldReturn(_ textField: UITextField) -> Bool { + return false + } + + @objc public func textFieldDidBeginEditing(_ textField: UITextField) { + self.enableTextSelectionProcessing = true + self.focusUpdated(true) + + let stringCaretIndex = self.representation.stringCaretIndex + self.previousResolvedCaretIndex = stringCaretIndex + if let caretPosition = textField.position(from: textField.beginningOfDocument, offset: stringCaretIndex) { + self.ignoreTextSelection = true + textField.selectedTextRange = textField.textRange(from: caretPosition, to: caretPosition) + DispatchQueue.main.async { + textField.selectedTextRange = textField.textRange(from: caretPosition, to: caretPosition) + self.ignoreTextSelection = false + } + } + } + + @objc public func textFieldDidChangeSelection(_ textField: UITextField) { + if self.ignoreTextSelection { + return + } + if !self.enableTextSelectionProcessing { + return + } + + if let selectedTextRange = textField.selectedTextRange { + let index = textField.offset(from: textField.beginningOfDocument, to: selectedTextRange.end) + if self.previousResolvedCaretIndex != index { + self.representation.moveCaret(offset: self.previousResolvedCaretIndex < index ? 1 : -1) + + let stringCaretIndex = self.representation.stringCaretIndex + self.previousResolvedCaretIndex = stringCaretIndex + if let caretPosition = textField.position(from: textField.beginningOfDocument, offset: stringCaretIndex) { + textField.selectedTextRange = textField.textRange(from: caretPosition, to: caretPosition) + } + } + } + } + + @objc public func textFieldDidEndEditing(_ textField: UITextField) { + self.enableTextSelectionProcessing = false + self.focusUpdated(false) + } +} + +private final class AmountFieldStarsFormatter: NSObject, UITextFieldDelegate { + private let currency: CurrencyAmount.Currency + private let dateTimeFormat: PresentationDateTimeFormat + + private let textField: UITextField + private let minValue: Int64 + private let maxValue: Int64 + private let updated: (Int64) -> Void + private let isEmptyUpdated: (Bool) -> Void + private let animateError: () -> Void + private let focusUpdated: (Bool) -> Void + + init?(textField: UITextField, currency: CurrencyAmount.Currency, dateTimeFormat: PresentationDateTimeFormat, minValue: Int64, maxValue: Int64, updated: @escaping (Int64) -> Void, isEmptyUpdated: @escaping (Bool) -> Void, animateError: @escaping () -> Void, focusUpdated: @escaping (Bool) -> Void) { + self.textField = textField + self.currency = currency + self.dateTimeFormat = dateTimeFormat + self.minValue = minValue + self.maxValue = maxValue + self.updated = updated + self.isEmptyUpdated = isEmptyUpdated + self.animateError = animateError + self.focusUpdated = focusUpdated + + super.init() + } + + func amountFrom(text: String) -> Int64 { + var amount: Int64? + if !text.isEmpty { + switch self.currency { + case .stars: + if let value = Int64(text) { + amount = value + } + case .ton: + let scale: Int64 = 1_000_000_000 // 10⁹ (one “nano”) + if let dot = text.firstIndex(of: ".") { + // Slices for the parts on each side of the dot + var wholeSlice = String(text[.. 9 { + fractionStr = String(fractionStr.prefix(9)) // trim extra digits + } else { + fractionStr = fractionStr.padding( + toLength: 9, withPad: "0", startingAt: 0) // pad with zeros + } + + // Convert and combine + if let whole = Int64(wholeSlice), + let frac = Int64(fractionStr) { + amount = whole * scale + frac + } + } else if let whole = Int64(text) { // string had no dot at all + amount = whole * scale + } + } + } + return amount ?? 0 + } + + func onTextChanged(text: String) { + self.updated(self.amountFrom(text: text)) + self.isEmptyUpdated(text.isEmpty) + } + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + var acceptZero = false + if self.minValue <= 0 { + acceptZero = true + } + + var newText = ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string) + if newText.contains(where: { c in + switch c { + case "0", "1", "2", "3", "4", "5", "6", "7", "8", "9": + return false + default: + if case .ton = self.currency { + if c == "." { + return false + } + } + return true + } + }) { + return false + } + if newText.count(where: { $0 == "." }) > 1 { + return false + } + + switch self.currency { + case .stars: + if (newText == "0" && !acceptZero) || (newText.count > 1 && newText.hasPrefix("0")) { + newText.removeFirst() + textField.text = newText + self.onTextChanged(text: newText) + return false + } + case .ton: + if (newText == "0" && !acceptZero) || (newText.count > 1 && newText.hasPrefix("0") && !newText.hasPrefix("0.")) { + newText.removeFirst() + textField.text = newText + self.onTextChanged(text: newText) + return false + } + } + + let amount: Int64 = self.amountFrom(text: newText) + if amount > self.maxValue { + switch self.currency { + case .stars: + textField.text = "\(self.maxValue)" + case .ton: + textField.text = "\(formatTonAmountText(self.maxValue, dateTimeFormat: self.dateTimeFormat))" + } + self.onTextChanged(text: self.textField.text ?? "") + self.animateError() + return false + } + + self.onTextChanged(text: newText) + + return true + } +} + private final class AmountFieldComponent: Component { typealias EnvironmentType = Empty @@ -1048,6 +1519,7 @@ private final class AmountFieldComponent: Component { let placeholderText: String let labelText: String? let currency: CurrencyAmount.Currency + let dateTimeFormat: PresentationDateTimeFormat let amountUpdated: (Int64?) -> Void let tag: AnyObject? @@ -1062,6 +1534,7 @@ private final class AmountFieldComponent: Component { placeholderText: String, labelText: String?, currency: CurrencyAmount.Currency, + dateTimeFormat: PresentationDateTimeFormat, amountUpdated: @escaping (Int64?) -> Void, tag: AnyObject? = nil ) { @@ -1075,6 +1548,7 @@ private final class AmountFieldComponent: Component { self.placeholderText = placeholderText self.labelText = labelText self.currency = currency + self.dateTimeFormat = dateTimeFormat self.amountUpdated = amountUpdated self.tag = tag } @@ -1127,10 +1601,13 @@ private final class AmountFieldComponent: Component { private let placeholderView: ComponentView private let icon = ComponentView() private let textField: TextFieldNodeView + private var starsFormatter: AmountFieldStarsFormatter? + private var tonFormatter: AmountFieldStarsFormatter? private let labelView: ComponentView private var component: AmountFieldComponent? private weak var state: EmptyComponentState? + private var isUpdating: Bool = false override init(frame: CGRect) { self.placeholderView = ComponentView() @@ -1138,9 +1615,6 @@ private final class AmountFieldComponent: Component { self.labelView = ComponentView() super.init(frame: frame) - - self.textField.delegate = self - self.textField.addTarget(self, action: #selector(self.textChanged(_:)), for: .editingChanged) self.addSubview(self.textField) } @@ -1148,56 +1622,6 @@ private final class AmountFieldComponent: Component { required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - @objc func textChanged(_ sender: Any) { - let text = self.textField.text ?? "" - let amount: Int64? - if !text.isEmpty, let value = Int64(text) { - amount = value - } else { - amount = nil - } - self.component?.amountUpdated(amount) - self.placeholderView.view?.isHidden = !text.isEmpty - } - - func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { - guard let component = self.component else { - return false - } - - if string.rangeOfCharacter(from: invalidAmountCharacters) != nil { - return false - } - - var acceptZero = false - if let minValue = component.minValue, minValue <= 0 { - acceptZero = true - } - - var newText = ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string) - if (newText == "0" && !acceptZero) || (newText.count > 1 && newText.hasPrefix("0")) { - newText.removeFirst() - textField.text = newText - self.textChanged(self.textField) - return false - } - - let amount: Int64? - if !newText.isEmpty, let value = Int64(normalizeArabicNumeralString(newText, type: .western)) { - amount = value - } else { - amount = nil - } - if let amount, let maxAmount = component.maxValue, amount > maxAmount { - textField.text = "\(maxAmount)" - self.textChanged(self.textField) - self.animateError() - return false - } - - return true - } func activateInput() { self.textField.becomeFirstResponder() @@ -1217,18 +1641,109 @@ private final class AmountFieldComponent: Component { } func update(component: AmountFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + self.textField.textColor = component.textColor - if let value = component.value { - self.textField.text = "\(value)" - } else { - self.textField.text = "" + if self.component?.currency != component.currency { + if let value = component.value { + var text = "" + switch component.currency { + case .stars: + text = "\(value)" + case .ton: + text = "\(formatTonAmountText(value, dateTimeFormat: component.dateTimeFormat))" + } + self.textField.text = text + } else { + self.textField.text = "" + } } self.textField.font = Font.regular(17.0) - self.textField.keyboardType = .numberPad self.textField.returnKeyType = .done self.textField.autocorrectionType = .no self.textField.autocapitalizationType = .none + + if self.component?.currency != component.currency { + switch component.currency { + case .stars: + self.textField.delegate = self + self.textField.keyboardType = .numberPad + if self.starsFormatter == nil { + self.starsFormatter = AmountFieldStarsFormatter( + textField: self.textField, + currency: component.currency, + dateTimeFormat: component.dateTimeFormat, + minValue: component.minValue ?? 0, + maxValue: component.maxValue ?? Int64.max, + updated: { [weak self] value in + guard let self, let component = self.component else { + return + } + if !self.isUpdating { + component.amountUpdated(value == 0 ? nil : value) + } + }, + isEmptyUpdated: { [weak self] isEmpty in + guard let self else { + return + } + self.placeholderView.view?.isHidden = !isEmpty + }, + animateError: { [weak self] in + guard let self else { + return + } + self.animateError() + }, + focusUpdated: { _ in + } + ) + } + self.tonFormatter = nil + self.textField.delegate = self.starsFormatter + self.textField.text = "" + case .ton: + self.textField.keyboardType = .numbersAndPunctuation + if self.tonFormatter == nil { + self.tonFormatter = AmountFieldStarsFormatter( + textField: self.textField, + currency: component.currency, + dateTimeFormat: component.dateTimeFormat, + minValue: component.minValue ?? 0, + maxValue: component.maxValue ?? Int64.max, + updated: { [weak self] value in + guard let self, let component = self.component else { + return + } + if !self.isUpdating { + component.amountUpdated(value == 0 ? nil : value) + } + }, + isEmptyUpdated: { [weak self] isEmpty in + guard let self else { + return + } + self.placeholderView.view?.isHidden = !isEmpty + }, + animateError: { [weak self] in + guard let self else { + return + } + self.animateError() + }, + focusUpdated: { _ in + } + ) + } + self.starsFormatter = nil + self.textField.delegate = self.tonFormatter + } + self.textField.reloadInputViews() + } self.component = component self.state = state @@ -1460,7 +1975,13 @@ private final class BalanceComponent: CombinedComponent { let balanceText: String if let value = context.component.balance { - balanceText = "\(value.stringValue)" + switch context.component.currency { + case .stars: + balanceText = "\(value.stringValue)" + case .ton: + let dateTimeFormat = context.component.context.sharedContext.currentPresentationData.with({ $0 }).dateTimeFormat + balanceText = "\(formatTonAmountText(value.value, dateTimeFormat: dateTimeFormat))" + } } else { balanceText = "..." } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift index b72d5a0be0..bf51e55999 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -1817,16 +1817,26 @@ extension ChatControllerImpl { return } - if actions.options.contains(.deleteGlobally) && messages.contains(where: { message in message.attributes.contains(where: { $0 is PublishedSuggestedPostMessageAttribute }) }) { + if actions.options.contains(.deleteGlobally), let message = messages.first(where: { message in message.attributes.contains(where: { $0 is PublishedSuggestedPostMessageAttribute }) }), let attribute = message.attributes.first(where: { $0 is PublishedSuggestedPostMessageAttribute }) as? PublishedSuggestedPostMessageAttribute { let commit = { [weak self] in guard let self else { return } //TODO:localize + let titleString: String + let textString: String + switch attribute.currency { + case .stars: + titleString = "Stars Will Be Lost" + textString = "You won't receive **Stars** for this post if you delete it now. The post must remain visible for at least **24 hours** after publication." + case .ton: + titleString = "TON Will Be Lost" + textString = "You won't receive **TON** for this post if you delete it now. The post must remain visible for at least **24 hours** after publication." + } self.present(standardTextAlertController( theme: AlertControllerTheme(presentationData: self.presentationData), - title: "Stars Will Be Lost", - text: "You won't receive **Stars** for this post if you delete it now. The post must remain visible for at least **24 hours** after publication.", + title: titleString, + text: textString, actions: [ TextAlertAction(type: .destructiveAction, title: "Delete Anyway", action: { [weak self] in guard let self else { diff --git a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift index 98531b6385..3777fa27a6 100644 --- a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift +++ b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift @@ -409,7 +409,7 @@ public final class ChatTextInputTextCustomEmojiAttribute: NSObject, Codable { case topic(id: Int64, info: EngineMessageHistoryThread.Info) case nameColors([UInt32]) case stars(tinted: Bool) - case ton + case ton(tinted: Bool) case animation(name: String) case verification }