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/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 6639889c56..7fa267a4cb 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -603,7 +603,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1434950843] = { return Api.MessageAction.parse_messageActionSetChatTheme($0) } dict[1348510708] = { return Api.MessageAction.parse_messageActionSetChatWallPaper($0) } dict[1007897979] = { return Api.MessageAction.parse_messageActionSetMessagesTTL($0) } - dict[1192749220] = { return Api.MessageAction.parse_messageActionStarGift($0) } + dict[-956938735] = { return Api.MessageAction.parse_messageActionStarGift($0) } dict[888627955] = { return Api.MessageAction.parse_messageActionStarGiftUnique($0) } dict[1474192222] = { return Api.MessageAction.parse_messageActionSuggestProfilePhoto($0) } dict[-293988970] = { return Api.MessageAction.parse_messageActionSuggestedPostApproval($0) } @@ -946,7 +946,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1124938064] = { return Api.SponsoredMessageReportOption.parse_sponsoredMessageReportOption($0) } dict[-963180333] = { return Api.SponsoredPeer.parse_sponsoredPeer($0) } dict[12386139] = { return Api.StarGift.parse_starGift($0) } - dict[352555441] = { return Api.StarGift.parse_starGiftUnique($0) } + dict[648369470] = { return Api.StarGift.parse_starGiftUnique($0) } dict[-650279524] = { return Api.StarGiftAttribute.parse_starGiftAttributeBackdrop($0) } dict[970559507] = { return Api.StarGiftAttribute.parse_starGiftAttributeModel($0) } dict[-524291476] = { return Api.StarGiftAttribute.parse_starGiftAttributeOriginalDetails($0) } diff --git a/submodules/TelegramApi/Sources/Api15.swift b/submodules/TelegramApi/Sources/Api15.swift index 508eff99e8..c323578fef 100644 --- a/submodules/TelegramApi/Sources/Api15.swift +++ b/submodules/TelegramApi/Sources/Api15.swift @@ -393,7 +393,7 @@ public extension Api { case messageActionSetChatTheme(emoticon: String) case messageActionSetChatWallPaper(flags: Int32, wallpaper: Api.WallPaper) case messageActionSetMessagesTTL(flags: Int32, period: Int32, autoSettingFrom: Int64?) - case messageActionStarGift(flags: Int32, gift: Api.StarGift, message: Api.TextWithEntities?, convertStars: Int64?, upgradeMsgId: Int32?, upgradeStars: Int64?, fromId: Api.Peer?, peer: Api.Peer?, savedId: Int64?) + case messageActionStarGift(flags: Int32, gift: Api.StarGift, message: Api.TextWithEntities?, convertStars: Int64?, upgradeMsgId: Int32?, upgradeStars: Int64?, fromId: Api.Peer?, peer: Api.Peer?, savedId: Int64?, prepaidUpgradeHash: String?) case messageActionStarGiftUnique(flags: Int32, gift: Api.StarGift, canExportAt: Int32?, transferStars: Int64?, fromId: Api.Peer?, peer: Api.Peer?, savedId: Int64?, resaleAmount: Api.StarsAmount?, canTransferAt: Int32?, canResellAt: Int32?) case messageActionSuggestProfilePhoto(photo: Api.Photo) case messageActionSuggestedPostApproval(flags: Int32, rejectComment: String?, scheduleDate: Int32?, price: Api.StarsAmount?) @@ -783,9 +783,9 @@ public extension Api { serializeInt32(period, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 0) != 0 {serializeInt64(autoSettingFrom!, buffer: buffer, boxed: false)} break - case .messageActionStarGift(let flags, let gift, let message, let convertStars, let upgradeMsgId, let upgradeStars, let fromId, let peer, let savedId): + case .messageActionStarGift(let flags, let gift, let message, let convertStars, let upgradeMsgId, let upgradeStars, let fromId, let peer, let savedId, let prepaidUpgradeHash): if boxed { - buffer.appendInt32(1192749220) + buffer.appendInt32(-956938735) } serializeInt32(flags, buffer: buffer, boxed: false) gift.serialize(buffer, true) @@ -796,6 +796,7 @@ public extension Api { if Int(flags) & Int(1 << 11) != 0 {fromId!.serialize(buffer, true)} if Int(flags) & Int(1 << 12) != 0 {peer!.serialize(buffer, true)} if Int(flags) & Int(1 << 12) != 0 {serializeInt64(savedId!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 14) != 0 {serializeString(prepaidUpgradeHash!, buffer: buffer, boxed: false)} break case .messageActionStarGiftUnique(let flags, let gift, let canExportAt, let transferStars, let fromId, let peer, let savedId, let resaleAmount, let canTransferAt, let canResellAt): if boxed { @@ -991,8 +992,8 @@ public extension Api { return ("messageActionSetChatWallPaper", [("flags", flags as Any), ("wallpaper", wallpaper as Any)]) case .messageActionSetMessagesTTL(let flags, let period, let autoSettingFrom): return ("messageActionSetMessagesTTL", [("flags", flags as Any), ("period", period as Any), ("autoSettingFrom", autoSettingFrom as Any)]) - case .messageActionStarGift(let flags, let gift, let message, let convertStars, let upgradeMsgId, let upgradeStars, let fromId, let peer, let savedId): - return ("messageActionStarGift", [("flags", flags as Any), ("gift", gift as Any), ("message", message as Any), ("convertStars", convertStars as Any), ("upgradeMsgId", upgradeMsgId as Any), ("upgradeStars", upgradeStars as Any), ("fromId", fromId as Any), ("peer", peer as Any), ("savedId", savedId as Any)]) + case .messageActionStarGift(let flags, let gift, let message, let convertStars, let upgradeMsgId, let upgradeStars, let fromId, let peer, let savedId, let prepaidUpgradeHash): + return ("messageActionStarGift", [("flags", flags as Any), ("gift", gift as Any), ("message", message as Any), ("convertStars", convertStars as Any), ("upgradeMsgId", upgradeMsgId as Any), ("upgradeStars", upgradeStars as Any), ("fromId", fromId as Any), ("peer", peer as Any), ("savedId", savedId as Any), ("prepaidUpgradeHash", prepaidUpgradeHash as Any)]) case .messageActionStarGiftUnique(let flags, let gift, let canExportAt, let transferStars, let fromId, let peer, let savedId, let resaleAmount, let canTransferAt, let canResellAt): return ("messageActionStarGiftUnique", [("flags", flags as Any), ("gift", gift as Any), ("canExportAt", canExportAt as Any), ("transferStars", transferStars as Any), ("fromId", fromId as Any), ("peer", peer as Any), ("savedId", savedId as Any), ("resaleAmount", resaleAmount as Any), ("canTransferAt", canTransferAt as Any), ("canResellAt", canResellAt as Any)]) case .messageActionSuggestProfilePhoto(let photo): @@ -1755,6 +1756,8 @@ public extension Api { } } var _9: Int64? if Int(_1!) & Int(1 << 12) != 0 {_9 = reader.readInt64() } + var _10: String? + if Int(_1!) & Int(1 << 14) != 0 {_10 = parseString(reader) } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil @@ -1764,8 +1767,9 @@ public extension Api { let _c7 = (Int(_1!) & Int(1 << 11) == 0) || _7 != nil let _c8 = (Int(_1!) & Int(1 << 12) == 0) || _8 != nil let _c9 = (Int(_1!) & Int(1 << 12) == 0) || _9 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { - return Api.MessageAction.messageActionStarGift(flags: _1!, gift: _2!, message: _3, convertStars: _4, upgradeMsgId: _5, upgradeStars: _6, fromId: _7, peer: _8, savedId: _9) + let _c10 = (Int(_1!) & Int(1 << 14) == 0) || _10 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 { + return Api.MessageAction.messageActionStarGift(flags: _1!, gift: _2!, message: _3, convertStars: _4, upgradeMsgId: _5, upgradeStars: _6, fromId: _7, peer: _8, savedId: _9, prepaidUpgradeHash: _10) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api24.swift b/submodules/TelegramApi/Sources/Api24.swift index e51177a0bc..3ae1450ab0 100644 --- a/submodules/TelegramApi/Sources/Api24.swift +++ b/submodules/TelegramApi/Sources/Api24.swift @@ -637,7 +637,7 @@ public extension Api { public extension Api { enum StarGift: TypeConstructorDescription { case starGift(flags: Int32, id: Int64, sticker: Api.Document, stars: Int64, availabilityRemains: Int32?, availabilityTotal: Int32?, availabilityResale: Int64?, convertStars: Int64, firstSaleDate: Int32?, lastSaleDate: Int32?, upgradeStars: Int64?, resellMinStars: Int64?, title: String?, releasedBy: Api.Peer?, perUserTotal: Int32?, perUserRemains: Int32?) - case starGiftUnique(flags: Int32, id: Int64, title: String, slug: String, num: Int32, ownerId: Api.Peer?, ownerName: String?, ownerAddress: String?, attributes: [Api.StarGiftAttribute], availabilityIssued: Int32, availabilityTotal: Int32, giftAddress: String?, resellAmount: [Api.StarsAmount]?, releasedBy: Api.Peer?, valueAmount: Int64?, valueCurrency: String?) + case starGiftUnique(flags: Int32, id: Int64, giftId: Int64, title: String, slug: String, num: Int32, ownerId: Api.Peer?, ownerName: String?, ownerAddress: String?, attributes: [Api.StarGiftAttribute], availabilityIssued: Int32, availabilityTotal: Int32, giftAddress: String?, resellAmount: [Api.StarsAmount]?, releasedBy: Api.Peer?, valueAmount: Int64?, valueCurrency: String?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -662,12 +662,13 @@ public extension Api { if Int(flags) & Int(1 << 8) != 0 {serializeInt32(perUserTotal!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 8) != 0 {serializeInt32(perUserRemains!, buffer: buffer, boxed: false)} break - case .starGiftUnique(let flags, let id, let title, let slug, let num, let ownerId, let ownerName, let ownerAddress, let attributes, let availabilityIssued, let availabilityTotal, let giftAddress, let resellAmount, let releasedBy, let valueAmount, let valueCurrency): + case .starGiftUnique(let flags, let id, let giftId, let title, let slug, let num, let ownerId, let ownerName, let ownerAddress, let attributes, let availabilityIssued, let availabilityTotal, let giftAddress, let resellAmount, let releasedBy, let valueAmount, let valueCurrency): if boxed { - buffer.appendInt32(352555441) + buffer.appendInt32(648369470) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt64(id, buffer: buffer, boxed: false) + serializeInt64(giftId, buffer: buffer, boxed: false) serializeString(title, buffer: buffer, boxed: false) serializeString(slug, buffer: buffer, boxed: false) serializeInt32(num, buffer: buffer, boxed: false) @@ -698,8 +699,8 @@ public extension Api { switch self { case .starGift(let flags, let id, let sticker, let stars, let availabilityRemains, let availabilityTotal, let availabilityResale, let convertStars, let firstSaleDate, let lastSaleDate, let upgradeStars, let resellMinStars, let title, let releasedBy, let perUserTotal, let perUserRemains): return ("starGift", [("flags", flags as Any), ("id", id as Any), ("sticker", sticker as Any), ("stars", stars as Any), ("availabilityRemains", availabilityRemains as Any), ("availabilityTotal", availabilityTotal as Any), ("availabilityResale", availabilityResale as Any), ("convertStars", convertStars as Any), ("firstSaleDate", firstSaleDate as Any), ("lastSaleDate", lastSaleDate as Any), ("upgradeStars", upgradeStars as Any), ("resellMinStars", resellMinStars as Any), ("title", title as Any), ("releasedBy", releasedBy as Any), ("perUserTotal", perUserTotal as Any), ("perUserRemains", perUserRemains as Any)]) - case .starGiftUnique(let flags, let id, let title, let slug, let num, let ownerId, let ownerName, let ownerAddress, let attributes, let availabilityIssued, let availabilityTotal, let giftAddress, let resellAmount, let releasedBy, let valueAmount, let valueCurrency): - return ("starGiftUnique", [("flags", flags as Any), ("id", id as Any), ("title", title as Any), ("slug", slug as Any), ("num", num as Any), ("ownerId", ownerId as Any), ("ownerName", ownerName as Any), ("ownerAddress", ownerAddress as Any), ("attributes", attributes as Any), ("availabilityIssued", availabilityIssued as Any), ("availabilityTotal", availabilityTotal as Any), ("giftAddress", giftAddress as Any), ("resellAmount", resellAmount as Any), ("releasedBy", releasedBy as Any), ("valueAmount", valueAmount as Any), ("valueCurrency", valueCurrency as Any)]) + case .starGiftUnique(let flags, let id, let giftId, let title, let slug, let num, let ownerId, let ownerName, let ownerAddress, let attributes, let availabilityIssued, let availabilityTotal, let giftAddress, let resellAmount, let releasedBy, let valueAmount, let valueCurrency): + return ("starGiftUnique", [("flags", flags as Any), ("id", id as Any), ("giftId", giftId as Any), ("title", title as Any), ("slug", slug as Any), ("num", num as Any), ("ownerId", ownerId as Any), ("ownerName", ownerName as Any), ("ownerAddress", ownerAddress as Any), ("attributes", attributes as Any), ("availabilityIssued", availabilityIssued as Any), ("availabilityTotal", availabilityTotal as Any), ("giftAddress", giftAddress as Any), ("resellAmount", resellAmount as Any), ("releasedBy", releasedBy as Any), ("valueAmount", valueAmount as Any), ("valueCurrency", valueCurrency as Any)]) } } @@ -768,60 +769,63 @@ public extension Api { _1 = reader.readInt32() var _2: Int64? _2 = reader.readInt64() - var _3: String? - _3 = parseString(reader) + var _3: Int64? + _3 = reader.readInt64() var _4: String? _4 = parseString(reader) - var _5: Int32? - _5 = reader.readInt32() - var _6: Api.Peer? + var _5: String? + _5 = parseString(reader) + var _6: Int32? + _6 = reader.readInt32() + var _7: Api.Peer? if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { - _6 = Api.parse(reader, signature: signature) as? Api.Peer + _7 = Api.parse(reader, signature: signature) as? Api.Peer } } - var _7: String? - if Int(_1!) & Int(1 << 1) != 0 {_7 = parseString(reader) } var _8: String? - if Int(_1!) & Int(1 << 2) != 0 {_8 = parseString(reader) } - var _9: [Api.StarGiftAttribute]? + if Int(_1!) & Int(1 << 1) != 0 {_8 = parseString(reader) } + var _9: String? + if Int(_1!) & Int(1 << 2) != 0 {_9 = parseString(reader) } + var _10: [Api.StarGiftAttribute]? if let _ = reader.readInt32() { - _9 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StarGiftAttribute.self) + _10 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StarGiftAttribute.self) } - var _10: Int32? - _10 = reader.readInt32() var _11: Int32? _11 = reader.readInt32() - var _12: String? - if Int(_1!) & Int(1 << 3) != 0 {_12 = parseString(reader) } - var _13: [Api.StarsAmount]? + var _12: Int32? + _12 = reader.readInt32() + var _13: String? + if Int(_1!) & Int(1 << 3) != 0 {_13 = parseString(reader) } + var _14: [Api.StarsAmount]? if Int(_1!) & Int(1 << 4) != 0 {if let _ = reader.readInt32() { - _13 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StarsAmount.self) + _14 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StarsAmount.self) } } - var _14: Api.Peer? + var _15: Api.Peer? if Int(_1!) & Int(1 << 5) != 0 {if let signature = reader.readInt32() { - _14 = Api.parse(reader, signature: signature) as? Api.Peer + _15 = Api.parse(reader, signature: signature) as? Api.Peer } } - var _15: Int64? - if Int(_1!) & Int(1 << 8) != 0 {_15 = reader.readInt64() } - var _16: String? - if Int(_1!) & Int(1 << 8) != 0 {_16 = parseString(reader) } + var _16: Int64? + if Int(_1!) & Int(1 << 8) != 0 {_16 = reader.readInt64() } + var _17: String? + if Int(_1!) & Int(1 << 8) != 0 {_17 = parseString(reader) } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 2) == 0) || _8 != nil - let _c9 = _9 != nil + let _c6 = _6 != nil + let _c7 = (Int(_1!) & Int(1 << 0) == 0) || _7 != nil + let _c8 = (Int(_1!) & Int(1 << 1) == 0) || _8 != nil + let _c9 = (Int(_1!) & Int(1 << 2) == 0) || _9 != nil let _c10 = _10 != nil let _c11 = _11 != nil - let _c12 = (Int(_1!) & Int(1 << 3) == 0) || _12 != nil - let _c13 = (Int(_1!) & Int(1 << 4) == 0) || _13 != nil - let _c14 = (Int(_1!) & Int(1 << 5) == 0) || _14 != nil - let _c15 = (Int(_1!) & Int(1 << 8) == 0) || _15 != nil + let _c12 = _12 != nil + let _c13 = (Int(_1!) & Int(1 << 3) == 0) || _13 != nil + let _c14 = (Int(_1!) & Int(1 << 4) == 0) || _14 != nil + let _c15 = (Int(_1!) & Int(1 << 5) == 0) || _15 != nil let _c16 = (Int(_1!) & Int(1 << 8) == 0) || _16 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 { - return Api.StarGift.starGiftUnique(flags: _1!, id: _2!, title: _3!, slug: _4!, num: _5!, ownerId: _6, ownerName: _7, ownerAddress: _8, attributes: _9!, availabilityIssued: _10!, availabilityTotal: _11!, giftAddress: _12, resellAmount: _13, releasedBy: _14, valueAmount: _15, valueCurrency: _16) + let _c17 = (Int(_1!) & Int(1 << 8) == 0) || _17 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 { + return Api.StarGift.starGiftUnique(flags: _1!, id: _2!, giftId: _3!, title: _4!, slug: _5!, num: _6!, ownerId: _7, ownerName: _8, ownerAddress: _9, attributes: _10!, availabilityIssued: _11!, availabilityTotal: _12!, giftAddress: _13, resellAmount: _14, releasedBy: _15, valueAmount: _16, valueCurrency: _17) } else { return nil diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift index 7c9a6871c2..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 b31a18c773..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, 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 b429d9e209..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 @@ -548,6 +549,7 @@ public enum StarGift: Equatable, Codable, PostboxCoding { public struct ValueInfo: Equatable { public let isLastSaleOnFragment: Bool + public let valueIsAverage: Bool public let value: Int64 public let currency: String public let initialSaleDate: Int32 @@ -567,6 +569,7 @@ public enum StarGift: Equatable, Codable, PostboxCoding { } public let id: Int64 + public let giftId: Int64 public let title: String public let number: Int32 public let slug: String @@ -580,8 +583,9 @@ public enum StarGift: Equatable, Codable, PostboxCoding { 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?, valueAmount: Int64?, valueCurrency: String?) { + 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 @@ -599,6 +603,7 @@ public enum StarGift: Equatable, Codable, PostboxCoding { 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) ?? "" @@ -629,6 +634,7 @@ public enum StarGift: Equatable, Codable, PostboxCoding { 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: "") @@ -660,6 +666,7 @@ public enum StarGift: Equatable, Codable, PostboxCoding { 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) @@ -683,6 +690,7 @@ public enum StarGift: Equatable, Codable, PostboxCoding { 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) @@ -724,6 +732,7 @@ public enum StarGift: Equatable, Codable, PostboxCoding { public func withResellAmounts(_ resellAmounts: [CurrencyAmount]?) -> UniqueGift { return UniqueGift( id: self.id, + giftId: self.giftId, title: self.title, number: self.number, slug: self.slug, @@ -742,6 +751,7 @@ public enum StarGift: Equatable, Codable, PostboxCoding { public func withResellForTonOnly(_ resellForTonOnly: Bool) -> UniqueGift { return UniqueGift( id: self.id, + giftId: self.giftId, title: self.title, number: self.number, slug: self.slug, @@ -863,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, valueAmount, valueCurrency): + 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) @@ -875,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, valueAmount: valueAmount, valueCurrency: valueCurrency)) + 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)) } } } @@ -2579,8 +2589,11 @@ func _internal_getUniqueStarGiftValueInfo(account: Account, slug: String) -> Sig 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 << 0) != 0, + isLastSaleOnFragment: flags & (1 << 1) != 0, + valueIsAverage: flags & (1 << 6) != 0, value: value, currency: currency, initialSaleDate: initialSaleDate, 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/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 58defa6352..de9497d768 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -3730,7 +3730,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } } else if case let .gift(gift) = subject { isGift = true - let media: [Media] = [TelegramMediaAction(action: .starGiftUnique(gift: .unique(gift), isUpgrade: false, isTransferred: false, savedToProfile: false, canExportDate: nil, transferStars: nil, isRefunded: false, peerId: nil, senderId: nil, savedId: nil, resaleAmount: nil, canTransferDate: nil, canResaleDate: nil))] + let media: [Media] = [TelegramMediaAction(action: .starGiftUnique(gift: .unique(gift), isUpgrade: false, isTransferred: false, savedToProfile: false, canExportDate: nil, transferStars: nil, isRefunded: false, isPrepaidUpgrade: false, peerId: nil, senderId: nil, savedId: nil, resaleAmount: nil, canTransferDate: nil, canResaleDate: nil))] let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: self.context.account.peerId, namespace: Namespaces.Message.Cloud, id: -1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: media, peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) messages = .single([message]) } else { 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 fef795c9f9..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, 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