diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 47b65d1f2c..1a2573c803 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -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"; diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequencePaymentScreen.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequencePaymentScreen.swift index e410007a36..963e68e5ab 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequencePaymentScreen.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequencePaymentScreen.swift @@ -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) } }) diff --git a/submodules/Components/SheetComponent/Sources/SheetComponent.swift b/submodules/Components/SheetComponent/Sources/SheetComponent.swift index 28aa486e23..ba85ac254c 100644 --- a/submodules/Components/SheetComponent/Sources/SheetComponent.swift +++ b/submodules/Components/SheetComponent/Sources/SheetComponent.swift @@ -64,6 +64,7 @@ public final class SheetComponent: 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> public let onPan: () -> Void @@ -75,6 +76,7 @@ public final class SheetComponent: C followContentSizeChanges: Bool = false, clipsContent: Bool = false, isScrollEnabled: Bool = true, + hasDimView: Bool = true, externalState: ExternalState? = nil, animateOut: ActionSlot>, onPan: @escaping () -> Void = {}, @@ -85,6 +87,7 @@ public final class SheetComponent: 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: 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: 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) { diff --git a/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift b/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift index 4eeac4b0c1..bd1bfaceac 100644 --- a/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift +++ b/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift @@ -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, transition: ComponentTransition) { self.component = component self.node.updateComponent(component: component, transition: transition) diff --git a/submodules/GalleryUI/Sources/Items/VideoAdComponent.swift b/submodules/GalleryUI/Sources/Items/VideoAdComponent.swift index ea84451943..0f47b793bc 100644 --- a/submodules/GalleryUI/Sources/Items/VideoAdComponent.swift +++ b/submodules/GalleryUI/Sources/Items/VideoAdComponent.swift @@ -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: {}, diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift index e44e98cbd7..7b1af50f2f 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift @@ -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 diff --git a/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift b/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift index c8e63e28ad..778fd37a4c 100644 --- a/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift +++ b/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift @@ -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)) diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift index 0c098f7f2b..2dc3b1aa31 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift @@ -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) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift index 10403f2139..84d4731c18 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift @@ -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 Signal { + return account.network.request(Api.functions.payments.getUniqueStarGiftValueInfo(slug: slug)) + |> map(Optional.init) + |> `catch` { _ -> Signal 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 diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift index 4723234c4c..dc4e9f10e7 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift @@ -145,6 +145,10 @@ public extension TelegramEngine { return _internal_getUniqueStarGift(account: self.account, slug: slug) } + public func getUniqueStarGiftValueInfo(slug: String) -> Signal { + return _internal_getUniqueStarGiftValueInfo(account: self.account, slug: slug) + } + public func checkStarGiftWithdrawalAvailability(reference: StarGiftReference) -> Signal { return _internal_checkStarGiftWithdrawalAvailability(account: self.account, reference: reference) } diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index 4c4970e4bf..b27610f213 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -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 { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift index 9192ee2363..de1a60305f 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift @@ -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}") diff --git a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift index 6af3bfcb3f..6881c00ac3 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift @@ -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 diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift index 04e89a3152..94f5733f4b 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift @@ -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) ) ] } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftPagerComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftPagerComponent.swift index 83def8af34..e3fd5656f3 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftPagerComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftPagerComponent.swift @@ -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] = [:] @@ -71,7 +79,11 @@ final class GiftPagerComponent: Component { private var environment: Environment? 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, 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 } } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftPurchaseAlertController.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftPurchaseAlertController.swift index aea26bab1a..2566964cc3 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftPurchaseAlertController.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftPurchaseAlertController.swift @@ -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) }) diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftValueScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftValueScreen.swift new file mode 100644 index 0000000000..2ea3a3654f --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftValueScreen.swift @@ -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> + let getController: () -> ViewController? + + init( + context: AccountContext, + gift: ProfileGiftsContext.State.StarGift, + valueInfo: StarGift.UniqueGift.ValueInfo, + animateOut: ActionSlot>, + 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> + 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>, + 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] = [] + 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] = [] + 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] = [] + 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.self) + let animateOut = StoredActionSlot(Action.self) + + let sheetExternalState = SheetComponent.ExternalState() + + return { context in + let environment = context.environment[EnvironmentType.self] + let controller = environment.controller + + let sheet = sheet.update( + component: SheetComponent( + content: AnyComponent(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.View.Tag()) as? SheetComponent.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 + }) + } +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index c5b99f3021..9dc6c87eec 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -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] = [] - 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() + 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, transition: ComponentTransition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, 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)" + } + } +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/GiftContextPreviewController.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/GiftContextPreviewController.swift index 7c1e42f704..f65937faf1 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/GiftContextPreviewController.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/GiftContextPreviewController.swift @@ -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 diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/GiftsListView.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/GiftsListView.swift index e1d4f54e32..92eefe5c98 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/GiftsListView.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/GiftsListView.swift @@ -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 { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift index 5d3442cf5f..b24eaa07c4 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift @@ -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 diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift index 049f96e0a4..c4169c8d8e 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift @@ -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 { diff --git a/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift b/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift index 0b8ea2f845..2b75c31f42 100644 --- a/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift +++ b/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift @@ -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, transition: ComponentTransition) -> CGSize { let selectionColorUpdated = component.colors.selection != self.component?.colors.selection