# Conflicts:
#	submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift
#	submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift
This commit is contained in:
Isaac 2025-08-11 16:55:18 +02:00
commit 403a163692
23 changed files with 1552 additions and 117 deletions

View file

@ -14892,3 +14892,22 @@ Sorry for the inconvenience.";
"PeerInfo.Gifts.Upgradable" = "Upgradable";
"Gift.Upgrade.GiftTitle" = "Make Unique";
"Gift.Upgrade.GiftDescription" = "Let %@ turn this gift into a unique collectible.";
"Gift.Upgrade.Unique.GiftDescription" = "%@ will get a unique number, model, backdrop, and symbol for the gift.";
"Gift.Upgrade.Transferable.GiftDescription" = "%@ will be able to send the gift to anyone Telegram.";
"Gift.Upgrade.Tradable.GiftDescription" = "%@ will be able to auction the gift on third-party NFT marketplaces.";
"Notification.StarGift.Subtitle.Upgrade.Prepaid" = "%@ can now turn this gift to a unique collectible";
"Notification.StarsGift.SentUpgradeYou" = "You sent an upgrade worth %1$@ for your gift to %2$@";
"Notification.StarsGift.SentUpgradeYouOther" = "You sent an upgrade worth %1$@ for the gift %2$@ received from %3$@";
"Notification.StarsGift.SentUpgradeYouUnknown" = "You sent an upgrade worth %1$@ for the gift to %2$@";
"Notification.StarsGift.SentUpgrade" = "%1$@ sent an upgrade worth %2$@ for your gift";
"Notification.StarsGift.SentUpgradeOther" = "%1$@ sent an upgrade worth %2$@ for the gift you received from %3$@";
"Notification.StarsGift.UpgradePrepaidYou" = "You unpacked the gift that %@ helped upgrade";
"Notification.StarsGift.UpgradePrepaid" = "%@ unpacked the gift that you helped upgrade";
"Gift.View.Unknown.Title" = "Gift";

View file

@ -393,7 +393,7 @@ public final class AuthorizationSequencePaymentScreen: ViewControllerComponentCo
loadServerCountryCodes(accountManager: sharedContext.accountManager, engine: engine, completion: { [weak self] in
if let strongSelf = self {
strongSelf.requestLayout(forceUpdate: true, transition: .immediate)
strongSelf.requestLayout(forceUpdate: true, transition: ContainedViewLayoutTransition.immediate)
}
})

View file

@ -64,6 +64,7 @@ public final class SheetComponent<ChildEnvironmentType: Sendable & Equatable>: C
public let followContentSizeChanges: Bool
public let clipsContent: Bool
public let isScrollEnabled: Bool
public let hasDimView: Bool
public let externalState: ExternalState?
public let animateOut: ActionSlot<Action<()>>
public let onPan: () -> Void
@ -75,6 +76,7 @@ public final class SheetComponent<ChildEnvironmentType: Sendable & Equatable>: C
followContentSizeChanges: Bool = false,
clipsContent: Bool = false,
isScrollEnabled: Bool = true,
hasDimView: Bool = true,
externalState: ExternalState? = nil,
animateOut: ActionSlot<Action<()>>,
onPan: @escaping () -> Void = {},
@ -85,6 +87,7 @@ public final class SheetComponent<ChildEnvironmentType: Sendable & Equatable>: C
self.followContentSizeChanges = followContentSizeChanges
self.clipsContent = clipsContent
self.isScrollEnabled = isScrollEnabled
self.hasDimView = hasDimView
self.externalState = externalState
self.animateOut = animateOut
self.onPan = onPan
@ -101,6 +104,12 @@ public final class SheetComponent<ChildEnvironmentType: Sendable & Equatable>: C
if lhs.followContentSizeChanges != rhs.followContentSizeChanges {
return false
}
if lhs.isScrollEnabled != rhs.isScrollEnabled {
return false
}
if lhs.hasDimView != rhs.hasDimView {
return false
}
if lhs.animateOut != rhs.animateOut {
return false
}
@ -414,6 +423,10 @@ public final class SheetComponent<ChildEnvironmentType: Sendable & Equatable>: C
self.currentAvailableSize = availableSize
if !component.hasDimView {
self.dimView.backgroundColor = .clear
}
if environment[SheetComponentEnvironment.self].value.isDisplaying, !self.previousIsDisplaying, let _ = transition.userData(ViewControllerComponentContainer.AnimateInTransition.self) {
self.animateIn()
} else if !environment[SheetComponentEnvironment.self].value.isDisplaying, self.previousIsDisplaying, let _ = transition.userData(ViewControllerComponentContainer.AnimateOutTransition.self) {

View file

@ -134,9 +134,13 @@ open class ViewControllerComponentContainer: ViewController {
}
public final class AnimateInTransition {
public init() {
}
}
public final class AnimateOutTransition {
public init() {
}
}
public final class Node: ViewControllerTracingNode {
@ -356,7 +360,7 @@ open class ViewControllerComponentContainer: ViewController {
}
if let layout = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout, transition: .immediate)
strongSelf.containerLayoutUpdated(layout, transition: ContainedViewLayoutTransition.immediate)
}
}
}).strict()
@ -412,6 +416,14 @@ open class ViewControllerComponentContainer: ViewController {
self.forceNextUpdate = false
}
public func requestLayout(forceUpdate: Bool, transition: ComponentTransition) {
self.forceNextUpdate = forceUpdate
if self.isViewLoaded, let validLayout = self.validLayout {
self.containerLayoutUpdated(validLayout, transition: transition)
}
self.forceNextUpdate = false
}
override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
@ -421,6 +433,15 @@ open class ViewControllerComponentContainer: ViewController {
self.node.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(transition))
}
public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ComponentTransition) {
super.containerLayoutUpdated(layout, transition: transition.containedViewLayoutTransition)
let navigationHeight = self.navigationLayout(layout: layout).navigationFrame.maxY
self.validLayout = layout
self.node.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: transition)
}
public func updateComponent(component: AnyComponent<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) {
self.component = component
self.node.updateComponent(component: component, transition: transition)

View file

@ -156,7 +156,8 @@ final class VideoAdComponent: Component {
animationCache: component.context.animationCache,
animationRenderer: component.context.animationRenderer,
placeholderColor: UIColor.white.withAlphaComponent(0.2),
text: .plain(attributedText)
text: .plain(attributedText),
maximumNumberOfLines: 0
)
),
environment: {},

View file

@ -177,7 +177,7 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe
return TelegramMediaAction(action: .paymentRefunded(peerId: peer.peerId, currency: currency, totalAmount: totalAmount, payload: payload?.makeData(), transactionId: transactionId))
case let .messageActionPrizeStars(flags, stars, transactionId, boostPeer, giveawayMsgId):
return TelegramMediaAction(action: .prizeStars(amount: stars, isUnclaimed: (flags & (1 << 2)) != 0, boostPeerId: boostPeer.peerId, transactionId: transactionId, giveawayMessageId: MessageId(peerId: boostPeer.peerId, namespace: Namespaces.Message.Cloud, id: giveawayMsgId)))
case let .messageActionStarGift(flags, apiGift, message, convertStars, upgradeMessageId, upgradeStars, fromId, peer, savedId, _):
case let .messageActionStarGift(flags, apiGift, message, convertStars, upgradeMessageId, upgradeStars, fromId, peer, savedId, prepaidUpgradeHash):
let text: String?
let entities: [MessageTextEntity]?
switch message {
@ -191,7 +191,7 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe
guard let gift = StarGift(apiStarGift: apiGift) else {
return nil
}
return TelegramMediaAction(action: .starGift(gift: gift, convertStars: convertStars, text: text, entities: entities, nameHidden: (flags & (1 << 0)) != 0, savedToProfile: (flags & (1 << 2)) != 0, converted: (flags & (1 << 3)) != 0, upgraded: (flags & (1 << 5)) != 0, canUpgrade: (flags & (1 << 10)) != 0, upgradeStars: upgradeStars, isRefunded: (flags & (1 << 9)) != 0, isPrepaidUpgrade: (flags & (1 << 13)) != 0, upgradeMessageId: upgradeMessageId, peerId: peer?.peerId, senderId: fromId?.peerId, savedId: savedId))
return TelegramMediaAction(action: .starGift(gift: gift, convertStars: convertStars, text: text, entities: entities, nameHidden: (flags & (1 << 0)) != 0, savedToProfile: (flags & (1 << 2)) != 0, converted: (flags & (1 << 3)) != 0, upgraded: (flags & (1 << 5)) != 0, canUpgrade: (flags & (1 << 10)) != 0, upgradeStars: upgradeStars, isRefunded: (flags & (1 << 9)) != 0, isPrepaidUpgrade: (flags & (1 << 13)) != 0, upgradeMessageId: upgradeMessageId, peerId: peer?.peerId, senderId: fromId?.peerId, savedId: savedId, prepaidUpgradeHash: prepaidUpgradeHash))
case let .messageActionStarGiftUnique(flags, apiGift, canExportAt, transferStars, fromId, peer, savedId, resaleAmount, canTransferDate, canResaleDate):
guard let gift = StarGift(apiStarGift: apiGift) else {
return nil

View file

@ -343,6 +343,7 @@ func managedUniqueStarGifts(accountPeerId: PeerId, postbox: Postbox, network: Ne
if let file = files[fileId], let patternFile = files[patternFileId], let numberString = slugComponents.last, let number = Int32(numberString) {
let gift = StarGift.UniqueGift(
id: id,
giftId: 0,
title: title,
number: number,
slug: slug,
@ -356,7 +357,9 @@ func managedUniqueStarGifts(accountPeerId: PeerId, postbox: Postbox, network: Ne
giftAddress: nil,
resellAmounts: nil,
resellForTonOnly: false,
releasedBy: nil
releasedBy: nil,
valueAmount: nil,
valueCurrency: nil
)
if let entry = CodableEntry(RecentStarGiftItem(gift)) {
items.append(OrderedItemListEntry(id: RecentStarGiftItemId(id).rawValue, contents: entry))

View file

@ -243,7 +243,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
case paymentRefunded(peerId: PeerId, currency: String, totalAmount: Int64, payload: Data?, transactionId: String)
case giftStars(currency: String, amount: Int64, count: Int64, cryptoCurrency: String?, cryptoAmount: Int64?, transactionId: String?)
case prizeStars(amount: Int64, isUnclaimed: Bool, boostPeerId: PeerId?, transactionId: String?, giveawayMessageId: MessageId?)
case starGift(gift: StarGift, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool, upgraded: Bool, canUpgrade: Bool, upgradeStars: Int64?, isRefunded: Bool, isPrepaidUpgrade: Bool, upgradeMessageId: Int32?, peerId: EnginePeer.Id?, senderId: EnginePeer.Id?, savedId: Int64?)
case starGift(gift: StarGift, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool, upgraded: Bool, canUpgrade: Bool, upgradeStars: Int64?, isRefunded: Bool, isPrepaidUpgrade: Bool, upgradeMessageId: Int32?, peerId: EnginePeer.Id?, senderId: EnginePeer.Id?, savedId: Int64?, prepaidUpgradeHash: String?)
case starGiftUnique(gift: StarGift, isUpgrade: Bool, isTransferred: Bool, savedToProfile: Bool, canExportDate: Int32?, transferStars: Int64?, isRefunded: Bool, isPrepaidUpgrade: Bool, peerId: EnginePeer.Id?, senderId: EnginePeer.Id?, savedId: Int64?, resaleAmount: CurrencyAmount?, canTransferDate: Int32?, canResaleDate: Int32?)
case paidMessagesRefunded(count: Int32, stars: Int64)
case paidMessagesPriceEdited(stars: Int64, broadcastMessagesAllowed: Bool)
@ -375,7 +375,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
}
self = .prizeStars(amount: decoder.decodeInt64ForKey("amount", orElse: 0), isUnclaimed: decoder.decodeBoolForKey("unclaimed", orElse: false), boostPeerId: boostPeerId, transactionId: decoder.decodeOptionalStringForKey("transactionId"), giveawayMessageId: giveawayMessageId)
case 44:
self = .starGift(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, convertStars: decoder.decodeOptionalInt64ForKey("convertStars"), text: decoder.decodeOptionalStringForKey("text"), entities: decoder.decodeOptionalObjectArrayWithDecoderForKey("entities"), nameHidden: decoder.decodeBoolForKey("nameHidden", orElse: false), savedToProfile: decoder.decodeBoolForKey("savedToProfile", orElse: false), converted: decoder.decodeBoolForKey("converted", orElse: false), upgraded: decoder.decodeBoolForKey("upgraded", orElse: false), canUpgrade: decoder.decodeBoolForKey("canUpgrade", orElse: false), upgradeStars: decoder.decodeOptionalInt64ForKey("upgradeStars"), isRefunded: decoder.decodeBoolForKey("isRefunded", orElse: false), isPrepaidUpgrade: decoder.decodeBoolForKey("isPrepaidUpgrade", orElse: false), upgradeMessageId: decoder.decodeOptionalInt32ForKey("upgradeMessageId"), peerId: decoder.decodeOptionalInt64ForKey("peerId").flatMap { EnginePeer.Id($0) }, senderId: decoder.decodeOptionalInt64ForKey("senderId").flatMap { EnginePeer.Id($0) }, savedId: decoder.decodeOptionalInt64ForKey("savedId"))
self = .starGift(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, convertStars: decoder.decodeOptionalInt64ForKey("convertStars"), text: decoder.decodeOptionalStringForKey("text"), entities: decoder.decodeOptionalObjectArrayWithDecoderForKey("entities"), nameHidden: decoder.decodeBoolForKey("nameHidden", orElse: false), savedToProfile: decoder.decodeBoolForKey("savedToProfile", orElse: false), converted: decoder.decodeBoolForKey("converted", orElse: false), upgraded: decoder.decodeBoolForKey("upgraded", orElse: false), canUpgrade: decoder.decodeBoolForKey("canUpgrade", orElse: false), upgradeStars: decoder.decodeOptionalInt64ForKey("upgradeStars"), isRefunded: decoder.decodeBoolForKey("isRefunded", orElse: false), isPrepaidUpgrade: decoder.decodeBoolForKey("isPrepaidUpgrade", orElse: false), upgradeMessageId: decoder.decodeOptionalInt32ForKey("upgradeMessageId"), peerId: decoder.decodeOptionalInt64ForKey("peerId").flatMap { EnginePeer.Id($0) }, senderId: decoder.decodeOptionalInt64ForKey("senderId").flatMap { EnginePeer.Id($0) }, savedId: decoder.decodeOptionalInt64ForKey("savedId"), prepaidUpgradeHash: decoder.decodeOptionalStringForKey("prepaidUpgradeHash"))
case 45:
var resaleAmount: CurrencyAmount?
if let amount = decoder.decodeCodable(CurrencyAmount.self, forKey: "resaleAmount") {
@ -705,7 +705,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
} else {
encoder.encodeNil(forKey: "giveawayMsgId")
}
case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, isRefunded, isPrepaidUpgrade, upgradeMessageId, peerId, senderId, savedId):
case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, isRefunded, isPrepaidUpgrade, upgradeMessageId, peerId, senderId, savedId, prepaidUpgradeHash):
encoder.encodeInt32(44, forKey: "_rawValue")
encoder.encodeObject(gift, forKey: "gift")
if let convertStars {
@ -752,6 +752,11 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
} else {
encoder.encodeNil(forKey: "savedId")
}
if let prepaidUpgradeHash {
encoder.encodeString(prepaidUpgradeHash, forKey: "prepaidUpgradeHash")
} else {
encoder.encodeNil(forKey: "prepaidUpgradeHash")
}
case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded, isPrepaidUpgrade, peerId, senderId, savedId, resaleAmount, canTransferDate, canResaleDate):
encoder.encodeInt32(45, forKey: "_rawValue")
encoder.encodeObject(gift, forKey: "gift")
@ -877,7 +882,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
return [peerId]
case let .prizeStars(_, _, boostPeerId, _, _):
return boostPeerId.flatMap { [$0] } ?? []
case let .starGift(gift, _, _, _, _, _, _, _, _, _, _, _, _, peerId, senderId, _):
case let .starGift(gift, _, _, _, _, _, _, _, _, _, _, _, _, peerId, senderId, _, _):
var peerIds: [PeerId] = []
if let peerId {
peerIds.append(peerId)

View file

@ -291,6 +291,7 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
public struct UniqueGift: Equatable, Codable, PostboxCoding {
enum CodingKeys: String, CodingKey {
case id
case giftId
case title
case number
case slug
@ -304,6 +305,8 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
case resellAmounts
case resellForTonOnly
case releasedBy
case valueAmount
case valueCurrency
}
public enum Attribute: Equatable, Codable, PostboxCoding {
@ -543,12 +546,30 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
return nil
}
}
public struct ValueInfo: Equatable {
public let isLastSaleOnFragment: Bool
public let valueIsAverage: Bool
public let value: Int64
public let currency: String
public let initialSaleDate: Int32
public let initialSaleStars: Int64
public let initialSalePrice: Int64
public let lastSaleDate: Int32?
public let lastSalePrice: Int64?
public let floorPrice: Int64?
public let averagePrice: Int64?
public let listedCount: Int32?
public let fragmentListedCount: Int32?
public let fragmentListedUrl: String?
}
public enum DecodingError: Error {
case generic
}
public let id: Int64
public let giftId: Int64
public let title: String
public let number: Int32
public let slug: String
@ -559,9 +580,12 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
public let resellAmounts: [CurrencyAmount]?
public let resellForTonOnly: Bool
public let releasedBy: EnginePeer.Id?
public let valueAmount: Int64?
public let valueCurrency: String?
public init(id: Int64, title: String, number: Int32, slug: String, owner: Owner, attributes: [Attribute], availability: Availability, giftAddress: String?, resellAmounts: [CurrencyAmount]?, resellForTonOnly: Bool, releasedBy: EnginePeer.Id?) {
public init(id: Int64, giftId: Int64, title: String, number: Int32, slug: String, owner: Owner, attributes: [Attribute], availability: Availability, giftAddress: String?, resellAmounts: [CurrencyAmount]?, resellForTonOnly: Bool, releasedBy: EnginePeer.Id?, valueAmount: Int64?, valueCurrency: String?) {
self.id = id
self.giftId = giftId
self.title = title
self.number = number
self.slug = slug
@ -572,11 +596,14 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
self.resellAmounts = resellAmounts
self.resellForTonOnly = resellForTonOnly
self.releasedBy = releasedBy
self.valueAmount = valueAmount
self.valueCurrency = valueCurrency
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int64.self, forKey: .id)
self.giftId = try container.decode(Int64.self, forKey: .giftId)
self.title = try container.decode(String.self, forKey: .title)
self.number = try container.decode(Int32.self, forKey: .number)
self.slug = try container.decodeIfPresent(String.self, forKey: .slug) ?? ""
@ -601,10 +628,13 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
}
self.resellForTonOnly = try container.decodeIfPresent(Bool.self, forKey: .resellForTonOnly) ?? false
self.releasedBy = try container.decodeIfPresent(EnginePeer.Id.self, forKey: .releasedBy)
self.valueAmount = try container.decodeIfPresent(Int64.self, forKey: .valueAmount)
self.valueCurrency = try container.decodeIfPresent(String.self, forKey: .valueCurrency)
}
public init(decoder: PostboxDecoder) {
self.id = decoder.decodeInt64ForKey(CodingKeys.id.rawValue, orElse: 0)
self.giftId = decoder.decodeInt64ForKey(CodingKeys.giftId.rawValue, orElse: 0)
self.title = decoder.decodeStringForKey(CodingKeys.title.rawValue, orElse: "")
self.number = decoder.decodeInt32ForKey(CodingKeys.number.rawValue, orElse: 0)
self.slug = decoder.decodeStringForKey(CodingKeys.slug.rawValue, orElse: "")
@ -629,11 +659,14 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
}
self.resellForTonOnly = decoder.decodeBoolForKey(CodingKeys.resellForTonOnly.rawValue, orElse: false)
self.releasedBy = decoder.decodeOptionalInt64ForKey(CodingKeys.releasedBy.rawValue).flatMap { EnginePeer.Id($0) }
self.valueAmount = decoder.decodeOptionalInt64ForKey(CodingKeys.valueAmount.rawValue)
self.valueCurrency = decoder.decodeOptionalStringForKey(CodingKeys.valueCurrency.rawValue)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.id, forKey: .id)
try container.encode(self.giftId, forKey: .giftId)
try container.encode(self.title, forKey: .title)
try container.encode(self.number, forKey: .number)
try container.encode(self.slug, forKey: .slug)
@ -651,10 +684,13 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
try container.encodeIfPresent(self.resellAmounts, forKey: .resellAmounts)
try container.encode(self.resellForTonOnly, forKey: .resellForTonOnly)
try container.encodeIfPresent(self.releasedBy, forKey: .releasedBy)
try container.encodeIfPresent(self.valueAmount, forKey: .valueAmount)
try container.encodeIfPresent(self.valueCurrency, forKey: .valueCurrency)
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeInt64(self.id, forKey: CodingKeys.id.rawValue)
encoder.encodeInt64(self.giftId, forKey: CodingKeys.giftId.rawValue)
encoder.encodeString(self.title, forKey: CodingKeys.title.rawValue)
encoder.encodeInt32(self.number, forKey: CodingKeys.number.rawValue)
encoder.encodeString(self.slug, forKey: CodingKeys.slug.rawValue)
@ -684,11 +720,19 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
} else {
encoder.encodeNil(forKey: CodingKeys.releasedBy.rawValue)
}
if let valueAmount = self.valueAmount, let valueCurrency = self.valueCurrency {
encoder.encodeInt64(valueAmount, forKey: CodingKeys.valueAmount.rawValue)
encoder.encodeString(valueCurrency, forKey: CodingKeys.valueCurrency.rawValue)
} else {
encoder.encodeNil(forKey: CodingKeys.valueAmount.rawValue)
encoder.encodeNil(forKey: CodingKeys.valueCurrency.rawValue)
}
}
public func withResellAmounts(_ resellAmounts: [CurrencyAmount]?) -> UniqueGift {
return UniqueGift(
id: self.id,
giftId: self.giftId,
title: self.title,
number: self.number,
slug: self.slug,
@ -698,13 +742,16 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
giftAddress: self.giftAddress,
resellAmounts: resellAmounts,
resellForTonOnly: self.resellForTonOnly,
releasedBy: self.releasedBy
releasedBy: self.releasedBy,
valueAmount: self.valueAmount,
valueCurrency: self.valueCurrency
)
}
public func withResellForTonOnly(_ resellForTonOnly: Bool) -> UniqueGift {
return UniqueGift(
id: self.id,
giftId: self.giftId,
title: self.title,
number: self.number,
slug: self.slug,
@ -714,7 +761,9 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
giftAddress: self.giftAddress,
resellAmounts: self.resellAmounts,
resellForTonOnly: resellForTonOnly,
releasedBy: self.releasedBy
releasedBy: self.releasedBy,
valueAmount: self.valueAmount,
valueCurrency: self.valueCurrency
)
}
}
@ -824,7 +873,7 @@ extension StarGift {
return nil
}
self = .generic(StarGift.Gift(id: id, title: title, file: file, price: stars, convertStars: convertStars, availability: availability, soldOut: soldOut, flags: flags, upgradeStars: upgradeStars, releasedBy: releasedBy?.peerId, perUserLimit: perUserLimit))
case let .starGiftUnique(flags, id, _, title, slug, num, ownerPeerId, ownerName, ownerAddress, attributes, availabilityIssued, availabilityTotal, giftAddress, resellAmounts, releasedBy, _, _):
case let .starGiftUnique(flags, id, giftId, title, slug, num, ownerPeerId, ownerName, ownerAddress, attributes, availabilityIssued, availabilityTotal, giftAddress, resellAmounts, releasedBy, valueAmount, valueCurrency):
let owner: StarGift.UniqueGift.Owner
if let ownerAddress {
owner = .address(ownerAddress)
@ -836,7 +885,7 @@ extension StarGift {
return nil
}
let resellAmounts = resellAmounts?.compactMap { CurrencyAmount(apiAmount: $0) }
self = .unique(StarGift.UniqueGift(id: id, title: title, number: num, slug: slug, owner: owner, attributes: attributes.compactMap { UniqueGift.Attribute(apiAttribute: $0) }, availability: UniqueGift.Availability(issued: availabilityIssued, total: availabilityTotal), giftAddress: giftAddress, resellAmounts: resellAmounts, resellForTonOnly: (flags & (1 << 7)) != 0, releasedBy: releasedBy?.peerId))
self = .unique(StarGift.UniqueGift(id: id, giftId: giftId, title: title, number: num, slug: slug, owner: owner, attributes: attributes.compactMap { UniqueGift.Attribute(apiAttribute: $0) }, availability: UniqueGift.Availability(issued: availabilityIssued, total: availabilityTotal), giftAddress: giftAddress, resellAmounts: resellAmounts, resellForTonOnly: (flags & (1 << 7)) != 0, releasedBy: releasedBy?.peerId, valueAmount: valueAmount, valueCurrency: valueCurrency))
}
}
}
@ -2530,6 +2579,41 @@ func _internal_getUniqueStarGift(account: Account, slug: String) -> Signal<StarG
}
}
func _internal_getUniqueStarGiftValueInfo(account: Account, slug: String) -> Signal<StarGift.UniqueGift.ValueInfo?, NoError> {
return account.network.request(Api.functions.payments.getUniqueStarGiftValueInfo(slug: slug))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.payments.UniqueStarGiftValueInfo?, NoError> in
return .single(nil)
}
|> map { result -> StarGift.UniqueGift.ValueInfo? in
if let result {
switch result {
case let .uniqueStarGiftValueInfo(flags, currency, value, initialSaleDate, initialSaleStars, initialSalePrice, lastSaleDate, lastSalePrice, floorPrice, averagePrice, listedCount, fragmentListedCount, fragmentListedUrl):
let _ = listedCount
let _ = fragmentListedCount
return StarGift.UniqueGift.ValueInfo(
isLastSaleOnFragment: flags & (1 << 1) != 0,
valueIsAverage: flags & (1 << 6) != 0,
value: value,
currency: currency,
initialSaleDate: initialSaleDate,
initialSaleStars: initialSaleStars,
initialSalePrice: initialSalePrice,
lastSaleDate: lastSaleDate,
lastSalePrice: lastSalePrice,
floorPrice: floorPrice,
averagePrice: averagePrice,
listedCount: listedCount,
fragmentListedCount: fragmentListedCount,
fragmentListedUrl: fragmentListedUrl
)
}
} else {
return nil
}
}
}
public enum StarGiftReference: Equatable, Hashable, Codable {
enum CodingKeys: String, CodingKey {
case type

View file

@ -145,6 +145,10 @@ public extension TelegramEngine {
return _internal_getUniqueStarGift(account: self.account, slug: slug)
}
public func getUniqueStarGiftValueInfo(slug: String) -> Signal<StarGift.UniqueGift.ValueInfo?, NoError> {
return _internal_getUniqueStarGiftValueInfo(account: self.account, slug: slug)
}
public func checkStarGiftWithdrawalAvailability(reference: StarGiftReference) -> Signal<Never, RequestStarGiftWithdrawalError> {
return _internal_checkStarGiftWithdrawalAvailability(account: self.account, reference: reference)
}

View file

@ -1134,7 +1134,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
attributedString = mutableString
case .prizeStars:
attributedString = NSAttributedString(string: strings.Notification_StarsPrize, font: titleFont, textColor: primaryTextColor)
case let .starGift(gift, _, text, entities, _, _, _, _, _, upgradeStars, _, _, _, peerId, senderId, _):
case let .starGift(gift, _, text, entities, _, _, _, _, _, upgradeStars, _, isPrepaidUpgrade, _, peerId, senderId, _, _):
if !forAdditionalServiceMessage {
if let text {
let mutableAttributedString = NSMutableAttributedString(attributedString: stringWithAppliedEntities(text, entities: entities ?? [], baseColor: primaryTextColor, linkColor: primaryTextColor, baseFont: titleFont, linkFont: titleBoldFont, boldFont: titleBoldFont, italicFont: titleFont, boldItalicFont: titleBoldFont, fixedFont: titleFont, blockQuoteFont: titleFont, underlineLinks: false, message: message._asMessage()))
@ -1159,7 +1159,36 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
} else if message.id.peerId == accountPeerId {
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_Self_Bought(starsPrice)._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes])
} else if message.author?.id == accountPeerId {
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_SentYou(starsPrice)._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes])
if isPrepaidUpgrade, let peerId {
peerIds = [(1, peerId)]
var peerName = ""
if let name = message.peers[peerId].flatMap(EnginePeer.init)?.compactDisplayTitle {
peerName = name
}
let starsPrice = strings.Notification_StarsGift_Stars(Int32(clamping: upgradeStars ?? 0))
if let senderId, senderId != accountPeerId {
if senderId == peerId {
var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds)
attributes[0] = boldAttributes
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_SentUpgradeYouUnknown(starsPrice, peerName)._tuple, body: bodyAttributes, argumentAttributes: attributes)
} else {
var otherPeerName = ""
if let name = message.peers[senderId].flatMap(EnginePeer.init)?.compactDisplayTitle {
otherPeerName = name
}
peerIds = [(1, peerId), (2, senderId)]
var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds)
attributes[0] = boldAttributes
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_SentUpgradeYouOther(starsPrice, peerName, otherPeerName)._tuple, body: bodyAttributes, argumentAttributes: attributes)
}
} else {
var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds)
attributes[0] = boldAttributes
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_SentUpgradeYou(starsPrice, peerName)._tuple, body: bodyAttributes, argumentAttributes: attributes)
}
} else {
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_SentYou(starsPrice)._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes])
}
} else if let peerId {
peerIds = [(1, peerId)]
var peerName = ""
@ -1181,7 +1210,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_Sent(authorName, starsPrice)._tuple, body: bodyAttributes, argumentAttributes: attributes)
}
}
case let .starGiftUnique(gift, isUpgrade, _, _, _, _, _, _, peerId, senderId, _, resaleStars, _, _):
case let .starGiftUnique(gift, isUpgrade, _, _, _, _, _, isPrepaidUpgrade, peerId, senderId, _, resaleStars, _, _):
if case let .unique(gift) = gift {
if !forAdditionalServiceMessage && !"".isEmpty {
attributedString = NSAttributedString(string: "\(gift.title) #\(presentationStringsFormattedNumber(gift.number, dateTimeFormat.groupingSeparator))", font: titleFont, textColor: primaryTextColor)
@ -1195,10 +1224,18 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
attributedString = NSAttributedString(string: strings.Notification_StarsGift_UpgradeSelf, font: titleFont, textColor: primaryTextColor)
} else if message.author?.id == accountPeerId {
let attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds)
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_UpgradeYou(peerName)._tuple, body: bodyAttributes, argumentAttributes: attributes)
if isPrepaidUpgrade {
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_UpgradePrepaidYou(peerName)._tuple, body: bodyAttributes, argumentAttributes: attributes)
} else {
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_UpgradeYou(peerName)._tuple, body: bodyAttributes, argumentAttributes: attributes)
}
} else {
let attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds)
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_Upgrade(peerName)._tuple, body: bodyAttributes, argumentAttributes: attributes)
if isPrepaidUpgrade {
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_UpgradePrepaid(peerName)._tuple, body: bodyAttributes, argumentAttributes: attributes)
} else {
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_Upgrade(peerName)._tuple, body: bodyAttributes, argumentAttributes: attributes)
}
}
} else {
if message.id.peerId.isTelegramNotifications && senderId == nil {

View file

@ -274,7 +274,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
for media in item.message.media {
if let action = media as? TelegramMediaAction {
switch action.action {
case let .starGift(gift, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
case let .starGift(gift, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
releasedBy = gift.releasedBy
case let .starGiftUnique(gift, _, _, _, _, _, _, _, _, _, _, _, _, _):
releasedBy = gift.releasedBy
@ -547,7 +547,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
buttonTitle = item.presentationData.strings.Notification_PremiumPrize_View
hasServiceMessage = false
}
case let .starGift(gift, convertStars, giftText, giftEntities, _, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, isRefunded, _, _, channelPeerId, senderPeerId, _):
case let .starGift(gift, convertStars, giftText, giftEntities, _, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, isRefunded, isPrepaidUpgrade, _, channelPeerId, senderPeerId, _, _):
if case let .generic(gift) = gift {
if let releasedBy = gift.releasedBy, let peer = item.message.peers[releasedBy], let addressName = peer.addressName {
creatorButtonTitle = item.presentationData.strings.Notification_StarGift_ReleasedBy("**@\(addressName)**").string
@ -561,10 +561,14 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
if isSelfGift {
title = item.presentationData.strings.Notification_StarGift_Self_Title
} else {
if let senderPeerId, let name = item.message.peers[senderPeerId].flatMap(EnginePeer.init)?.compactDisplayTitle {
authorName = name
if isPrepaidUpgrade && senderPeerId == channelPeerId {
title = item.presentationData.strings.Gift_View_Unknown_Title
} else {
if let senderPeerId, let name = item.message.peers[senderPeerId].flatMap(EnginePeer.init)?.compactDisplayTitle {
authorName = name
}
title = item.presentationData.strings.Notification_StarGift_Title(authorName).string
}
title = item.presentationData.strings.Notification_StarGift_Title(authorName).string
}
if let giftText, !giftText.isEmpty {
text = giftText
@ -611,7 +615,9 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
text = item.presentationData.strings.Notification_StarGift_Bot_Subtitle
}
} else {
if upgradeStars != nil {
if isPrepaidUpgrade {
text = item.presentationData.strings.Notification_StarGift_Subtitle_Upgrade_Prepaid(peerName).string
} else if upgradeStars != nil {
text = item.presentationData.strings.Notification_StarGift_Subtitle_Upgrade_Other(peerName).string
} else if let convertStars, convertStars > 0 {
let starsString = item.presentationData.strings.Notification_StarGift_Subtitle_Other_Stars(Int32(clamping: convertStars)).replacingOccurrences(of: " ", with: "\u{00A0}")

View file

@ -134,6 +134,7 @@ public final class GiftItemComponent: Component {
case preview
case grid
case select
case buttonIcon
}
let context: AccountContext
@ -375,6 +376,10 @@ public final class GiftItemComponent: Component {
size = availableSize
iconSize = CGSize(width: floor(size.width * 0.6), height: floor(size.width * 0.6))
cornerRadius = 4.0
case .buttonIcon:
size = CGSize(width: 26.0, height: 26.0)
iconSize = size
cornerRadius = 0.0
}
var backgroundSize = size
if case .grid = component.mode {
@ -464,7 +469,7 @@ public final class GiftItemComponent: Component {
break
}
}
if let animationFile {
emoji = ChatTextInputTextCustomEmojiAttribute(
interactivelySelectedFromPackId: nil,
@ -476,6 +481,13 @@ public final class GiftItemComponent: Component {
}
}
if case .buttonIcon = component.mode {
backgroundColor = nil
secondBackgroundColor = nil
patternColor = nil
placeholderColor = component.theme.list.mediaPlaceholderColor
}
var animationTransition = transition
if self.animationLayer == nil || self.animationFile?.fileId != animationFile?.fileId, let emoji {
animationTransition = .immediate

View file

@ -239,7 +239,7 @@ final class ChatGiftPreviewItemNode: ListViewItemNode {
case let .starGift(gift):
media = [
TelegramMediaAction(
action: .starGift(gift: .generic(gift), convertStars: gift.convertStars, text: item.text, entities: item.entities, nameHidden: false, savedToProfile: false, converted: false, upgraded: false, canUpgrade: gift.upgradeStars != nil, upgradeStars: item.upgradeStars, isRefunded: false, isPrepaidUpgrade: false, upgradeMessageId: nil, peerId: nil, senderId: nil, savedId: nil)
action: .starGift(gift: .generic(gift), convertStars: gift.convertStars, text: item.text, entities: item.entities, nameHidden: false, savedToProfile: false, converted: false, upgraded: false, canUpgrade: gift.upgradeStars != nil, upgradeStars: item.upgradeStars, isRefunded: false, isPrepaidUpgrade: false, upgradeMessageId: nil, peerId: nil, senderId: nil, savedId: nil, prepaidUpgradeHash: nil)
)
]
}

View file

@ -4,6 +4,7 @@ import ComponentFlow
import Display
import TelegramPresentationData
import ViewControllerComponent
import SheetComponent
import AccountContext
final class GiftPagerComponent: Component {
@ -34,6 +35,7 @@ final class GiftPagerComponent: Component {
let items: [Item]
let index: Int
let itemSpacing: CGFloat
let isSwitching: Bool
let updated: (CGFloat, Int) -> Void
public init(
@ -41,12 +43,14 @@ final class GiftPagerComponent: Component {
items: [Item],
index: Int = 0,
itemSpacing: CGFloat = 0.0,
isSwitching: Bool = false,
updated: @escaping (CGFloat, Int) -> Void
) {
self.context = context
self.items = items
self.index = index
self.itemSpacing = itemSpacing
self.isSwitching = isSwitching
self.updated = updated
}
@ -60,10 +64,14 @@ final class GiftPagerComponent: Component {
if lhs.itemSpacing != rhs.itemSpacing {
return false
}
if lhs.isSwitching != rhs.isSwitching {
return false
}
return true
}
final class View: UIView, UIScrollViewDelegate {
private let dimView: UIView
private let scrollView: UIScrollView
private var itemViews: [AnyHashable: ComponentHostView<EnvironmentType>] = [:]
@ -71,7 +79,11 @@ final class GiftPagerComponent: Component {
private var environment: Environment<EnvironmentType>?
override init(frame: CGRect) {
self.dimView = UIView()
self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.4)
self.scrollView = UIScrollView(frame: frame)
self.scrollView.clipsToBounds = true
self.scrollView.isPagingEnabled = true
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.showsVerticalScrollIndicator = false
@ -84,8 +96,9 @@ final class GiftPagerComponent: Component {
super.init(frame: frame)
self.scrollView.delegate = self
self.addSubview(self.dimView)
self.scrollView.delegate = self
self.addSubview(self.scrollView)
}
@ -93,6 +106,15 @@ final class GiftPagerComponent: Component {
fatalError("init(coder:) has not been implemented")
}
private func animateIn() {
self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
}
private func animateOut() {
self.dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
}
private var isSwiping: Bool = false
private var lastScrollTime: TimeInterval = 0
private let swipeInactiveThreshold: TimeInterval = 0.5
@ -128,33 +150,43 @@ final class GiftPagerComponent: Component {
self.ignoreContentOffsetChange = false
}
private var previousIsDisplaying: Bool = false
private var isUpdating = true
func update(component: GiftPagerComponent, availableSize: CGSize, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize), completion: nil)
var validIds: [AnyHashable] = []
let previousComponent = self.component
self.component = component
self.environment = environment
let firstTime = self.itemViews.isEmpty
var countDecreased = false
if let previousComponent, previousComponent.items.count != component.items.count {
countDecreased = true
}
let firstTime = self.itemViews.isEmpty || countDecreased
let itemWidth = availableSize.width
let totalWidth = itemWidth * CGFloat(component.items.count) + component.itemSpacing * CGFloat(max(0, component.items.count - 1))
let totalWidth = itemWidth * CGFloat(component.items.count) + component.itemSpacing * 2.0 * CGFloat(max(0, component.items.count))
let contentSize = CGSize(width: totalWidth, height: availableSize.height)
if self.scrollView.contentSize != contentSize {
self.scrollView.contentSize = contentSize
}
let scrollFrame = CGRect(origin: .zero, size: availableSize)
let scrollFrame = CGRect(origin: CGPoint(x: -component.itemSpacing / 2.0, y: 0.0), size: CGSize(width: availableSize.width + component.itemSpacing * 2.0, height: availableSize.height))
if self.scrollView.frame != scrollFrame {
self.scrollView.frame = scrollFrame
}
if firstTime {
let initialOffset = CGFloat(component.index) * (itemWidth + component.itemSpacing)
let initialOffset = CGFloat(component.index) * (itemWidth + component.itemSpacing * 2.0)
self.scrollView.contentOffset = CGPoint(x: initialOffset, y: 0.0)
var position: CGFloat
@ -172,7 +204,7 @@ final class GiftPagerComponent: Component {
var i = 0
for item in component.items {
let itemOriginX = (itemWidth + component.itemSpacing) * CGFloat(i)
let itemOriginX = component.itemSpacing * 0.5 + (itemWidth + component.itemSpacing * 2.0) * CGFloat(i)
let itemFrame = CGRect(origin: CGPoint(x: itemOriginX, y: 0.0), size: CGSize(width: itemWidth, height: availableSize.height))
let centerDelta = itemFrame.midX - viewportCenter
@ -216,17 +248,39 @@ final class GiftPagerComponent: Component {
itemView.frame = itemFrame
}
var animateTransitionMovement = false
var removeIds: [AnyHashable] = []
for (id, itemView) in self.itemViews {
if !validIds.contains(id) {
removeIds.append(id)
itemView.removeFromSuperview()
if countDecreased && !transition.animation.isImmediate {
self.addSubview(itemView)
itemView.center = CGPoint(x: self.scrollView.frame.width / 2.0 - component.itemSpacing, y: self.scrollView.frame.height / 2.0)
transition.setPosition(view: itemView, position: CGPoint(x: itemView.center.x - itemView.frame.width, y: itemView.center.y), completion: { _ in
itemView.removeFromSuperview()
})
animateTransitionMovement = true
} else {
itemView.removeFromSuperview()
}
}
}
for id in removeIds {
self.itemViews.removeValue(forKey: id)
}
if animateTransitionMovement {
transition.animatePosition(view: self.scrollView, from: CGPoint(x: self.scrollView.frame.width, y: 0.0), to: .zero, additive: true)
}
let viewEnvironment = environment[ViewControllerComponentContainer.Environment.self].value
if let _ = transition.userData(ViewControllerComponentContainer.AnimateInTransition.self) {
self.animateIn()
} else if self.previousIsDisplaying, let _ = transition.userData(ViewControllerComponentContainer.AnimateOutTransition.self) {
self.animateOut()
}
self.previousIsDisplaying = viewEnvironment.isVisible
return availableSize
}
}

View file

@ -135,31 +135,11 @@ private final class GiftPurchaseAlertContentNode: AlertContentNode {
}
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
let containerSize = size
var size = size
size.width = min(size.width, 270.0)
self.validLayout = size
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
var origin: CGPoint = CGPoint(x: 0.0, y: 20.0)
var resellPrice: CurrencyAmount?
if let actionNode = self.actionNodes.first {
switch self.currency {
case .stars:
if let resellAmount = self.gift.resellAmounts?.first(where: { $0.currency == .stars }) {
resellPrice = resellAmount
actionNode.action = TextAlertAction(type: .defaultAction, title: self.strings.Gift_Buy_Confirm_BuyFor(Int32(resellAmount.amount.value)), action: actionNode.action.action)
}
case .ton:
if let resellAmount = self.gift.resellAmounts?.first(where: { $0.currency == .ton }) {
resellPrice = resellAmount
let valueString = formatTonAmountText(resellAmount.amount.value, dateTimeFormat: presentationData.dateTimeFormat)
actionNode.action = TextAlertAction(type: .defaultAction, title: self.strings.Gift_Buy_Confirm_BuyForTon(valueString).string, action: actionNode.action.action)
}
}
}
var origin = CGPoint(x: 0.0, y: 20.0)
if self.gift.resellForTonOnly {
let headerSize = self.header.update(
transition: .immediate,
@ -173,6 +153,7 @@ private final class GiftPurchaseAlertContentNode: AlertContentNode {
environment: {},
containerSize: CGSize(width: size.width - 32.0, height: size.height)
)
let headerFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - headerSize.width) / 2.0), y: origin.y), size: headerSize)
if let view = self.header.view {
if view.superview == nil {
@ -226,8 +207,11 @@ private final class GiftPurchaseAlertContentNode: AlertContentNode {
}
)),
environment: {},
containerSize: CGSize(width: size.width - 16.0 * 2.0, height: 100.0)
containerSize: CGSize(width: containerSize.width - 16.0 * 2.0, height: 100.0)
)
size.width = min(containerSize.width, max(270.0, headerSize.width + 32.0))
let headerFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - headerSize.width) / 2.0), y: origin.y), size: headerSize)
if let view = self.header.view {
if view.superview == nil {
@ -238,6 +222,27 @@ private final class GiftPurchaseAlertContentNode: AlertContentNode {
origin.y += headerSize.height + 17.0
}
self.validLayout = size
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
var resellPrice: CurrencyAmount?
if let actionNode = self.actionNodes.first {
switch self.currency {
case .stars:
if let resellAmount = self.gift.resellAmounts?.first(where: { $0.currency == .stars }) {
resellPrice = resellAmount
actionNode.action = TextAlertAction(type: .defaultAction, title: self.strings.Gift_Buy_Confirm_BuyFor(Int32(resellAmount.amount.value)), action: actionNode.action.action)
}
case .ton:
if let resellAmount = self.gift.resellAmounts?.first(where: { $0.currency == .ton }) {
resellPrice = resellAmount
let valueString = formatTonAmountText(resellAmount.amount.value, dateTimeFormat: presentationData.dateTimeFormat)
actionNode.action = TextAlertAction(type: .defaultAction, title: self.strings.Gift_Buy_Confirm_BuyForTon(valueString).string, action: actionNode.action.action)
}
}
}
let avatarSize = CGSize(width: 60.0, height: 60.0)
self.avatarNode.updateSize(size: avatarSize)
@ -495,9 +500,13 @@ public func giftPurchaseAlertController(
if !gift.resellForTonOnly {
Queue.mainQueue().after(0.3) {
if let headerView = contentNode?.header.view {
if let headerView = contentNode?.header.view as? TabSelectorComponent.View {
let absoluteFrame = headerView.convert(headerView.bounds, to: nil)
let location = CGRect(origin: CGPoint(x: absoluteFrame.minX + floor(absoluteFrame.width * 0.75), y: absoluteFrame.minY - 8.0), size: CGSize())
var originX = absoluteFrame.width * 0.75
if let itemFrame = headerView.frameForItem(AnyHashable(1)) {
originX = itemFrame.midX
}
let location = CGRect(origin: CGPoint(x: absoluteFrame.minX + floor(originX), y: absoluteFrame.minY - 8.0), size: CGSize())
let tooltipController = TooltipScreen(account: context.account, sharedContext: context.sharedContext, text: .plain(text: presentationData.strings.Gift_Buy_PayInTon_Tooltip), style: .wide, location: .point(location, .bottom), displayDuration: .default, inset: 16.0, shouldDismissOnTouch: { _, _ in
return .dismiss(consume: false)
})

View file

@ -0,0 +1,862 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SwiftSignalKit
import AccountContext
import TelegramPresentationData
import PresentationDataUtils
import ComponentFlow
import ViewControllerComponent
import SheetComponent
import MultilineTextComponent
import MultilineTextWithEntitiesComponent
import BundleIconComponent
import ButtonComponent
import Markdown
import BalancedTextComponent
import AvatarNode
import TextFormat
import TelegramStringFormatting
import StarsAvatarComponent
import EmojiTextAttachmentView
import EmojiStatusComponent
import UndoUI
import PlainButtonComponent
import TooltipUI
import GiftAnimationComponent
import LottieComponent
import ContextUI
import TelegramNotices
import GiftItemComponent
private final class GiftValueSheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let gift: ProfileGiftsContext.State.StarGift
let valueInfo: StarGift.UniqueGift.ValueInfo
let animateOut: ActionSlot<Action<()>>
let getController: () -> ViewController?
init(
context: AccountContext,
gift: ProfileGiftsContext.State.StarGift,
valueInfo: StarGift.UniqueGift.ValueInfo,
animateOut: ActionSlot<Action<()>>,
getController: @escaping () -> ViewController?
) {
self.context = context
self.gift = gift
self.valueInfo = valueInfo
self.animateOut = animateOut
self.getController = getController
}
static func ==(lhs: GiftValueSheetContent, rhs: GiftValueSheetContent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.gift != rhs.gift {
return false
}
if lhs.valueInfo != rhs.valueInfo {
return false
}
return true
}
final class State: ComponentState {
let minimumPriceTag = GenericComponentViewTag()
let averagePriceTag = GenericComponentViewTag()
private let context: AccountContext
private let animateOut: ActionSlot<Action<()>>
private let getController: () -> ViewController?
private var disposable: Disposable?
var initialized = false
var starGiftsMap: [Int64: StarGift.Gift] = [:]
var cachedStarImage: (UIImage, PresentationTheme)?
var cachedSmallStarImage: (UIImage, PresentationTheme)?
var cachedSubtitleStarImage: (UIImage, PresentationTheme)?
var cachedTonImage: (UIImage, PresentationTheme)?
var cachedChevronImage: (UIImage, PresentationTheme)?
var cachedSmallChevronImage: (UIImage, PresentationTheme)?
init(
context: AccountContext,
animateOut: ActionSlot<Action<()>>,
getController: @escaping () -> ViewController?
) {
self.context = context
self.animateOut = animateOut
self.getController = getController
super.init()
self.disposable = (context.engine.payments.cachedStarGifts()
|> deliverOnMainQueue).startStrict(next: { [weak self] starGifts in
if let strongSelf = self {
var starGiftsMap: [Int64: StarGift.Gift] = [:]
if let starGifts {
for gift in starGifts {
if case let .generic(gift) = gift {
starGiftsMap[gift.id] = gift
}
}
}
strongSelf.starGiftsMap = starGiftsMap
strongSelf.updated(transition: .immediate)
}
})
}
deinit {
self.disposable?.dispose()
}
func showAttributeInfo(tag: Any, text: String) {
guard let controller = self.getController() as? GiftValueScreen else {
return
}
controller.dismissAllTooltips()
guard let sourceView = controller.node.hostView.findTaggedView(tag: tag), let absoluteLocation = sourceView.superview?.convert(sourceView.center, to: controller.view) else {
return
}
let location = CGRect(origin: CGPoint(x: absoluteLocation.x, y: absoluteLocation.y - 12.0), size: CGSize())
let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: .markdown(text: text), style: .wide, location: .point(location, .bottom), displayDuration: .default, inset: 16.0, shouldDismissOnTouch: { _, _ in
return .dismiss(consume: false)
})
controller.present(tooltipController, in: .current)
}
func openGiftResale(gift: StarGift.Gift) {
guard let controller = self.getController() as? GiftValueScreen else {
return
}
let storeController = self.context.sharedContext.makeGiftStoreController(
context: self.context,
peerId: self.context.account.peerId,
gift: gift
)
controller.push(storeController)
Queue.mainQueue().after(2.0, {
controller.dismiss(animated: false)
})
}
func dismiss(animated: Bool) {
guard let controller = self.getController() as? GiftValueScreen else {
return
}
if animated {
controller.dismissAllTooltips()
self.animateOut.invoke(Action { [weak controller] _ in
controller?.dismiss(completion: nil)
})
} else {
controller.dismiss(animated: false)
}
}
}
func makeState() -> State {
return State(context: self.context, animateOut: self.animateOut, getController: self.getController)
}
static var body: Body {
let buttons = Child(ButtonsComponent.self)
let animation = Child(GiftCompositionComponent.self)
let titleBackground = Child(RoundedRectangle.self)
let title = Child(MultilineTextComponent.self)
let description = Child(MultilineTextComponent.self)
let table = Child(TableComponent.self)
let telegramSaleButton = Child(PlainButtonComponent.self)
let fragmentSaleButton = Child(PlainButtonComponent.self)
let giftCompositionExternalState = GiftCompositionComponent.ExternalState()
return { context in
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
let component = context.component
let theme = environment.theme
let strings = environment.strings
let dateTimeFormat = environment.dateTimeFormat
//let nameDisplayOrder = component.context.sharedContext.currentPresentationData.with { $0 }.nameDisplayOrder
//let controller = environment.controller
let state = context.state
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
let titleString: String = formatCurrencyAmount(component.valueInfo.value, currency: component.valueInfo.currency)
var giftTitle: String = ""
var giftCollectionTitle: String = ""
var animationFile: TelegramMediaFile?
var giftIconSubject: GiftItemComponent.Subject?
var genericGift: StarGift.Gift?
switch component.gift.gift {
case let .generic(gift):
animationFile = gift.file
giftIconSubject = .starGift(gift: gift, price: "")
case let .unique(gift):
for attribute in gift.attributes {
if case let .model(_, file, _) = attribute {
animationFile = file
}
}
giftCollectionTitle = gift.title
giftTitle = "\(gift.title) #\(formatCollectibleNumber(gift.number, dateTimeFormat: dateTimeFormat))"
if let gift = state.starGiftsMap[gift.giftId] {
giftIconSubject = .starGift(gift: gift, price: "")
genericGift = gift
}
}
let buttons = buttons.update(
component: ButtonsComponent(
theme: theme,
isOverlay: false,
showMoreButton: false,
closePressed: { [weak state] in
guard let state else {
return
}
state.dismiss(animated: true)
},
morePressed: { _, _ in
}
),
availableSize: CGSize(width: 30.0, height: 30.0),
transition: context.transition
)
var originY: CGFloat = 0.0
let headerHeight: CGFloat = 210.0
let headerSubject: GiftCompositionComponent.Subject?
if let animationFile {
headerSubject = .generic(animationFile)
} else {
headerSubject = nil
}
if let headerSubject {
let animation = animation.update(
component: GiftCompositionComponent(
context: component.context,
theme: environment.theme,
subject: headerSubject,
animationOffset: nil,
animationScale: nil,
displayAnimationStars: false,
externalState: giftCompositionExternalState,
requestUpdate: { [weak state] in
state?.updated()
}
),
availableSize: CGSize(width: context.availableSize.width, height: headerHeight),
transition: context.transition
)
context.add(animation
.position(CGPoint(x: context.availableSize.width / 2.0, y: headerHeight / 2.0))
)
}
originY += headerHeight
let title = title.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(
string: titleString,
font: Font.with(size: 24.0, design: .round, weight: .bold),
textColor: theme.list.itemCheckColors.foregroundColor,
paragraphAlignment: .center
)),
horizontalAlignment: .center,
maximumNumberOfLines: 1
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
let titleBackground = titleBackground.update(
component: RoundedRectangle(color: theme.actionSheet.controlAccentColor, cornerRadius: 24.0),
environment: {},
availableSize: CGSize(width: title.size.width + 32.0, height: 48.0),
transition: .immediate
)
context.add(titleBackground
.position(CGPoint(x: context.availableSize.width / 2.0, y: 187.0))
)
context.add(title
.position(CGPoint(x: context.availableSize.width / 2.0, y: 187.0))
)
var descriptionText: String
if component.valueInfo.valueIsAverage {
descriptionText = "This is the average sale price of **\(giftCollectionTitle)** on Telegram and Fragment over the past month."
} else {
if component.valueInfo.isLastSaleOnFragment {
descriptionText = "This is the last price at which **\(giftTitle)** was last sold on Fragment."
} else {
descriptionText = "This is the last price at which **\(giftTitle)** was last sold on Telegram."
}
}
if !descriptionText.isEmpty {
let linkColor = theme.actionSheet.controlAccentColor
if state.cachedSmallStarImage == nil || state.cachedSmallStarImage?.1 !== environment.theme {
state.cachedSmallStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Premium/Stars/ButtonStar"), color: .white)!, theme)
}
if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== environment.theme {
state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: linkColor)!, theme)
}
let textFont = Font.regular(15.0)
let boldTextFont = Font.semibold(15.0)
let textColor = theme.list.itemPrimaryTextColor
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
})
descriptionText = descriptionText.replacingOccurrences(of: " >]", with: "\u{00A0}>]")
let attributedString = parseMarkdownIntoAttributedString(descriptionText, attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString
if let range = attributedString.string.range(of: "*"), let starImage = state.cachedSmallStarImage?.0 {
attributedString.addAttribute(.font, value: Font.regular(13.0), range: NSRange(range, in: attributedString.string))
attributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: attributedString.string))
attributedString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: attributedString.string))
}
if let range = attributedString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 {
attributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedString.string))
}
let description = description.update(
component: MultilineTextComponent(
text: .plain(attributedString),
horizontalAlignment: .center,
maximumNumberOfLines: 5,
lineSpacing: 0.2,
highlightColor: linkColor.withAlphaComponent(0.1),
highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
} else {
return nil
}
},
tapAction: { _, _ in
}
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 50.0, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
context.add(description
.position(CGPoint(x: context.availableSize.width / 2.0, y: 231.0 + description.size.height / 2.0))
.appear(.default(alpha: true))
.disappear(.default(alpha: true))
)
originY += description.size.height
originY += 42.0
} else {
originY += 9.0
}
let tableFont = Font.regular(15.0)
let tableTextColor = theme.list.itemPrimaryTextColor
var tableItems: [TableComponent.Item] = []
tableItems.append(.init(
id: "initialDate",
title: "Initial Sale",
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: component.valueInfo.initialSaleDate, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor)))
)
))
let valueString = "⭐️\(formatStarsAmountText(StarsAmount(value: component.valueInfo.initialSaleStars, nanos: 0), dateTimeFormat: dateTimeFormat)) (≈\(formatCurrencyAmount(component.valueInfo.initialSalePrice, currency: component.valueInfo.currency)))"
let valueAttributedString = NSMutableAttributedString(string: valueString, font: tableFont, textColor: tableTextColor)
let range = (valueAttributedString.string as NSString).range(of: "⭐️")
if range.location != NSNotFound {
valueAttributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range)
valueAttributedString.addAttribute(.baselineOffset, value: 1.0, range: range)
}
tableItems.append(.init(
id: "initialPrice",
title: "Initial Price",
component: AnyComponent(MultilineTextWithEntitiesComponent(
context: component.context,
animationCache: component.context.animationCache,
animationRenderer: component.context.animationRenderer,
placeholderColor: theme.list.mediaPlaceholderColor,
text: .plain(valueAttributedString),
maximumNumberOfLines: 0
)),
insets: UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 12.0)
))
if let lastSaleDate = component.valueInfo.lastSaleDate {
tableItems.append(.init(
id: "lastDate",
title: "Last Sale",
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: lastSaleDate, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor)))
)
))
}
if let lastSalePrice = component.valueInfo.lastSalePrice {
let lastSalePriceString = formatCurrencyAmount(lastSalePrice, currency: component.valueInfo.currency)
let tag = state.minimumPriceTag
var items: [AnyComponentWithIdentity<Empty>] = []
items.append(
AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: lastSalePriceString, font: tableFont, textColor: tableTextColor)))
)
)
)
let percentage = Double(lastSalePrice) / Double(component.valueInfo.initialSalePrice) * 100.0
items.append(AnyComponentWithIdentity(
id: AnyHashable(1),
component: AnyComponent(Button(
content: AnyComponent(ButtonContentComponent(
context: component.context,
text: "+\(Int32(percentage - 100))%",
color: theme.list.itemAccentColor
)),
action: { [weak state] in
state?.showAttributeInfo(tag: tag, text: "**\(lastSalePriceString)** is the last price for \(giftCollectionTitle) gifts listed on Telegram and Fragment.")
}
).tagged(tag))
))
let itemComponent = AnyComponent(
HStack(items, spacing: 4.0)
)
tableItems.append(.init(
id: "lastPrice",
title: "Last Price",
hasBackground: false,
component: itemComponent
))
}
if let floorPrice = component.valueInfo.floorPrice {
let floorPriceString = formatCurrencyAmount(floorPrice, currency: component.valueInfo.currency)
let tag = state.minimumPriceTag
var items: [AnyComponentWithIdentity<Empty>] = []
items.append(
AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: floorPriceString, font: tableFont, textColor: tableTextColor)))
)
)
)
items.append(AnyComponentWithIdentity(
id: AnyHashable(1),
component: AnyComponent(Button(
content: AnyComponent(ButtonContentComponent(
context: component.context,
text: "?",
color: theme.list.itemAccentColor
)),
action: { [weak state] in
state?.showAttributeInfo(tag: tag, text: "**\(floorPriceString)** is the floor price for \(giftCollectionTitle) gifts listed on Telegram and Fragment.")
}
).tagged(tag))
))
let itemComponent = AnyComponent(
HStack(items, spacing: 4.0)
)
tableItems.append(.init(
id: "minimumPrice",
title: "Minumum Price",
hasBackground: false,
component: itemComponent
))
}
if let averagePrice = component.valueInfo.averagePrice {
let averagePriceString = formatCurrencyAmount(averagePrice, currency: component.valueInfo.currency)
let tag = state.averagePriceTag
var items: [AnyComponentWithIdentity<Empty>] = []
items.append(
AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: averagePriceString, font: tableFont, textColor: tableTextColor)))
)
)
)
items.append(AnyComponentWithIdentity(
id: AnyHashable(1),
component: AnyComponent(Button(
content: AnyComponent(ButtonContentComponent(
context: component.context,
text: "?",
color: theme.list.itemAccentColor
)),
action: { [weak state] in
state?.showAttributeInfo(tag: tag, text: "**\(averagePriceString)** is the average sale price of \(giftCollectionTitle) on Telegram and Fragment over the past month.")
}
).tagged(tag))
))
let itemComponent = AnyComponent(
HStack(items, spacing: 4.0)
)
tableItems.append(.init(
id: "averagePrice",
title: "Average Price",
hasBackground: false,
component: itemComponent
))
}
let table = table.update(
component: TableComponent(
theme: environment.theme,
items: tableItems
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude),
transition: context.transition
)
context.add(table
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY + table.size.height / 2.0))
.appear(.default(alpha: true))
.disappear(.default(alpha: true))
)
originY += table.size.height + 23.0
if component.valueInfo.listedCount != nil || component.valueInfo.fragmentListedCount != nil {
originY += 5.0
}
if let listedCount = component.valueInfo.listedCount, let giftIconSubject {
let telegramSaleButton = telegramSaleButton.update(
component: PlainButtonComponent(
content: AnyComponent(
HStack([
AnyComponentWithIdentity(id: "count", component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: presentationStringsFormattedNumber(listedCount, dateTimeFormat.groupingSeparator), font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor)))
)),
AnyComponentWithIdentity(id: "spacing", component: AnyComponent(
Rectangle(color: .clear, width: 8.0, height: 1.0)
)),
AnyComponentWithIdentity(id: "icon", component: AnyComponent(
GiftItemComponent(
context: component.context,
theme: theme,
strings: strings,
peer: nil,
subject: giftIconSubject,
mode: .buttonIcon
)
)),
AnyComponentWithIdentity(id: "label", component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: " for sale on Telegram", font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor)))
)),
AnyComponentWithIdentity(id: "arrow", component: AnyComponent(
BundleIconComponent(name: "Chat/Context Menu/Arrow", tintColor: theme.actionSheet.controlAccentColor)
))
], spacing: 0.0)
),
action: { [weak state] in
guard let state, let genericGift else {
return
}
state.openGiftResale(gift: genericGift)
},
animateScale: false
),
environment: {},
availableSize: context.availableSize,
transition: .immediate
)
context.add(telegramSaleButton
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY + telegramSaleButton.size.height / 2.0))
)
originY += telegramSaleButton.size.height
originY += 12.0
}
if let listedCount = component.valueInfo.fragmentListedCount, let giftIconSubject {
if component.valueInfo.listedCount != nil {
originY += 18.0
}
let fragmentSaleButton = fragmentSaleButton.update(
component: PlainButtonComponent(
content: AnyComponent(
HStack([
AnyComponentWithIdentity(id: "count", component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: presentationStringsFormattedNumber(listedCount, dateTimeFormat.groupingSeparator), font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor)))
)),
AnyComponentWithIdentity(id: "spacing", component: AnyComponent(
Rectangle(color: .clear, width: 8.0, height: 1.0)
)),
AnyComponentWithIdentity(id: "icon", component: AnyComponent(
GiftItemComponent(
context: component.context,
theme: theme,
strings: strings,
peer: nil,
subject: giftIconSubject,
mode: .buttonIcon
)
)),
AnyComponentWithIdentity(id: "label", component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: " for sale on Fragment", font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor)))
)),
AnyComponentWithIdentity(id: "arrow", component: AnyComponent(
BundleIconComponent(name: "Chat/Context Menu/Arrow", tintColor: theme.actionSheet.controlAccentColor)
))
], spacing: 0.0)
),
action: {
},
animateScale: false
),
environment: {},
availableSize: context.availableSize,
transition: .immediate
)
context.add(fragmentSaleButton
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY + fragmentSaleButton.size.height / 2.0))
)
originY += fragmentSaleButton.size.height
originY += 12.0
}
context.add(buttons
.position(CGPoint(x: context.availableSize.width - environment.safeInsets.left - 16.0 - buttons.size.width / 2.0, y: 28.0))
)
let effectiveBottomInset: CGFloat = environment.metrics.isTablet ? 0.0 : environment.safeInsets.bottom
return CGSize(width: context.availableSize.width, height: originY + 5.0 + effectiveBottomInset)
}
}
}
final class GiftValueSheetComponent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let gift: ProfileGiftsContext.State.StarGift
let valueInfo: StarGift.UniqueGift.ValueInfo
init(
context: AccountContext,
gift: ProfileGiftsContext.State.StarGift,
valueInfo: StarGift.UniqueGift.ValueInfo
) {
self.context = context
self.gift = gift
self.valueInfo = valueInfo
}
static func ==(lhs: GiftValueSheetComponent, rhs: GiftValueSheetComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.gift != rhs.gift {
return false
}
if lhs.valueInfo != rhs.valueInfo {
return false
}
return true
}
static var body: Body {
let sheet = Child(SheetComponent<EnvironmentType>.self)
let animateOut = StoredActionSlot(Action<Void>.self)
let sheetExternalState = SheetComponent<EnvironmentType>.ExternalState()
return { context in
let environment = context.environment[EnvironmentType.self]
let controller = environment.controller
let sheet = sheet.update(
component: SheetComponent<EnvironmentType>(
content: AnyComponent<EnvironmentType>(GiftValueSheetContent(
context: context.component.context,
gift: context.component.gift,
valueInfo: context.component.valueInfo,
animateOut: animateOut,
getController: controller
)),
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
followContentSizeChanges: true,
clipsContent: true,
externalState: sheetExternalState,
animateOut: animateOut,
onPan: {
if let controller = controller() as? GiftValueScreen {
controller.dismissAllTooltips()
}
},
willDismiss: {
}
),
environment: {
environment
SheetComponentEnvironment(
isDisplaying: environment.value.isVisible,
isCentered: environment.metrics.widthClass == .regular,
hasInputHeight: !environment.inputHeight.isZero,
regularMetricsSize: CGSize(width: 430.0, height: 900.0),
dismiss: { animated in
if animated {
if let controller = controller() as? GiftValueScreen {
controller.dismissAllTooltips()
animateOut.invoke(Action { _ in
controller.dismiss(completion: nil)
})
}
} else {
if let controller = controller() as? GiftValueScreen {
controller.dismissAllTooltips()
controller.dismiss(completion: nil)
}
}
}
)
},
availableSize: context.availableSize,
transition: context.transition
)
context.add(sheet
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
)
if let controller = controller(), !controller.automaticallyControlPresentationContextLayout {
var sideInset: CGFloat = 0.0
var bottomInset: CGFloat = max(environment.safeInsets.bottom, sheetExternalState.contentHeight)
if case .regular = environment.metrics.widthClass {
sideInset = floor((context.availableSize.width - 430.0) / 2.0) - 12.0
bottomInset = (context.availableSize.height - sheetExternalState.contentHeight) / 2.0 + sheetExternalState.contentHeight
}
let layout = ContainerViewLayout(
size: context.availableSize,
metrics: environment.metrics,
deviceMetrics: environment.deviceMetrics,
intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomInset, right: 0.0),
safeInsets: UIEdgeInsets(top: 0.0, left: max(sideInset, environment.safeInsets.left), bottom: 0.0, right: max(sideInset, environment.safeInsets.right)),
additionalInsets: .zero,
statusBarHeight: environment.statusBarHeight,
inputHeight: nil,
inputHeightIsInteractivellyChanging: false,
inVoiceOver: false
)
controller.presentationContext.containerLayoutUpdated(layout, transition: context.transition.containedViewLayoutTransition)
}
return context.availableSize
}
}
}
final class GiftValueScreen: ViewControllerComponentContainer {
private let context: AccountContext
private let gift: ProfileGiftsContext.State.StarGift
private let valueInfo: StarGift.UniqueGift.ValueInfo
public init(
context: AccountContext,
gift: ProfileGiftsContext.State.StarGift,
valueInfo: StarGift.UniqueGift.ValueInfo
) {
self.context = context
self.gift = gift
self.valueInfo = valueInfo
super.init(
context: context,
component: GiftValueSheetComponent(
context: context,
gift: gift,
valueInfo: valueInfo
),
navigationBarAppearance: .none,
statusBarStyle: .ignore,
theme: .default
)
self.navigationPresentation = .flatModal
self.automaticallyControlPresentationContextLayout = false
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
public override func viewDidLoad() {
super.viewDidLoad()
self.view.disablesInteractiveModalDismiss = true
}
public override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.dismissAllTooltips()
}
public func dismissAnimated() {
self.dismissAllTooltips()
if let view = self.node.hostView.findTaggedView(tag: SheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? SheetComponent<ViewControllerComponentContainer.Environment>.View {
view.dismissAnimated()
}
}
fileprivate func dismissAllTooltips() {
self.window?.forEachController({ controller in
if let controller = controller as? TooltipScreen {
controller.dismiss(inPlace: false)
}
if let controller = controller as? UndoOverlayController {
controller.dismiss()
}
})
self.forEachController({ controller in
if let controller = controller as? TooltipScreen {
controller.dismiss(inPlace: false)
}
if let controller = controller as? UndoOverlayController {
controller.dismiss()
}
return true
})
}
}

View file

@ -35,6 +35,7 @@ import TelegramNotices
import PremiumLockButtonSubtitleComponent
import StarsBalanceOverlayComponent
import BalanceNeededScreen
import GiftItemComponent
private final class GiftViewSheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -101,6 +102,7 @@ private final class GiftViewSheetContent: CombinedComponent {
var inProgress = false
var nextGiftToUpgrade: ProfileGiftsContext.State.StarGift?
var inUpgradePreview = false
var upgradeForm: BotPaymentForm?
var upgradeFormDisposable: Disposable?
@ -210,7 +212,7 @@ private final class GiftViewSheetContent: CombinedComponent {
if let releasedBy = gift.releasedBy {
peerIds.append(releasedBy)
}
if arguments.canUpgrade || arguments.upgradeStars != nil {
if arguments.canUpgrade || arguments.upgradeStars != nil || arguments.prepaidUpgradeHash != nil {
self.sampleDisposable.add((context.engine.payments.starGiftUpgradePreview(giftId: gift.id)
|> deliverOnMainQueue).start(next: { [weak self] attributes in
guard let self else {
@ -630,6 +632,20 @@ private final class GiftViewSheetContent: CombinedComponent {
controller.push(introController)
}
func openValue() {
guard let controller = self.getController(), case let .profileGift(_, gift) = self.subject, case let .unique(uniqueGift) = gift.gift else {
return
}
let _ = (self.context.engine.payments.getUniqueStarGiftValueInfo(slug: uniqueGift.slug)
|> deliverOnMainQueue).start(next: { [weak self] valueInfo in
guard let self, let valueInfo else {
return
}
let valueController = GiftValueScreen(context: self.context, gift: gift, valueInfo: valueInfo)
controller.push(valueController)
})
}
func sendGift(peerId: EnginePeer.Id) {
guard let controller = self.getController() else {
return
@ -820,7 +836,7 @@ private final class GiftViewSheetContent: CombinedComponent {
return
}
let giftTitle = "\(gift.title) #\(presentationStringsFormattedNumber(gift.number, presentationData.dateTimeFormat.groupingSeparator))"
let giftTitle = "\(gift.title) #\(formatCollectibleNumber(gift.number, dateTimeFormat: presentationData.dateTimeFormat))"
let reference = arguments.reference ?? .slug(slug: gift.slug)
if let resellStars = gift.resellAmounts?.first, resellStars.amount.value > 0, !update {
@ -1113,6 +1129,7 @@ private final class GiftViewSheetContent: CombinedComponent {
if animated {
controller.dismissAllTooltips()
controller.dismissBalanceOverlay()
controller.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.3).withUserData(ViewControllerComponentContainer.AnimateOutTransition()))
self.animateOut.invoke(Action { [weak controller] _ in
controller?.dismiss(completion: nil)
})
@ -1154,9 +1171,6 @@ private final class GiftViewSheetContent: CombinedComponent {
}
func requestUpgradePreview() {
guard let arguments = self.subject.arguments, arguments.canUpgrade || arguments.upgradeStars != nil else {
return
}
self.context.starsContext?.load(force: false)
self.inUpgradePreview = true
@ -1202,7 +1216,7 @@ private final class GiftViewSheetContent: CombinedComponent {
return
}
let giftTitle = "\(uniqueGift.title) #\(uniqueGift.number)"
let giftTitle = "\(uniqueGift.title) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: presentationData.dateTimeFormat))"
let recipientPeerId = self.recipientPeerId ?? self.context.account.peerId
let action: (CurrencyAmount.Currency) -> Void = { currency in
@ -1485,9 +1499,9 @@ private final class GiftViewSheetContent: CombinedComponent {
guard let arguments = self.subject.arguments, let peerId = arguments.peerId, let starsContext = self.context.starsContext, let starsState = starsContext.currentState else {
return
}
let proceed: (Int64?) -> Void = { formId in
guard let controller = self.getController() as? GiftViewScreen else {
let proceed: (Int64?) -> Void = { [weak self] formId in
guard let self, let controller = self.getController() as? GiftViewScreen else {
return
}
self.inProgress = true
@ -1528,6 +1542,11 @@ private final class GiftViewSheetContent: CombinedComponent {
self.inProgress = false
self.inUpgradePreview = false
if let reference = arguments.reference {
controller.upgradedGiftReferences.insert(reference)
self.nextGiftToUpgrade = controller.nextUpgradableGift
}
self.subject = .profileGift(peerId, result)
controller.animateSuccess()
self.updated(transition: .spring(duration: 0.4))
@ -1579,6 +1598,100 @@ private final class GiftViewSheetContent: CombinedComponent {
controller.showBalance = true
}
}
func commitPrepaidUpgrade() {
guard let arguments = self.subject.arguments, let peerId = arguments.peerId, let prepaidUpgradeHash = arguments.prepaidUpgradeHash, let starsContext = self.context.starsContext, let starsState = starsContext.currentState else {
return
}
guard case let .generic(gift) = arguments.gift else {
return
}
guard let gift = self.starGiftsMap[gift.id], let price = gift.upgradeStars else {
return
}
let context = self.context
let proceed: () -> Void = { [weak self, weak starsContext] in
guard let self else {
return
}
self.inProgress = true
self.updated()
let source: BotPaymentInvoiceSource = .starGiftPrepaidUpgrade(peerId: peerId, hash: prepaidUpgradeHash)
let signal = context.engine.payments.fetchBotPaymentForm(source: source, themeParams: nil)
|> map(Optional.init)
|> `catch` { _ in
return .single(nil)
}
|> mapToSignal { paymentForm in
if let paymentForm {
return context.engine.payments.sendStarsPaymentForm(formId: paymentForm.id, source: source)
} else {
return .fail(.generic)
}
}
self.upgradeDisposable = (signal
|> deliverOnMainQueue).start(next: { [weak self, weak starsContext] result in
guard let self else {
return
}
self.dismiss(animated: true)
Queue.mainQueue().after(0.5) {
starsContext?.load(force: true)
}
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let self, let peer else {
return
}
self.openPeer(peer, gifts: false, dismiss: true)
})
}, error: { _ in
})
}
if starsState.balance < StarsAmount(value: price, nanos: 0) {
let _ = (self.optionsPromise.get()
|> filter { $0 != nil }
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { [weak self] options in
guard let self, let controller = self.getController() else {
return
}
let purchaseController = self.context.sharedContext.makeStarsPurchaseScreen(
context: self.context,
starsContext: starsContext,
options: options ?? [],
purpose: .upgradeStarGift(requiredStars: price),
completion: { [weak self, weak starsContext] stars in
guard let self, let starsContext else {
return
}
self.inProgress = true
self.updated()
starsContext.add(balance: StarsAmount(value: stars, nanos: 0))
let _ = (starsContext.onUpdate
|> deliverOnMainQueue).start(next: {
proceed()
})
}
)
controller.push(purchaseController)
})
} else {
proceed()
}
}
func switchToNextUpgradable() {
if let controller = self.getController() as? GiftViewScreen {
controller.switchToNextUpgradable()
}
}
}
func makeState() -> State {
@ -1611,6 +1724,7 @@ private final class GiftViewSheetContent: CombinedComponent {
let table = Child(TableComponent.self)
let additionalText = Child(MultilineTextComponent.self)
let button = Child(ButtonComponent.self)
let upgradeNextButton = Child(PlainButtonComponent.self)
let upgradeTitle = Child(MultilineTextComponent.self)
let upgradeDescription = Child(BalancedTextComponent.self)
@ -1661,6 +1775,7 @@ private final class GiftViewSheetContent: CombinedComponent {
var isChannelGift = false
var isMyUniqueGift = false
var releasedByPeer: EnginePeer?
var canGiftUpgrade = false
if case let .soldOutGift(gift) = subject {
animationFile = gift.file
@ -1703,13 +1818,14 @@ private final class GiftViewSheetContent: CombinedComponent {
uniqueGift = gift
}
savedToProfile = arguments.savedToProfile
if let reference = arguments.reference, case .peer = reference {
if let reference = arguments.reference, case let .peer(peerId, _) = reference, peerId.namespace == Namespaces.Peer.CloudChannel {
isChannelGift = true
incoming = true
} else {
incoming = arguments.incoming || arguments.peerId == component.context.account.peerId
}
nameHidden = arguments.nameHidden
canGiftUpgrade = arguments.prepaidUpgradeHash != nil
isSelfGift = arguments.messageId?.peerId == component.context.account.peerId
@ -1735,7 +1851,7 @@ private final class GiftViewSheetContent: CombinedComponent {
if !canUpgrade, let gift = state.starGiftsMap[giftId], let _ = gift.upgradeStars {
canUpgrade = true
}
var showUpgradePreview = false
if state.inUpgradePreview, let _ = state.sampleGiftAttributes {
showUpgradePreview = true
@ -1837,8 +1953,7 @@ private final class GiftViewSheetContent: CombinedComponent {
if case .wearPreview = component.subject {
giftTitle = uniqueGift.title
} else {
giftTitle = "\(uniqueGift.title) #\(presentationStringsFormattedNumber(uniqueGift.number, environment.dateTimeFormat.groupingSeparator))"
giftTitle = "\(uniqueGift.title) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: environment.dateTimeFormat))"
}
let wearTitle = wearTitle.update(
@ -2042,7 +2157,14 @@ private final class GiftViewSheetContent: CombinedComponent {
let uniqueText: String
let transferableText: String
let tradableText: String
if case let .upgradePreview(_, name) = component.subject {
if !incoming, case let .profileGift(peerId, _) = subject, let peer = state.peerMap[peerId] {
let name = peer.compactDisplayTitle
title = environment.strings.Gift_Upgrade_GiftTitle
description = environment.strings.Gift_Upgrade_GiftDescription(name).string
uniqueText = strings.Gift_Upgrade_Unique_GiftDescription(name).string
transferableText = strings.Gift_Upgrade_Transferable_GiftDescription(name).string
tradableText = strings.Gift_Upgrade_Tradable_GiftDescription(name).string
} else if case let .upgradePreview(_, name) = component.subject {
title = environment.strings.Gift_Upgrade_IncludeTitle
description = environment.strings.Gift_Upgrade_IncludeDescription(name).string
uniqueText = strings.Gift_Upgrade_Unique_IncludeDescription
@ -2166,6 +2288,7 @@ private final class GiftViewSheetContent: CombinedComponent {
if case .upgradePreview = component.subject {
} else if !incoming {
} else {
let checkTheme = CheckComponent.Theme(
backgroundColor: theme.list.itemCheckColors.fillColor,
@ -2222,10 +2345,10 @@ private final class GiftViewSheetContent: CombinedComponent {
var hasDescriptionButton = false
if let uniqueGift {
titleString = uniqueGift.title
descriptionText = "\(strings.Gift_Unique_Collectible) #\(presentationStringsFormattedNumber(uniqueGift.number, environment.dateTimeFormat.groupingSeparator))"
descriptionText = "\(strings.Gift_Unique_Collectible) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: environment.dateTimeFormat))"
if let releasedBy = uniqueGift.releasedBy, let peer = state.peerMap[releasedBy], let addressName = peer.addressName {
descriptionText = strings.Gift_Unique_CollectibleBy("#\(presentationStringsFormattedNumber(uniqueGift.number, environment.dateTimeFormat.groupingSeparator))", "[@\(addressName)]()").string
descriptionText = strings.Gift_Unique_CollectibleBy("#\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: environment.dateTimeFormat))", "[@\(addressName)]()").string
hasDescriptionButton = true
releasedByPeer = peer
}
@ -2775,7 +2898,7 @@ private final class GiftViewSheetContent: CombinedComponent {
if isWearing {
state.commitTakeOff()
state.showAttributeInfo(tag: state.statusTag, text: strings.Gift_View_TookOff("\(uniqueGift.title) #\(presentationStringsFormattedNumber(uniqueGift.number, environment.dateTimeFormat.groupingSeparator))").string)
state.showAttributeInfo(tag: state.statusTag, text: strings.Gift_View_TookOff("\(uniqueGift.title) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: environment.dateTimeFormat))").string)
} else {
if let controller = controller() as? GiftViewScreen {
controller.dismissAllTooltips()
@ -2802,7 +2925,7 @@ private final class GiftViewSheetContent: CombinedComponent {
state.requestWearPreview()
} else {
state.commitWear(uniqueGift)
state.showAttributeInfo(tag: state.statusTag, text: strings.Gift_View_PutOn("\(uniqueGift.title) #\(presentationStringsFormattedNumber(uniqueGift.number, environment.dateTimeFormat.groupingSeparator))").string)
state.showAttributeInfo(tag: state.statusTag, text: strings.Gift_View_PutOn("\(uniqueGift.title) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: environment.dateTimeFormat))").string)
}
})
}
@ -3006,6 +3129,36 @@ private final class GiftViewSheetContent: CombinedComponent {
MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_Unique_Issued("\(issuedString)/\(totalString)").string, font: tableFont, textColor: tableTextColor)))
)
), at: hasOriginalInfo ? tableItems.count - 1 : tableItems.count)
//TODO:localize
if let valueAmount = uniqueGift.valueAmount, let valueCurrency = uniqueGift.valueCurrency {
tableItems.insert(.init(
id: "fiatValue",
title: "Value",
component: AnyComponent(
HStack([
AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: "\(formatCurrencyAmount(valueAmount, currency: valueCurrency))", font: tableFont, textColor: tableTextColor))))
),
AnyComponentWithIdentity(
id: AnyHashable(1),
component: AnyComponent(Button(
content: AnyComponent(ButtonContentComponent(
context: component.context,
text: "learn more",
color: theme.list.itemAccentColor
)),
action: { [weak state] in
state?.openValue()
}
))
)
], spacing: 4.0)
),
insets: UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 12.0)
), at: hasOriginalInfo ? tableItems.count - 1 : tableItems.count)
}
} else {
if case let .soldOutGift(gift) = subject, let soldOut = gift.soldOut {
tableItems.append(.init(
@ -3129,23 +3282,16 @@ private final class GiftViewSheetContent: CombinedComponent {
}
if !soldOut && canUpgrade {
var items: [AnyComponentWithIdentity<Empty>] = []
items.append(
AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_View_Status_NonUnique, font: tableFont, textColor: tableTextColor))))
)
)
tableItems.append(.init(
id: "status",
title: strings.Gift_View_Status,
component: AnyComponent(
HStack(items, spacing: 4.0)
MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_View_Status_NonUnique, font: tableFont, textColor: tableTextColor)))
),
insets: UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 12.0)
))
}
if let text {
let attributedText = stringWithAppliedEntities(text, entities: entities ?? [], baseColor: tableTextColor, linkColor: tableLinkColor, baseFont: tableFont, linkFont: tableFont, boldFont: tableBoldFont, italicFont: tableItalicFont, boldItalicFont: tableBoldItalicFont, fixedFont: tableMonospaceFont, blockQuoteFont: tableFont, message: nil)
@ -3436,7 +3582,7 @@ private final class GiftViewSheetContent: CombinedComponent {
state.dismiss(animated: true)
} else {
Queue.mainQueue().after(0.2) {
state.showAttributeInfo(tag: state.statusTag, text: strings.Gift_View_PutOn("\(uniqueGift.title) #\(presentationStringsFormattedNumber(uniqueGift.number, environment.dateTimeFormat.groupingSeparator))").string)
state.showAttributeInfo(tag: state.statusTag, text: strings.Gift_View_PutOn("\(uniqueGift.title) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: environment.dateTimeFormat))").string)
}
}
}
@ -3450,7 +3596,12 @@ private final class GiftViewSheetContent: CombinedComponent {
state.cachedStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: theme.list.itemCheckColors.foregroundColor)!, theme)
}
var upgradeString = strings.Gift_Upgrade_Upgrade
if let upgradeForm = state.upgradeForm, let price = upgradeForm.invoice.prices.first?.amount {
if !incoming {
//TODO:localize
if let gift = state.starGiftsMap[giftId], let upgradeStars = gift.upgradeStars {
upgradeString = "Pay # \(upgradeStars) for Upgrade"
}
} else if let upgradeForm = state.upgradeForm, let price = upgradeForm.invoice.prices.first?.amount {
upgradeString += " # \(presentationStringsFormattedNumber(Int32(price), environment.dateTimeFormat.groupingSeparator))"
}
let buttonTitle = subject.arguments?.upgradeStars != nil ? strings.Gift_Upgrade_Confirm : upgradeString
@ -3471,7 +3622,11 @@ private final class GiftViewSheetContent: CombinedComponent {
isEnabled: true,
displaysProgress: state.inProgress,
action: { [weak state] in
state?.commitUpgrade()
if canGiftUpgrade {
state?.commitPrepaidUpgrade()
} else {
state?.commitUpgrade()
}
}),
availableSize: buttonSize,
transition: context.transition
@ -3495,9 +3650,12 @@ private final class GiftViewSheetContent: CombinedComponent {
availableSize: buttonSize,
transition: context.transition
)
} else if incoming && !converted && !upgraded && canUpgrade {
} else if (incoming && !converted && !upgraded && canUpgrade) || canGiftUpgrade {
let buttonTitle: String
if let upgradeStars, upgradeStars > 0 {
if canGiftUpgrade {
//TODO:localize
buttonTitle = "Gift an Upgrade"
} else if let upgradeStars, upgradeStars > 0 {
buttonTitle = strings.Gift_View_UpgradeForFree
} else {
buttonTitle = strings.Gift_View_Upgrade
@ -3641,9 +3799,50 @@ private final class GiftViewSheetContent: CombinedComponent {
)
}
let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: originY), size: buttonChild.size)
var buttonAlpha: CGFloat = 1.0
if let nextGiftToUpgrade = state.nextGiftToUpgrade, case let .generic(gift) = nextGiftToUpgrade.gift {
buttonAlpha = 0.0
let upgradeNextButton = upgradeNextButton.update(
component: PlainButtonComponent(
content: AnyComponent(
HStack([
AnyComponentWithIdentity(id: "label", component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: "Upgrade Next Gift", font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor)))
)),
AnyComponentWithIdentity(id: "icon", component: AnyComponent(
GiftItemComponent(
context: component.context,
theme: theme,
strings: strings,
peer: nil,
subject: .starGift(gift: gift, price: ""),
mode: .buttonIcon
)
)),
], spacing: 5.0)
),
action: { [weak state] in
state?.switchToNextUpgradable()
},
animateScale: false
),
environment: {},
availableSize: buttonChild.size,
transition: .immediate
)
context.add(upgradeNextButton
.position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY))
.appear(.default(scale: true, alpha: true))
.disappear(.default(scale: true, alpha: true))
)
}
context.add(buttonChild
.position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY))
.cornerRadius(10.0)
.opacity(buttonAlpha)
)
originY += buttonChild.size.height
originY += 7.0
@ -3703,6 +3902,7 @@ final class GiftViewSheetComponent: CombinedComponent {
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
followContentSizeChanges: true,
clipsContent: true,
hasDimView: false,
externalState: sheetExternalState,
animateOut: animateOut,
onPan: {
@ -3713,6 +3913,7 @@ final class GiftViewSheetComponent: CombinedComponent {
willDismiss: {
if let controller = controller() as? GiftViewScreen {
controller.dismissBalanceOverlay()
controller.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.3).withUserData(ViewControllerComponentContainer.AnimateOutTransition()))
}
}
),
@ -3726,6 +3927,7 @@ final class GiftViewSheetComponent: CombinedComponent {
dismiss: { animated in
if animated {
if let controller = controller() as? GiftViewScreen {
controller.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.3).withUserData(ViewControllerComponentContainer.AnimateOutTransition()))
controller.dismissAllTooltips()
controller.dismissBalanceOverlay()
animateOut.invoke(Action { _ in
@ -3787,19 +3989,19 @@ public class GiftViewScreen: ViewControllerComponentContainer {
case upgradePreview([StarGift.UniqueGift.Attribute], String)
case wearPreview(StarGift.UniqueGift)
var arguments: (peerId: EnginePeer.Id?, fromPeerId: EnginePeer.Id?, fromPeerName: String?, messageId: EngineMessage.Id?, reference: StarGiftReference?, incoming: Bool, gift: StarGift, date: Int32, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, pinnedToTop: Bool?, converted: Bool, upgraded: Bool, refunded: Bool, canUpgrade: Bool, upgradeStars: Int64?, transferStars: Int64?, resellAmounts: [CurrencyAmount]?, canExportDate: Int32?, upgradeMessageId: Int32?, canTransferDate: Int32?, canResaleDate: Int32?)? {
var arguments: (peerId: EnginePeer.Id?, fromPeerId: EnginePeer.Id?, fromPeerName: String?, messageId: EngineMessage.Id?, reference: StarGiftReference?, incoming: Bool, gift: StarGift, date: Int32, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, pinnedToTop: Bool?, converted: Bool, upgraded: Bool, refunded: Bool, canUpgrade: Bool, upgradeStars: Int64?, transferStars: Int64?, resellAmounts: [CurrencyAmount]?, canExportDate: Int32?, upgradeMessageId: Int32?, canTransferDate: Int32?, canResaleDate: Int32?, prepaidUpgradeHash: String?)? {
switch self {
case let .message(message):
if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction {
switch action.action {
case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, isRefunded, _, upgradeMessageId, peerId, senderId, savedId):
case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, isRefunded, _, upgradeMessageId, peerId, senderId, savedId, prepaidUpgradeHash):
var reference: StarGiftReference
if let peerId, let savedId {
reference = .peer(peerId: peerId, id: savedId)
} else {
reference = .message(messageId: message.id)
}
return (message.id.peerId, senderId ?? message.author?.id, message.author?.compactDisplayTitle, message.id, reference, message.flags.contains(.Incoming), gift, message.timestamp, convertStars, text, entities, nameHidden, savedToProfile, nil, converted, upgraded, isRefunded, canUpgrade, upgradeStars, nil, nil, nil, upgradeMessageId, nil, nil)
return (message.id.peerId, senderId ?? message.author?.id, message.author?.compactDisplayTitle, message.id, reference, message.flags.contains(.Incoming), gift, message.timestamp, convertStars, text, entities, nameHidden, savedToProfile, nil, converted, upgraded, isRefunded, canUpgrade, upgradeStars, nil, nil, nil, upgradeMessageId, nil, nil, prepaidUpgradeHash)
case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, _, _, peerId, senderId, savedId, _, canTransferDate, canResaleDate):
var reference: StarGiftReference
if let peerId, let savedId {
@ -3824,13 +4026,13 @@ public class GiftViewScreen: ViewControllerComponentContainer {
if case let .unique(uniqueGift) = gift {
resellAmounts = uniqueGift.resellAmounts
}
return (message.id.peerId, senderId ?? message.author?.id, message.author?.compactDisplayTitle, message.id, reference, incoming, gift, message.timestamp, nil, nil, nil, false, savedToProfile, nil, false, false, false, false, nil, transferStars, resellAmounts, canExportDate, nil, canTransferDate, canResaleDate)
return (message.id.peerId, senderId ?? message.author?.id, message.author?.compactDisplayTitle, message.id, reference, incoming, gift, message.timestamp, nil, nil, nil, false, savedToProfile, nil, false, false, false, false, nil, transferStars, resellAmounts, canExportDate, nil, canTransferDate, canResaleDate, nil)
default:
return nil
}
}
case let .uniqueGift(gift, _), let .wearPreview(gift):
return (nil, nil, nil, nil, nil, false, .unique(gift), 0, nil, nil, nil, false, false, nil, false, false, false, false, nil, nil, gift.resellAmounts, nil, nil, nil, nil)
return (nil, nil, nil, nil, nil, false, .unique(gift), 0, nil, nil, nil, false, false, nil, false, false, false, false, nil, nil, gift.resellAmounts, nil, nil, nil, nil, nil)
case let .profileGift(peerId, gift):
var messageId: EngineMessage.Id?
if case let .message(messageIdValue) = gift.reference {
@ -3840,7 +4042,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
if case let .unique(uniqueGift) = gift.gift {
resellAmounts = uniqueGift.resellAmounts
}
return (peerId, gift.fromPeer?.id, gift.fromPeer?.compactDisplayTitle, messageId, gift.reference, false, gift.gift, gift.date, gift.convertStars, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, gift.pinnedToTop, false, false, false, gift.canUpgrade, gift.upgradeStars, gift.transferStars, resellAmounts, gift.canExportDate, nil, gift.canTransferDate, gift.canResaleDate)
return (peerId, gift.fromPeer?.id, gift.fromPeer?.compactDisplayTitle, messageId, gift.reference, false, gift.gift, gift.date, gift.convertStars, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, gift.pinnedToTop, false, false, false, gift.canUpgrade, gift.upgradeStars, gift.transferStars, resellAmounts, gift.canExportDate, nil, gift.canTransferDate, gift.canResaleDate, gift.prepaidUpgradeHash)
case .soldOutGift:
return nil
case .upgradePreview:
@ -3853,6 +4055,24 @@ public class GiftViewScreen: ViewControllerComponentContainer {
private let context: AccountContext
private let subject: GiftViewScreen.Subject
private var upgradableGiftsContext: ProfileGiftsContext?
fileprivate private(set) var upgradableGifts: [ProfileGiftsContext.State.StarGift]?
fileprivate var upgradedGiftReferences = Set<StarGiftReference>()
private var upgradableDisposable: Disposable?
fileprivate var nextUpgradableGift: ProfileGiftsContext.State.StarGift? {
if let upgradableGifts = self.upgradableGifts {
return upgradableGifts.first(where: { gift in
if let reference = gift.reference {
if !self.upgradedGiftReferences.contains(reference) {
return true
}
}
return false
})
}
return nil
}
fileprivate var showBalance = false {
didSet {
self.requestLayout(transition: .immediate)
@ -3910,7 +4130,13 @@ public class GiftViewScreen: ViewControllerComponentContainer {
if let allSubjects, !allSubjects.isEmpty {
items.removeAll()
for i in 0 ..< allSubjects.count {
items.append(GiftPagerComponent.Item(id: i, subject: allSubjects[i]))
var id: AnyHashable
if case let .profileGift(_, starGift) = allSubjects[i], let reference = starGift.reference {
id = reference.stringValue
} else {
id = i
}
items.append(GiftPagerComponent.Item(id: id, subject: allSubjects[i]))
}
}
var dismissTooltipsImpl: (() -> Void)?
@ -3920,6 +4146,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
context: context,
items: items,
index: index ?? 0,
itemSpacing: 10.0,
updated: { _, _ in
dismissTooltipsImpl?()
}
@ -3934,6 +4161,18 @@ public class GiftViewScreen: ViewControllerComponentContainer {
self.navigationPresentation = .flatModal
self.automaticallyControlPresentationContextLayout = false
if "".isEmpty {
let upgradableGiftsContext = ProfileGiftsContext(account: context.account, peerId: context.account.peerId, collectionId: nil, sorting: .date, filter: [.displayed, .hidden, .limitedUpgradable])
self.upgradableDisposable = (upgradableGiftsContext.state
|> deliverOnMainQueue).start(next: { [weak self] state in
guard let self else {
return
}
self.upgradableGifts = state.filteredGifts
})
self.upgradableGiftsContext = upgradableGiftsContext
}
}
required public init(coder aDecoder: NSCoder) {
@ -3942,6 +4181,50 @@ public class GiftViewScreen: ViewControllerComponentContainer {
deinit {
self.disposed()
self.upgradableDisposable?.dispose()
}
fileprivate func switchToNextUpgradable() {
guard let upgradableGifts = self.upgradableGifts, case let .profileGift(peerId, _) = self.subject else {
return
}
var effectiveUpgradableGifts: [ProfileGiftsContext.State.StarGift] = []
for gift in upgradableGifts {
if let reference = gift.reference {
if !self.upgradedGiftReferences.contains(reference) {
effectiveUpgradableGifts.append(gift)
}
}
}
guard !effectiveUpgradableGifts.isEmpty else {
return
}
var items: [GiftPagerComponent.Item] = []
for i in 0 ..< effectiveUpgradableGifts.count {
let gift = effectiveUpgradableGifts[i]
var id: AnyHashable
if let reference = gift.reference {
id = reference.stringValue
} else {
id = i
}
items.append(GiftPagerComponent.Item(id: id, subject: .profileGift(peerId, gift)))
}
self.updateComponent(
component: AnyComponent(GiftPagerComponent(
context: self.context,
items: items,
index: 0,
itemSpacing: 10.0,
updated: { [weak self] _, _ in
self?.dismissAllTooltips()
}
)),
transition: .spring(duration: 0.3)
)
}
public override func viewDidLoad() {
@ -4081,12 +4364,10 @@ public class GiftViewScreen: ViewControllerComponentContainer {
}
}
private func formatPercentage(_ value: Float) -> String {
func formatPercentage(_ value: Float) -> String {
return String(format: "%0.1f%%", value).replacingOccurrences(of: ".0%", with: "%").replacingOccurrences(of: ",0%", with: "%")
}
private final class PeerCellComponent: Component {
let context: AccountContext
let theme: PresentationTheme
@ -4193,12 +4474,12 @@ private final class PeerCellComponent: Component {
}
}
private final class ButtonContentComponent: Component {
final class ButtonContentComponent: Component {
let context: AccountContext
let text: String
let color: UIColor
public init(
init(
context: AccountContext,
text: String,
color: UIColor
@ -4208,7 +4489,7 @@ private final class ButtonContentComponent: Component {
self.color = color
}
public static func ==(lhs: ButtonContentComponent, rhs: ButtonContentComponent) -> Bool {
static func ==(lhs: ButtonContentComponent, rhs: ButtonContentComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
@ -4221,7 +4502,7 @@ private final class ButtonContentComponent: Component {
return true
}
public final class View: UIView {
final class View: UIView {
private var component: ButtonContentComponent?
private weak var componentState: EmptyComponentState?
@ -4279,11 +4560,11 @@ private final class ButtonContentComponent: Component {
}
}
public func makeView() -> View {
func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@ -4693,3 +4974,16 @@ private struct GiftViewConfiguration {
}
}
}
private extension StarGiftReference {
var stringValue: String {
switch self {
case let .message(messageId):
return "m_\(messageId.id)"
case let .peer(peerId, id):
return "p_\(peerId.toInt64())_\(id)"
case let .slug(slug):
return "s_\(slug)"
}
}
}

View file

@ -7,6 +7,7 @@ import SwiftSignalKit
import ViewControllerComponent
import ComponentDisplayAdapters
import TelegramPresentationData
import TelegramStringFormatting
import AccountContext
import TelegramCore
import MultilineTextComponent
@ -120,7 +121,7 @@ private final class GiftContextPreviewComponent: Component {
let subtitleSize = self.subtitle.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(text: .plain(
NSAttributedString(string: "\(environment.strings.Gift_Unique_Collectible) #\(presentationStringsFormattedNumber(uniqueGift.number, environment.dateTimeFormat.groupingSeparator))", font: Font.regular(13.0), textColor: vibrantColor)
NSAttributedString(string: "\(environment.strings.Gift_Unique_Collectible) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: environment.dateTimeFormat))", font: Font.regular(13.0), textColor: vibrantColor)
))),
environment: {},
containerSize: availableSize

View file

@ -656,7 +656,7 @@ final class GiftsListView: UIView {
var title = ""
if case let .unique(uniqueGift) = product.gift {
title = "\(uniqueGift.title) #\(presentationStringsFormattedNumber(uniqueGift.number, params.presentationData.dateTimeFormat.groupingSeparator))"
title = "\(uniqueGift.title) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: params.presentationData.dateTimeFormat))"
}
if pinnedToTop {

View file

@ -383,7 +383,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
var replacingTitle = ""
for gift in pinnedGifts {
if gift.reference == unpinnedReference, case let .unique(uniqueGift) = gift.gift {
replacingTitle = "\(uniqueGift.title) #\(presentationStringsFormattedNumber(uniqueGift.number, presentationData.dateTimeFormat.groupingSeparator))"
replacingTitle = "\(uniqueGift.title) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: presentationData.dateTimeFormat))"
}
}
@ -395,7 +395,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
var title = ""
if case let .unique(uniqueGift) = gift.gift {
title = "\(uniqueGift.title) #\(presentationStringsFormattedNumber(uniqueGift.number, presentationData.dateTimeFormat.groupingSeparator))"
title = "\(uniqueGift.title) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: presentationData.dateTimeFormat))"
}
let _ = self.scrollToTop()
@ -1058,7 +1058,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
case let .generic(gift):
giftFile = gift.file
case let .unique(uniqueGift):
giftTitle = uniqueGift.title + " #\(presentationStringsFormattedNumber(uniqueGift.number, currentParams.presentationData.dateTimeFormat.groupingSeparator))"
giftTitle = uniqueGift.title + " #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: currentParams.presentationData.dateTimeFormat))"
for attribute in uniqueGift.attributes {
if case let .model(_, file, _) = attribute {
giftFile = file
@ -1126,7 +1126,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
} else {
var title = ""
if case let .unique(uniqueGift) = gift.gift {
title = "\(uniqueGift.title) #\(presentationStringsFormattedNumber(uniqueGift.number, presentationData.dateTimeFormat.groupingSeparator))"
title = "\(uniqueGift.title) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: presentationData.dateTimeFormat))"
}
toastTitle = strings.PeerInfo_Gifts_ToastPinned_TitleNew(title).string
toastText = strings.PeerInfo_Gifts_ToastPinned_Text
@ -1378,7 +1378,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
case let .generic(gift):
giftFile = gift.file
case let .unique(uniqueGift):
giftTitle = uniqueGift.title + " #\(presentationStringsFormattedNumber(uniqueGift.number, currentParams.presentationData.dateTimeFormat.groupingSeparator))"
giftTitle = uniqueGift.title + " #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: currentParams.presentationData.dateTimeFormat))"
for attribute in uniqueGift.attributes {
if case let .model(_, file, _) = attribute {
giftFile = file

View file

@ -449,6 +449,7 @@ final class UserAppearanceScreenComponent: Component {
if let file = self.cachedIconFiles[fileId], let patternFile = self.cachedIconFiles[patternFileId], let numberString = slugComponents.last, let number = Int32(numberString) {
let gift = StarGift.UniqueGift(
id: id,
giftId: 0,
title: title,
number: number,
slug: slug,
@ -462,7 +463,9 @@ final class UserAppearanceScreenComponent: Component {
giftAddress: nil,
resellAmounts: nil,
resellForTonOnly: false,
releasedBy: nil
releasedBy: nil,
valueAmount: nil,
valueCurrency: nil
)
signal = component.context.engine.accountData.setStarGiftStatus(starGift: gift, expirationDate: emojiStatus.expirationDate)
} else {

View file

@ -499,6 +499,13 @@ public final class TabSelectorComponent: Component {
self.setContentOffset(CGPoint(x: self.contentSize.width - self.bounds.width, y: 0.0), animated: true)
}
public func frameForItem(_ id: AnyHashable) -> CGRect? {
if let item = self.visibleItems[id] {
return item.convert(item.bounds, to: self)
}
return nil
}
func update(component: TabSelectorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let selectionColorUpdated = component.colors.selection != self.component?.colors.selection