This commit is contained in:
Isaac 2025-06-24 13:02:37 +02:00
parent 51a16c7110
commit b56a0143f3
19 changed files with 851 additions and 148 deletions

View File

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

View File

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

View File

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

View File

@ -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))
@ -724,7 +724,9 @@ 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)
let amount = CurrencyAmount(apiAmount: stars)
self.init(flags: flags, id: id, count: amount.amount, currency: amount.currency, date: date, peer: parsedPeer, title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), transactionDate: transactionDate, transactionUrl: transactionUrl, paidMessageId: paidMessageId, giveawayMessageId: giveawayMessageId, media: media, subscriptionPeriod: subscriptionPeriod, starGift: starGift.flatMap { StarGift(apiStarGift: $0) }, floodskipNumber: floodskipNumber, starrefCommissionPermille: starrefCommissionPermille, starrefPeerId: starrefPeer?.peerId, starrefAmount: starrefAmount.flatMap(StarsAmount.init(apiAmount:)), paidMessageCount: paidMessageCount, premiumGiftMonths: premiumGiftMonths)
}
}
}
@ -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?

View File

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

View File

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

View File

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

View File

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

View File

@ -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: "*") {

View File

@ -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
}
@ -407,7 +425,6 @@ public final class StarsLabelComponent: CombinedComponent {
transition: context.transition
)
var subtext: _UpdatedChildComponent? = nil
if let sublabel = component.subtext {
subtext = subLabel.update(
@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -523,11 +523,22 @@ 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(

View File

@ -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[..<dot])
if wholeSlice.isEmpty {
wholeSlice = "0"
}
let fractionSlice = text[text.index(after: dot)...]
// Make the fractional string exactly 9 characters long
var fractionStr = String(fractionSlice)
if fractionStr.count > 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<Empty>
private let icon = ComponentView<Empty>()
private let textField: TextFieldNodeView
private var starsFormatter: AmountFieldStarsFormatter?
private var tonFormatter: AmountFieldStarsFormatter?
private let labelView: ComponentView<Empty>
private var component: AmountFieldComponent?
private weak var state: EmptyComponentState?
private var isUpdating: Bool = false
override init(frame: CGRect) {
self.placeholderView = ComponentView<Empty>()
@ -1139,9 +1616,6 @@ private final class AmountFieldComponent: Component {
super.init(frame: frame)
self.textField.delegate = self
self.textField.addTarget(self, action: #selector(self.textChanged(_:)), for: .editingChanged)
self.addSubview(self.textField)
}
@ -1149,56 +1623,6 @@ private final class AmountFieldComponent: Component {
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,19 +1641,110 @@ private final class AmountFieldComponent: Component {
}
func update(component: AmountFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, 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 = "..."
}

View File

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

View File

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