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 { } else {
labelString = "+ \(formattedLabel)" 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 var itemDateColor = item.presentationData.theme.list.itemSecondaryTextColor
itemDate = stringForMediumCompactDate(timestamp: item.transaction.date, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat) 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), 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), 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, 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 action: { [weak self] _ in
guard let self, let item = self.item else { guard let self, let item = self.item else {
return return

View File

@ -1016,8 +1016,8 @@ extension StoreMessage {
attributes.append(SuggestedPostMessageAttribute(apiSuggestedPost: suggestedPost)) attributes.append(SuggestedPostMessageAttribute(apiSuggestedPost: suggestedPost))
} }
if (flags2 & (1 << 8)) != 0 { if (flags2 & (1 << 8)) != 0 || (flags2 & (1 << 9)) != 0 {
attributes.append(PublishedSuggestedPostMessageAttribute()) attributes.append(PublishedSuggestedPostMessageAttribute(currency: (flags2 & (1 << 8)) != 0 ? .stars : .ton))
} }
var storeFlags = StoreMessageFlags() var storeFlags = StoreMessageFlags()

View File

@ -93,16 +93,24 @@ extension SuggestedPostMessageAttribute {
} }
public final class PublishedSuggestedPostMessageAttribute: Equatable, MessageAttribute { 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) { public init(decoder: PostboxDecoder) {
self.currency = CurrencyAmount.Currency(rawValue: decoder.decodeInt32ForKey("c", orElse: 0)) ?? .stars
} }
public func encode(_ encoder: PostboxEncoder) { public func encode(_ encoder: PostboxEncoder) {
encoder.encodeInt32(self.currency.rawValue, forKey: "c")
} }
public static func == (lhs: PublishedSuggestedPostMessageAttribute, rhs: PublishedSuggestedPostMessageAttribute) -> Bool { public static func == (lhs: PublishedSuggestedPostMessageAttribute, rhs: PublishedSuggestedPostMessageAttribute) -> Bool {
if lhs.currency != rhs.currency {
return false
}
return true return true
} }
} }

View File

@ -618,7 +618,7 @@ private final class StarsContextImpl {
} }
var transactions = state.transactions var transactions = state.transactions
if addTransaction { 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)) 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 media = extendedMedia.flatMap({ $0.compactMap { textMediaAndExpirationTimerFromApiMedia($0, PeerId(0)).media } }) ?? []
let _ = subscriptionPeriod 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 flags: Flags
public let id: String public let id: String
public let count: StarsAmount public let count: StarsAmount
public let currency: CurrencyAmount.Currency
public let date: Int32 public let date: Int32
public let peer: Peer public let peer: Peer
public let title: String? public let title: String?
@ -813,6 +816,7 @@ public final class StarsContext {
flags: Flags, flags: Flags,
id: String, id: String,
count: StarsAmount, count: StarsAmount,
currency: CurrencyAmount.Currency,
date: Int32, date: Int32,
peer: Peer, peer: Peer,
title: String?, title: String?,
@ -835,6 +839,7 @@ public final class StarsContext {
self.flags = flags self.flags = flags
self.id = id self.id = id
self.count = count self.count = count
self.currency = currency
self.date = date self.date = date
self.peer = peer self.peer = peer
self.title = title self.title = title
@ -1074,7 +1079,7 @@ public final class StarsContext {
return peerId! return peerId!
} }
let ton: Bool public let ton: Bool
public var currentState: StarsContext.State? { public var currentState: StarsContext.State? {
var state: 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 return result
} }

View File

@ -1106,10 +1106,17 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
var range = NSRange(location: NSNotFound, length: 0) var range = NSRange(location: NSNotFound, length: 0)
range = (mutableString.string as NSString).range(of: "{amount}") range = (mutableString.string as NSString).range(of: "{amount}")
if range.location != NSNotFound { 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) let amountAttributedString = NSMutableAttributedString(string: "#\(totalAmount)", font: titleBoldFont, textColor: primaryTextColor)
if let range = amountAttributedString.string.range(of: "#") { 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)) amountAttributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: amountAttributedString.string))
} }
mutableString.replaceCharacters(in: range, with: amountAttributedString) mutableString.replaceCharacters(in: range, with: amountAttributedString)

View File

@ -112,6 +112,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
private var fetchDisposable: Disposable? private var fetchDisposable: Disposable?
private var setupTimestamp: Double? private var setupTimestamp: Double?
private var cachedTonImage: (UIImage, UIColor)?
required public init() { required public init() {
self.labelNode = TextNode() self.labelNode = TextNode()
self.labelNode.isUserInteractionEnabled = false self.labelNode.isUserInteractionEnabled = false
@ -339,6 +341,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
let currentIsExpanded = self.isExpanded let currentIsExpanded = self.isExpanded
let cachedTonImage = self.cachedTonImage
return { item, layoutConstants, _, _, _, _ in return { item, layoutConstants, _, _, _, _ in
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .always, forceFullCorners: false, forceAlignment: .center) 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 let cryptoAmount = cryptoAmount ?? 0
title = item.presentationData.strings.Notification_StarsGift_Title(Int32(cryptoAmount)) title = "$ \(formatTonAmountText(cryptoAmount, dateTimeFormat: item.presentationData.dateTimeFormat))"
text = incoming ? item.presentationData.strings.Notification_StarsGift_Subtitle : item.presentationData.strings.Notification_StarsGift_SubtitleYou(peerName).string 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, _, _): case let .prizeStars(count, _, channelId, _, _):
if count <= 1000 { if count <= 1000 {
months = 3 months = 3
@ -596,7 +600,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
} else { } else {
title = isStoryEntity ? uniqueGift.title : item.presentationData.strings.Notification_StarGift_Title(authorName).string 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 ribbonTitle = isStoryEntity ? "" : item.presentationData.strings.Notification_StarGift_Gift
buttonTitle = isStoryEntity ? "" : item.presentationData.strings.Notification_StarGift_View buttonTitle = isStoryEntity ? "" : item.presentationData.strings.Notification_StarGift_View
modelTitle = item.presentationData.strings.Notification_StarGift_Model 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 (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())) 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.animationNode.updateLayout(size: iconSize)
strongSelf.placeholderNode.frame = animationFrame strongSelf.placeholderNode.frame = animationFrame
strongSelf.cachedTonImage = updatedCachedTonImage
let _ = labelApply() let _ = labelApply()
let _ = titleApply() let _ = titleApply()
let _ = subtitleApply(TextNodeWithEntities.Arguments( let _ = subtitleApply(TextNodeWithEntities.Arguments(

View File

@ -487,8 +487,11 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
if tinted { if tinted {
self.updateTintColor() self.updateTintColor()
} }
case .ton: case let .ton(tinted):
self.updateTon() self.updateTon(tinted: tinted)
if tinted {
self.updateTintColor()
}
case let .animation(name): case let .animation(name):
self.updateLocalAnimation(name: name, attemptSynchronousLoad: attemptSynchronousLoad) self.updateLocalAnimation(name: name, attemptSynchronousLoad: attemptSynchronousLoad)
case .verification: case .verification:
@ -581,7 +584,7 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
} }
} else if let emoji = self.arguments?.emoji, let custom = emoji.custom { } else if let emoji = self.arguments?.emoji, let custom = emoji.custom {
switch custom { switch custom {
case .stars(true), .verification: case .stars(true), .ton(true), .verification:
customColor = self.dynamicColor customColor = self.dynamicColor
default: default:
break break
@ -687,8 +690,8 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
self.contents = tinted ? tintedStarImage?.cgImage : starImage?.cgImage self.contents = tinted ? tintedStarImage?.cgImage : starImage?.cgImage
} }
private func updateTon() { private func updateTon(tinted: Bool) {
self.contents = tonImage?.cgImage self.contents = tinted ? tintedTonImage?.cgImage : tonImage?.cgImage
} }
private func updateVerification() { private func updateVerification() {
@ -1053,6 +1056,16 @@ private let tonImage: UIImage? = {
})?.withRenderingMode(.alwaysTemplate) })?.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? = { private let verificationImage: UIImage? = {
if let backgroundImage = UIImage(bundleImageName: "Peer Info/VerifiedIconBackground"), let foregroundImage = UIImage(bundleImageName: "Peer Info/VerifiedIconForeground") { if let backgroundImage = UIImage(bundleImageName: "Peer Info/VerifiedIconBackground"), let foregroundImage = UIImage(bundleImageName: "Peer Info/VerifiedIconForeground") {
return generateImage(backgroundImage.size, contextGenerator: { size, context in 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 string = "*\(formatTonAmountText(revenueBalance, dateTimeFormat: presentationData.dateTimeFormat))"
let attributedString = NSMutableAttributedString(string: string, font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize), textColor: presentationData.theme.list.itemSecondaryTextColor) let attributedString = NSMutableAttributedString(string: string, font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize), textColor: presentationData.theme.list.itemSecondaryTextColor)
if let range = attributedString.string.range(of: "*") { 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)) 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: { 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) attributedString.append(starsAttributedString)
} }
if let range = attributedString.string.range(of: "#") { 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)) attributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: attributedString.string))
} }
if let range = attributedString.string.range(of: "*") { if let range = attributedString.string.range(of: "*") {

View File

@ -372,18 +372,36 @@ public final class StarsAvatarComponent: Component {
} }
public final class StarsLabelComponent: CombinedComponent { public final class StarsLabelComponent: CombinedComponent {
let theme: PresentationTheme
let currency: CurrencyAmount.Currency
let textColor: UIColor
let text: NSAttributedString let text: NSAttributedString
let subtext: NSAttributedString? let subtext: NSAttributedString?
public init( public init(
theme: PresentationTheme,
currency: CurrencyAmount.Currency,
textColor: UIColor,
text: NSAttributedString, text: NSAttributedString,
subtext: NSAttributedString? = nil subtext: NSAttributedString? = nil
) { ) {
self.currency = currency
self.theme = theme
self.textColor = textColor
self.text = text self.text = text
self.subtext = subtext self.subtext = subtext
} }
public static func ==(lhs: StarsLabelComponent, rhs: StarsLabelComponent) -> Bool { 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 { if lhs.text != rhs.text {
return false return false
} }
@ -406,7 +424,6 @@ public final class StarsLabelComponent: CombinedComponent {
availableSize: CGSize(width: 140.0, height: 40.0), availableSize: CGSize(width: 140.0, height: 40.0),
transition: context.transition transition: context.transition
) )
var subtext: _UpdatedChildComponent? = nil var subtext: _UpdatedChildComponent? = nil
if let sublabel = component.subtext { 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( let icon = icon.update(
component: BundleIconComponent( component: BundleIconComponent(
name: "Premium/Stars/StarMedium", name: component.currency == .ton ? "Ads/TonBig" : "Premium/Stars/StarMedium",
tintColor: nil tintColor: component.currency == .ton ? component.textColor : nil,
maxSize: iconSize
), ),
availableSize: iconSize, availableSize: iconSize,
transition: context.transition transition: context.transition

View File

@ -306,6 +306,7 @@ public final class StarsImageComponent: Component {
public enum Icon { public enum Icon {
case star case star
case ton
} }
public let context: AccountContext public let context: AccountContext
@ -865,7 +866,7 @@ public final class StarsImageComponent: Component {
animationNode.updateLayout(size: animationFrame.size) animationNode.updateLayout(size: animationFrame.size)
} }
if let _ = component.icon { if let icon = component.icon {
let smallIconView: UIImageView let smallIconView: UIImageView
let smallIconOutlineView: UIImageView let smallIconOutlineView: UIImageView
if let current = self.smallIconView, let currentOutline = self.smallIconOutlineView { if let current = self.smallIconView, let currentOutline = self.smallIconOutlineView {
@ -880,15 +881,27 @@ public final class StarsImageComponent: Component {
containerNode.view.addSubview(smallIconView) containerNode.view.addSubview(smallIconView)
self.smallIconView = smallIconView self.smallIconView = smallIconView
smallIconOutlineView.image = UIImage(bundleImageName: "Premium/Stars/TransactionStarOutline")?.withRenderingMode(.alwaysTemplate) switch icon {
smallIconView.image = UIImage(bundleImageName: "Premium/Stars/TransactionStar") 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 smallIconOutlineView.tintColor = component.backgroundColor
if let icon = smallIconView.image { if let iconImage = smallIconView.image {
let smallIconFrame = CGRect(origin: CGPoint(x: imageFrame.maxX - icon.size.width, y: imageFrame.maxY - icon.size.height), size: icon.size) let smallIconFrame = CGRect(origin: CGPoint(x: imageFrame.maxX - iconImage.size.width, y: imageFrame.maxY - iconImage.size.height), size: iconImage.size)
smallIconView.frame = smallIconFrame smallIconView.frame = smallIconFrame
switch icon {
case .star:
smallIconView.tintColor = nil
case .ton:
smallIconView.tintColor = component.theme.list.itemAccentColor
}
smallIconOutlineView.frame = smallIconFrame smallIconOutlineView.frame = smallIconFrame
} }
} else if let smallIconView = self.smallIconView, let smallIconOutlineView = self.smallIconOutlineView { } 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 statusText: String?
var statusIsDestructive = false var statusIsDestructive = false
let count: StarsAmount let count: CurrencyAmount
var countIsGeneric = false var countIsGeneric = false
var countOnTop = false var countOnTop = false
var transactionId: String? var transactionId: String?
@ -257,7 +257,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
titleText = strings.Stars_Transaction_Giveaway_Boost_Stars(Int32(stars)) titleText = strings.Stars_Transaction_Giveaway_Boost_Stars(Int32(stars))
descriptionText = "" descriptionText = ""
boostsText = strings.Stars_Transaction_Giveaway_Boost_Boosts(boosts) 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 date = boost.date
toPeer = state.peerMap[peerId] toPeer = state.peerMap[peerId]
giveawayMessageId = boost.giveawayMessageId 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) let usdValue = formatTonUsdValue(pricing.amount.value, divide: false, rate: usdRate, dateTimeFormat: environment.dateTimeFormat)
titleText = strings.Stars_Transaction_Subscription_Title titleText = strings.Stars_Transaction_Subscription_Title
descriptionText = strings.Stars_Transaction_Subscription_PerMonthUsd(usdValue).string descriptionText = strings.Stars_Transaction_Subscription_PerMonthUsd(usdValue).string
count = pricing.amount count = CurrencyAmount(amount: pricing.amount, currency: .stars)
countOnTop = true countOnTop = true
date = importer.date date = importer.date
toPeer = importer.peer.peer.flatMap(EnginePeer.init) toPeer = importer.peer.peer.flatMap(EnginePeer.init)
@ -288,7 +288,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
photo = subscription.photo photo = subscription.photo
descriptionText = "" descriptionText = ""
count = subscription.pricing.amount count = CurrencyAmount(amount: subscription.pricing.amount, currency: .stars)
date = subscription.untilDate date = subscription.untilDate
if let creationDate = (subscription.peer._asPeer() as? TelegramChannel)?.creationDate, creationDate > 0 { if let creationDate = (subscription.peer._asPeer() as? TelegramChannel)?.creationDate, creationDate > 0 {
additionalDate = creationDate additionalDate = creationDate
@ -376,7 +376,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
titleText = gift.title titleText = gift.title
descriptionText = "\(strings.Gift_Unique_Collectible) #\(presentationStringsFormattedNumber(gift.number, dateTimeFormat.groupingSeparator))" descriptionText = "\(strings.Gift_Unique_Collectible) #\(presentationStringsFormattedNumber(gift.number, dateTimeFormat.groupingSeparator))"
} }
count = transaction.count count = CurrencyAmount(amount: transaction.count, currency: transaction.currency)
transactionId = transaction.id transactionId = transaction.id
date = transaction.date date = transaction.date
if case let .peer(peer) = transaction.peer { if case let .peer(peer) = transaction.peer {
@ -395,7 +395,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
} else if let giveawayMessageIdValue = transaction.giveawayMessageId { } else if let giveawayMessageIdValue = transaction.giveawayMessageId {
titleText = strings.Stars_Transaction_Giveaway_Title titleText = strings.Stars_Transaction_Giveaway_Title
descriptionText = "" descriptionText = ""
count = transaction.count count = CurrencyAmount(amount: transaction.count, currency: transaction.currency)
transactionId = transaction.id transactionId = transaction.id
date = transaction.date date = transaction.date
giveawayMessageId = giveawayMessageIdValue giveawayMessageId = giveawayMessageIdValue
@ -406,7 +406,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
} else if let _ = transaction.subscriptionPeriod { } else if let _ = transaction.subscriptionPeriod {
titleText = strings.Stars_Transaction_SubscriptionFee titleText = strings.Stars_Transaction_SubscriptionFee
descriptionText = "" descriptionText = ""
count = transaction.count count = CurrencyAmount(amount: transaction.count, currency: transaction.currency)
transactionId = transaction.id transactionId = transaction.id
date = transaction.date date = transaction.date
if case let .peer(peer) = transaction.peer { if case let .peer(peer) = transaction.peer {
@ -417,7 +417,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
} else if transaction.flags.contains(.isGift) { } else if transaction.flags.contains(.isGift) {
titleText = strings.Stars_Gift_Received_Title titleText = strings.Stars_Gift_Received_Title
descriptionText = strings.Stars_Gift_Received_Text descriptionText = strings.Stars_Gift_Received_Text
count = transaction.count count = CurrencyAmount(amount: transaction.count, currency: transaction.currency)
countOnTop = true countOnTop = true
transactionId = transaction.id transactionId = transaction.id
date = transaction.date date = transaction.date
@ -446,7 +446,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
countOnTop = false countOnTop = false
descriptionText = "" descriptionText = ""
} }
count = transaction.count count = CurrencyAmount(amount: transaction.count, currency: transaction.currency)
transactionId = transaction.id transactionId = transaction.id
date = transaction.date date = transaction.date
transactionPeer = transaction.peer transactionPeer = transaction.peer
@ -457,7 +457,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
titleText = strings.Stars_Transaction_Reaction_Title titleText = strings.Stars_Transaction_Reaction_Title
descriptionText = "" descriptionText = ""
messageId = transaction.paidMessageId messageId = transaction.paidMessageId
count = transaction.count count = CurrencyAmount(amount: transaction.count, currency: transaction.currency)
transactionId = transaction.id transactionId = transaction.id
date = transaction.date date = transaction.date
if case let .peer(peer) = transaction.peer { if case let .peer(peer) = transaction.peer {
@ -545,7 +545,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
messageId = transaction.paidMessageId messageId = transaction.paidMessageId
count = transaction.count count = CurrencyAmount(amount: transaction.count, currency: transaction.currency)
transactionId = transaction.id transactionId = transaction.id
date = transaction.date date = transaction.date
if case let .peer(peer) = transaction.peer { if case let .peer(peer) = transaction.peer {
@ -564,7 +564,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
case let .receipt(receipt): case let .receipt(receipt):
titleText = receipt.invoiceMedia.title titleText = receipt.invoiceMedia.title
descriptionText = receipt.invoiceMedia.description 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 transactionId = receipt.transactionId
date = receipt.date date = receipt.date
if let peer = state.peerMap[receipt.botPaymentId] { if let peer = state.peerMap[receipt.botPaymentId] {
@ -581,7 +581,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
if case let .giftStars(_, _, countValue, _, _, _) = action.action { if case let .giftStars(_, _, countValue, _, _, _) = action.action {
titleText = incoming ? strings.Stars_Gift_Received_Title : strings.Stars_Gift_Sent_Title 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 { if !incoming {
countIsGeneric = true countIsGeneric = true
} }
@ -595,7 +595,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
} else if case let .prizeStars(countValue, _, boostPeerId, _, giveawayMessageIdValue) = action.action { } else if case let .prizeStars(countValue, _, boostPeerId, _, giveawayMessageIdValue) = action.action {
titleText = strings.Stars_Transaction_Giveaway_Title titleText = strings.Stars_Transaction_Giveaway_Title
count = StarsAmount(value: countValue, nanos: 0) count = CurrencyAmount(amount: StarsAmount(value: countValue, nanos: 0), currency: .stars)
countOnTop = true countOnTop = true
transactionId = nil transactionId = nil
giveawayMessageId = giveawayMessageIdValue giveawayMessageId = giveawayMessageIdValue
@ -648,8 +648,14 @@ private final class StarsTransactionSheetContent: CombinedComponent {
headerTextColor = theme.actionSheet.primaryTextColor headerTextColor = theme.actionSheet.primaryTextColor
} }
let absCount = StarsAmount(value: abs(count.value), nanos: abs(count.nanos)) let absCount = StarsAmount(value: abs(count.amount.value), nanos: abs(count.amount.nanos))
let formattedAmount = formatStarsAmountText(absCount, dateTimeFormat: dateTimeFormat) let formattedAmount: String
switch count.currency {
case .stars:
formattedAmount = formatStarsAmountText(absCount, dateTimeFormat: dateTimeFormat)
case .ton:
formattedAmount = formatTonAmountText(absCount.value, dateTimeFormat: dateTimeFormat)
}
let countColor: UIColor let countColor: UIColor
var countFont: UIFont = isSubscription || isSubscriber ? Font.regular(17.0) : Font.semibold(17.0) var countFont: UIFont = isSubscription || isSubscriber ? Font.regular(17.0) : Font.semibold(17.0)
var countBackgroundColor: UIColor? var countBackgroundColor: UIColor?
@ -664,7 +670,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
} else if countIsGeneric { } else if countIsGeneric {
amountText = "\(formattedAmount)" amountText = "\(formattedAmount)"
countColor = theme.list.itemPrimaryTextColor countColor = theme.list.itemPrimaryTextColor
} else if count < StarsAmount.zero { } else if count.amount < StarsAmount.zero {
amountText = "- \(formattedAmount)" amountText = "- \(formattedAmount)"
if case .unique = giftAnimationSubject { if case .unique = giftAnimationSubject {
countColor = .white countColor = .white
@ -706,9 +712,9 @@ private final class StarsTransactionSheetContent: CombinedComponent {
imageSubject = .gift(premiumGiftMonths) imageSubject = .gift(premiumGiftMonths)
} else if isGift { } else if isGift {
var value: Int32 = 3 var value: Int32 = 3
if count.value <= 1000 { if count.amount.value <= 1000 {
value = 3 value = 3
} else if count.value < 2500 { } else if count.amount.value < 2500 {
value = 6 value = 6
} else { } else {
value = 12 value = 12
@ -726,9 +732,9 @@ private final class StarsTransactionSheetContent: CombinedComponent {
imageSubject = .none imageSubject = .none
} }
if isSubscription || isSubscriber || isSubscriptionFee || giveawayMessageId != nil { if isSubscription || isSubscriber || isSubscriptionFee || giveawayMessageId != nil {
imageIcon = .star imageIcon = count.currency == .ton ? .ton : .star
} else { } else {
imageIcon = nil imageIcon = count.currency == .ton ? .ton : nil
} }
if isSubscription && "".isEmpty { if isSubscription && "".isEmpty {
@ -811,10 +817,26 @@ private final class StarsTransactionSheetContent: CombinedComponent {
transition: .immediate 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( let amountStar = amountStar.update(
component: BundleIconComponent( component: BundleIconComponent(
name: boostsText != nil ? "Premium/BoostButtonIcon" : "Premium/Stars/StarMedium", name: amountStarIconName,
tintColor: nil tintColor: amountStarTintColor,
maxSize: amountStarMaxSize
), ),
availableSize: context.availableSize, availableSize: context.availableSize,
transition: .immediate transition: .immediate
@ -836,7 +858,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
)) ))
} else if case .unique = giftAnimationSubject { } else if case .unique = giftAnimationSubject {
let reason: String 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) { if transaction.flags.contains(.isStarGiftResale) {
reason = strings.Stars_Transaction_GiftPurchase reason = strings.Stars_Transaction_GiftPurchase
} else { } else {
@ -892,7 +914,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
} else if isSubscriber { } else if isSubscriber {
title = strings.Stars_Transaction_Subscription_Subscriber title = strings.Stars_Transaction_Subscription_Subscriber
} else { } 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> let toComponent: AnyComponent<Empty>
@ -997,7 +1019,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
id: "prize", id: "prize",
title: strings.Stars_Transaction_Giveaway_Prize, title: strings.Stars_Transaction_Giveaway_Prize,
component: AnyComponent( 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 amountLabelOffsetY = 2.0
amountStarOffsetY = 5.0 amountStarOffsetY = 5.0
} }
amountStarOffsetY += amountOffset.y
context.add(amount context.add(amount
.position(CGPoint(x: amountLabelOriginX, y: amountOrigin + amount.size.height / 2.0 + amountLabelOffsetY)) .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 theme: PresentationTheme
let strings: PresentationStrings let strings: PresentationStrings
let dateTimeFormat: PresentationDateTimeFormat let dateTimeFormat: PresentationDateTimeFormat
let currency: CurrencyAmount.Currency
let count: StarsAmount let count: StarsAmount
let rate: Double? let rate: Double?
let actionTitle: String let actionTitle: String
@ -34,6 +35,7 @@ final class StarsBalanceComponent: Component {
theme: PresentationTheme, theme: PresentationTheme,
strings: PresentationStrings, strings: PresentationStrings,
dateTimeFormat: PresentationDateTimeFormat, dateTimeFormat: PresentationDateTimeFormat,
currency: CurrencyAmount.Currency,
count: StarsAmount, count: StarsAmount,
rate: Double?, rate: Double?,
actionTitle: String, actionTitle: String,
@ -51,6 +53,7 @@ final class StarsBalanceComponent: Component {
self.theme = theme self.theme = theme
self.strings = strings self.strings = strings
self.dateTimeFormat = dateTimeFormat self.dateTimeFormat = dateTimeFormat
self.currency = currency
self.count = count self.count = count
self.rate = rate self.rate = rate
self.actionTitle = actionTitle self.actionTitle = actionTitle
@ -164,7 +167,13 @@ final class StarsBalanceComponent: Component {
let sideInset: CGFloat = 16.0 let sideInset: CGFloat = 16.0
var contentHeight: CGFloat = sideInset 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 let labelFont: UIFont
if formattedLabel.contains(component.dateTimeFormat.decimalSeparator) { if formattedLabel.contains(component.dateTimeFormat.decimalSeparator) {
labelFont = Font.with(size: 48.0, design: .round, weight: .semibold) labelFont = Font.with(size: 48.0, design: .round, weight: .semibold)

View File

@ -579,6 +579,7 @@ final class StarsStatisticsScreenComponent: Component {
theme: environment.theme, theme: environment.theme,
strings: strings, strings: strings,
dateTimeFormat: environment.dateTimeFormat, dateTimeFormat: environment.dateTimeFormat,
currency: .stars,
count: self.starsState?.balances.availableBalance ?? StarsAmount.zero, count: self.starsState?.balances.availableBalance ?? StarsAmount.zero,
rate: self.starsState?.usdRate ?? 0, rate: self.starsState?.usdRate ?? 0,
actionTitle: strings.Stars_Intro_BuyShort, actionTitle: strings.Stars_Intro_BuyShort,
@ -622,6 +623,7 @@ final class StarsStatisticsScreenComponent: Component {
theme: environment.theme, theme: environment.theme,
strings: strings, strings: strings,
dateTimeFormat: environment.dateTimeFormat, dateTimeFormat: environment.dateTimeFormat,
currency: .stars,
count: self.starsState?.balances.availableBalance ?? StarsAmount.zero, count: self.starsState?.balances.availableBalance ?? StarsAmount.zero,
rate: self.starsState?.usdRate ?? 0, rate: self.starsState?.usdRate ?? 0,
actionTitle: strings.Stars_BotRevenue_Withdraw_WithdrawShort, actionTitle: strings.Stars_BotRevenue_Withdraw_WithdrawShort,

View File

@ -409,7 +409,13 @@ final class StarsTransactionsListPanelComponent: Component {
} }
let itemLabel: NSAttributedString 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 smallLabelFont = Font.with(size: floor(fontBaseDisplaySize / 17.0 * 13.0))
let labelFont = Font.medium(fontBaseDisplaySize) 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), 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), 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, 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 action: { [weak self] _ in
guard let self, let component = self.component else { guard let self, let component = self.component else {
return return

View File

@ -522,12 +522,23 @@ final class StarsTransactionsScreenComponent: Component {
} }
starTransition.setBounds(view: starView, bounds: starFrame) 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( let titleSize = self.titleView.update(
transition: .immediate, transition: .immediate,
component: AnyComponent( component: AnyComponent(
MultilineTextComponent( 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, horizontalAlignment: .center,
truncationType: .end, truncationType: .end,
maximumNumberOfLines: 1 maximumNumberOfLines: 1
@ -557,7 +568,12 @@ final class StarsTransactionsScreenComponent: Component {
containerSize: CGSize(width: 120.0, height: 100.0) 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 smallLabelFont = Font.regular(11.0)
let labelFont = Font.semibold(14.0) let labelFont = Font.semibold(14.0)
let balanceText = tonAmountAttributedString(formattedBalance, integralFont: labelFont, fractionalFont: smallLabelFont, color: environment.theme.actionSheet.primaryTextColor, decimalSeparator: environment.dateTimeFormat.decimalSeparator) let 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( let topBalanceIconSize = self.topBalanceIconView.update(
transition: .immediate, 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: {}, environment: {},
containerSize: availableSize containerSize: availableSize
) )
@ -598,7 +618,10 @@ final class StarsTransactionsScreenComponent: Component {
starTransition.setFrame(view: topBalanceValueView, frame: topBalanceValueFrame) 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 let topBalanceIconView = self.topBalanceIconView.view {
if topBalanceIconView.superview == nil { if topBalanceIconView.superview == nil {
topBalanceIconView.alpha = 0.0 topBalanceIconView.alpha = 0.0
@ -613,7 +636,7 @@ final class StarsTransactionsScreenComponent: Component {
transition: .immediate, transition: .immediate,
component: AnyComponent( component: AnyComponent(
BalancedTextComponent( 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, horizontalAlignment: .center,
maximumNumberOfLines: 0, maximumNumberOfLines: 0,
lineSpacing: 0.2 lineSpacing: 0.2
@ -648,6 +671,7 @@ final class StarsTransactionsScreenComponent: Component {
theme: environment.theme, theme: environment.theme,
strings: environment.strings, strings: environment.strings,
dateTimeFormat: environment.dateTimeFormat, dateTimeFormat: environment.dateTimeFormat,
currency: component.starsContext.ton ? .ton : .stars,
count: self.starsState?.balance ?? StarsAmount.zero, count: self.starsState?.balance ?? StarsAmount.zero,
rate: nil, rate: nil,
actionTitle: withdrawAvailable ? environment.strings.Stars_Intro_BuyShort : environment.strings.Stars_Intro_Buy, actionTitle: withdrawAvailable ? environment.strings.Stars_Intro_BuyShort : environment.strings.Stars_Intro_Buy,
@ -705,7 +729,7 @@ final class StarsTransactionsScreenComponent: Component {
contentHeight += 34.0 contentHeight += 34.0
var canJoinRefProgram = false 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 { if let value = value as? Double {
canJoinRefProgram = value != 0.0 canJoinRefProgram = value != 0.0
} else if let value = value as? Bool { } 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))) 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 { } 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) 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( subscriptionsItems.append(AnyComponentWithIdentity(

View File

@ -91,7 +91,7 @@ private final class SheetContent: CombinedComponent {
theme: environment.theme, theme: environment.theme,
strings: environment.strings, strings: environment.strings,
currency: state.currency, currency: state.currency,
balance: state.balance, balance: state.currency == .stars ? state.starsBalance : state.tonBalance,
alignment: .right alignment: .right
), ),
availableSize: CGSize(width: 200.0, height: 200.0), availableSize: CGSize(width: 200.0, height: 200.0),
@ -169,7 +169,7 @@ private final class SheetContent: CombinedComponent {
amountPlaceholder = environment.strings.Stars_Withdraw_AmountPlaceholder amountPlaceholder = environment.strings.Stars_Withdraw_AmountPlaceholder
minAmount = withdrawConfiguration.minWithdrawAmount.flatMap { StarsAmount(value: $0, nanos: 0) } minAmount = withdrawConfiguration.minWithdrawAmount.flatMap { StarsAmount(value: $0, nanos: 0) }
maxAmount = state.balance maxAmount = state.starsBalance
case .paidMedia: case .paidMedia:
titleString = environment.strings.Stars_PaidContent_Title titleString = environment.strings.Stars_PaidContent_Title
amountTitle = environment.strings.Stars_PaidContent_AmountTitle amountTitle = environment.strings.Stars_PaidContent_AmountTitle
@ -237,9 +237,9 @@ private final class SheetContent: CombinedComponent {
let balance: StarsAmount? let balance: StarsAmount?
if case .accountWithdraw = component.mode { if case .accountWithdraw = component.mode {
balance = state.balance balance = state.starsBalance
} else if case .reaction = component.mode { } else if case .reaction = component.mode {
balance = state.balance balance = state.starsBalance
} else if case let .withdraw(starsState, _) = component.mode { } else if case let .withdraw(starsState, _) = component.mode {
balance = starsState.balances.availableBalance balance = starsState.balances.availableBalance
} else { } else {
@ -329,10 +329,16 @@ private final class SheetContent: CombinedComponent {
guard let state else { guard let state else {
return return
} }
let currency: CurrencyAmount.Currency
if id == AnyHashable(0) { if id == AnyHashable(0) {
state.currency = .stars currency = .stars
} else { } else {
state.currency = .ton currency = .ton
}
if state.currency != currency {
state.currency = currency
state.amount = nil
} }
state.updated(transition: .spring(duration: 0.4)) state.updated(transition: .spring(duration: 0.4))
} }
@ -485,6 +491,7 @@ private final class SheetContent: CombinedComponent {
placeholderText: amountPlaceholder, placeholderText: amountPlaceholder,
labelText: amountLabel, labelText: amountLabel,
currency: state.currency, currency: state.currency,
dateTimeFormat: presentationData.dateTimeFormat,
amountUpdated: { [weak state] amount in amountUpdated: { [weak state] amount in
state?.amount = amount.flatMap { StarsAmount(value: $0, nanos: 0) } state?.amount = amount.flatMap { StarsAmount(value: $0, nanos: 0) }
state?.updated() state?.updated()
@ -632,13 +639,16 @@ private final class SheetContent: CombinedComponent {
case .sender: case .sender:
if let amount = state.amount { if let amount = state.amount {
let currencySymbol: String let currencySymbol: String
let currencyAmount: String
switch state.currency { switch state.currency {
case .stars: case .stars:
currencySymbol = "#" currencySymbol = "#"
currencyAmount = presentationStringsFormattedNumber(amount, environment.dateTimeFormat.groupingSeparator)
case .ton: case .ton:
currencySymbol = "$" currencySymbol = "$"
currencyAmount = formatTonAmountText(amount.value, dateTimeFormat: environment.dateTimeFormat)
} }
buttonString = "Offer \(currencySymbol) \(presentationStringsFormattedNumber(amount, environment.dateTimeFormat.groupingSeparator))" buttonString = "Offer \(currencySymbol) \(currencyAmount)"
} else { } else {
buttonString = "Offer for Free" buttonString = "Offer for Free"
} }
@ -756,8 +766,10 @@ private final class SheetContent: CombinedComponent {
fileprivate var currency: CurrencyAmount.Currency = .stars fileprivate var currency: CurrencyAmount.Currency = .stars
fileprivate var timestamp: Int32? fileprivate var timestamp: Int32?
fileprivate var balance: StarsAmount? fileprivate var starsBalance: StarsAmount?
private var stateDisposable: Disposable? private var starsStateDisposable: Disposable?
fileprivate var tonBalance: StarsAmount?
private var tonStateDisposable: Disposable?
var cachedCloseImage: (UIImage, PresentationTheme)? var cachedCloseImage: (UIImage, PresentationTheme)?
var cachedStarImage: (UIImage, PresentationTheme)? var cachedStarImage: (UIImage, PresentationTheme)?
@ -809,14 +821,25 @@ private final class SheetContent: CombinedComponent {
default: default:
break break
} }
if needsBalance, let starsContext = component.context.starsContext { if needsBalance {
self.stateDisposable = (starsContext.state if let starsContext = component.context.starsContext {
|> deliverOnMainQueue).startStrict(next: { [weak self] state in self.starsStateDisposable = (starsContext.state
if let self, let balance = state?.balance { |> deliverOnMainQueue).startStrict(next: { [weak self] state in
self.balance = balance if let self, let balance = state?.balance {
self.updated() 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 { if case let .starGiftResell(giftToMatch, update, _) = self.mode {
@ -851,7 +874,8 @@ private final class SheetContent: CombinedComponent {
} }
deinit { 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 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 { private final class AmountFieldComponent: Component {
typealias EnvironmentType = Empty typealias EnvironmentType = Empty
@ -1048,6 +1519,7 @@ private final class AmountFieldComponent: Component {
let placeholderText: String let placeholderText: String
let labelText: String? let labelText: String?
let currency: CurrencyAmount.Currency let currency: CurrencyAmount.Currency
let dateTimeFormat: PresentationDateTimeFormat
let amountUpdated: (Int64?) -> Void let amountUpdated: (Int64?) -> Void
let tag: AnyObject? let tag: AnyObject?
@ -1062,6 +1534,7 @@ private final class AmountFieldComponent: Component {
placeholderText: String, placeholderText: String,
labelText: String?, labelText: String?,
currency: CurrencyAmount.Currency, currency: CurrencyAmount.Currency,
dateTimeFormat: PresentationDateTimeFormat,
amountUpdated: @escaping (Int64?) -> Void, amountUpdated: @escaping (Int64?) -> Void,
tag: AnyObject? = nil tag: AnyObject? = nil
) { ) {
@ -1075,6 +1548,7 @@ private final class AmountFieldComponent: Component {
self.placeholderText = placeholderText self.placeholderText = placeholderText
self.labelText = labelText self.labelText = labelText
self.currency = currency self.currency = currency
self.dateTimeFormat = dateTimeFormat
self.amountUpdated = amountUpdated self.amountUpdated = amountUpdated
self.tag = tag self.tag = tag
} }
@ -1127,10 +1601,13 @@ private final class AmountFieldComponent: Component {
private let placeholderView: ComponentView<Empty> private let placeholderView: ComponentView<Empty>
private let icon = ComponentView<Empty>() private let icon = ComponentView<Empty>()
private let textField: TextFieldNodeView private let textField: TextFieldNodeView
private var starsFormatter: AmountFieldStarsFormatter?
private var tonFormatter: AmountFieldStarsFormatter?
private let labelView: ComponentView<Empty> private let labelView: ComponentView<Empty>
private var component: AmountFieldComponent? private var component: AmountFieldComponent?
private weak var state: EmptyComponentState? private weak var state: EmptyComponentState?
private var isUpdating: Bool = false
override init(frame: CGRect) { override init(frame: CGRect) {
self.placeholderView = ComponentView<Empty>() self.placeholderView = ComponentView<Empty>()
@ -1138,9 +1615,6 @@ private final class AmountFieldComponent: Component {
self.labelView = ComponentView<Empty>() self.labelView = ComponentView<Empty>()
super.init(frame: frame) super.init(frame: frame)
self.textField.delegate = self
self.textField.addTarget(self, action: #selector(self.textChanged(_:)), for: .editingChanged)
self.addSubview(self.textField) self.addSubview(self.textField)
} }
@ -1148,56 +1622,6 @@ private final class AmountFieldComponent: Component {
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented") 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() { func activateInput() {
self.textField.becomeFirstResponder() self.textField.becomeFirstResponder()
@ -1217,18 +1641,109 @@ private final class AmountFieldComponent: Component {
} }
func update(component: AmountFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize { 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 self.textField.textColor = component.textColor
if let value = component.value { if self.component?.currency != component.currency {
self.textField.text = "\(value)" if let value = component.value {
} else { var text = ""
self.textField.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.font = Font.regular(17.0)
self.textField.keyboardType = .numberPad
self.textField.returnKeyType = .done self.textField.returnKeyType = .done
self.textField.autocorrectionType = .no self.textField.autocorrectionType = .no
self.textField.autocapitalizationType = .none 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.component = component
self.state = state self.state = state
@ -1460,7 +1975,13 @@ private final class BalanceComponent: CombinedComponent {
let balanceText: String let balanceText: String
if let value = context.component.balance { 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 { } else {
balanceText = "..." balanceText = "..."
} }

View File

@ -1817,16 +1817,26 @@ extension ChatControllerImpl {
return 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 let commit = { [weak self] in
guard let self else { guard let self else {
return return
} }
//TODO:localize //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( self.present(standardTextAlertController(
theme: AlertControllerTheme(presentationData: self.presentationData), theme: AlertControllerTheme(presentationData: self.presentationData),
title: "Stars Will Be Lost", title: titleString,
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.", text: textString,
actions: [ actions: [
TextAlertAction(type: .destructiveAction, title: "Delete Anyway", action: { [weak self] in TextAlertAction(type: .destructiveAction, title: "Delete Anyway", action: { [weak self] in
guard let self else { guard let self else {

View File

@ -409,7 +409,7 @@ public final class ChatTextInputTextCustomEmojiAttribute: NSObject, Codable {
case topic(id: Int64, info: EngineMessageHistoryThread.Info) case topic(id: Int64, info: EngineMessageHistoryThread.Info)
case nameColors([UInt32]) case nameColors([UInt32])
case stars(tinted: Bool) case stars(tinted: Bool)
case ton case ton(tinted: Bool)
case animation(name: String) case animation(name: String)
case verification case verification
} }