mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-07-16 08:19:23 +00:00
Stars
This commit is contained in:
parent
51a16c7110
commit
b56a0143f3
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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?
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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: "*") {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
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 {
|
||||
|
@ -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))
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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,15 +821,26 @@ private final class SheetContent: CombinedComponent {
|
||||
default:
|
||||
break
|
||||
}
|
||||
if needsBalance, let starsContext = component.context.starsContext {
|
||||
self.stateDisposable = (starsContext.state
|
||||
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.balance = 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 {
|
||||
if update {
|
||||
@ -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 self.component?.currency != component.currency {
|
||||
if let value = component.value {
|
||||
self.textField.text = "\(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 {
|
||||
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 = "..."
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user