From 3d22de75fc6d0962d6412f1f0af12132823fa191 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 25 Oct 2023 02:14:16 +0400 Subject: [PATCH] Boost improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 3 + .../Sources/AccountContext.swift | 9 +- submodules/PremiumUI/BUILD | 1 + .../Sources/CreateGiveawayController.swift | 6 +- .../Sources/PremiumBoostScreen.swift | 3 +- .../Sources/PremiumGiftCodeScreen.swift | 384 +++++++++++++----- .../Sources/ReplaceBoostScreen.swift | 53 ++- .../Sources/ChannelStatsController.swift | 82 ++-- .../Sources/State/ChannelBoost.swift | 23 +- .../TelegramUI/Sources/OpenResolvedUrl.swift | 2 +- 10 files changed, 400 insertions(+), 166 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 84d31a91ec..745f4e8845 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -10152,3 +10152,6 @@ Sorry for the inconvenience."; "Chat.ErrorQuoteOutdatedActionEdit" = "Edit"; "Premium.BoostByGiftDescription" = "Boost your channel by gifting your subscribers Telegram Premium. [Get boosts >]()"; + +"Stats.Boosts.ShowMoreBoosts_1" = "Show %@ More Boost"; +"Stats.Boosts.ShowMoreBoosts_any" = "Show %@ More Boosts"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 550c56c2f0..f140b6eae9 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1062,19 +1062,21 @@ public protocol AccountContext: AnyObject { public struct PremiumConfiguration { public static var defaultValue: PremiumConfiguration { - return PremiumConfiguration(isPremiumDisabled: false, showPremiumGiftInAttachMenu: false, showPremiumGiftInTextField: false, giveawayGiftsPurchaseAvailable: false) + return PremiumConfiguration(isPremiumDisabled: false, showPremiumGiftInAttachMenu: false, showPremiumGiftInTextField: false, giveawayGiftsPurchaseAvailable: false, boostsPerGiftCount: 3) } public let isPremiumDisabled: Bool public let showPremiumGiftInAttachMenu: Bool public let showPremiumGiftInTextField: Bool public let giveawayGiftsPurchaseAvailable: Bool + public let boostsPerGiftCount: Int32 - fileprivate init(isPremiumDisabled: Bool, showPremiumGiftInAttachMenu: Bool, showPremiumGiftInTextField: Bool, giveawayGiftsPurchaseAvailable: Bool) { + fileprivate init(isPremiumDisabled: Bool, showPremiumGiftInAttachMenu: Bool, showPremiumGiftInTextField: Bool, giveawayGiftsPurchaseAvailable: Bool, boostsPerGiftCount: Int32) { self.isPremiumDisabled = isPremiumDisabled self.showPremiumGiftInAttachMenu = showPremiumGiftInAttachMenu self.showPremiumGiftInTextField = showPremiumGiftInTextField self.giveawayGiftsPurchaseAvailable = giveawayGiftsPurchaseAvailable + self.boostsPerGiftCount = boostsPerGiftCount } public static func with(appConfiguration: AppConfiguration) -> PremiumConfiguration { @@ -1083,7 +1085,8 @@ public struct PremiumConfiguration { isPremiumDisabled: data["premium_purchase_blocked"] as? Bool ?? false, showPremiumGiftInAttachMenu: data["premium_gift_attach_menu_icon"] as? Bool ?? false, showPremiumGiftInTextField: data["premium_gift_text_field_icon"] as? Bool ?? false, - giveawayGiftsPurchaseAvailable: data["giveaway_gifts_purchase_available"] as? Bool ?? false + giveawayGiftsPurchaseAvailable: data["giveaway_gifts_purchase_available"] as? Bool ?? false, + boostsPerGiftCount: Int32(data["boosts_per_sent_gift"] as? Double ?? 3) ) } else { return .defaultValue diff --git a/submodules/PremiumUI/BUILD b/submodules/PremiumUI/BUILD index 0cf15d47b7..4f360fed21 100644 --- a/submodules/PremiumUI/BUILD +++ b/submodules/PremiumUI/BUILD @@ -108,6 +108,7 @@ swift_library( "//submodules/TelegramUI/Components/Utils/RoundedRectWithTailPath", "//submodules/CountrySelectionUI", "//submodules/TelegramUI/Components/Stories/PeerListItemComponent", + "//submodules/InvisibleInkDustNode", ], visibility = [ "//visibility:public", diff --git a/submodules/PremiumUI/Sources/CreateGiveawayController.swift b/submodules/PremiumUI/Sources/CreateGiveawayController.swift index 4be43057ca..fde0cc2f57 100644 --- a/submodules/PremiumUI/Sources/CreateGiveawayController.swift +++ b/submodules/PremiumUI/Sources/CreateGiveawayController.swift @@ -361,7 +361,7 @@ private enum CreateGiveawayEntry: ItemListNodeEntry { default: color = .blue } - return GiftOptionItem(presentationData: presentationData, context: arguments.context, icon: .image(color: color, name: "Premium/Giveaway"), title: title, titleFont: .bold, subtitle: subtitle, label: .boosts(prepaidGiveaway.quantity), sectionId: self.section, action: nil) + return GiftOptionItem(presentationData: presentationData, context: arguments.context, icon: .image(color: color, name: "Premium/Giveaway"), title: title, titleFont: .bold, titleBadge: "\(prepaidGiveaway.quantity * 4)", subtitle: subtitle, sectionId: self.section, action: nil) case let .subscriptionsHeader(_, text, additionalText): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, accessoryText: ItemListSectionHeaderAccessoryText(value: additionalText, color: .generic), sectionId: self.section) case let .subscriptions(_, value): @@ -543,7 +543,7 @@ private func createGiveawayControllerEntries( if case .giveaway = state.mode { if case .generic = subject { - entries.append(.subscriptionsHeader(presentationData.theme, "QUANTITY OF PRIZES".uppercased(), "\(state.subscriptions) BOOSTS")) + entries.append(.subscriptionsHeader(presentationData.theme, "QUANTITY OF PRIZES".uppercased(), "\(state.subscriptions * 4) BOOSTS")) entries.append(.subscriptions(presentationData.theme, state.subscriptions)) entries.append(.subscriptionsInfo(presentationData.theme, "Choose how many Premium subscriptions to give away and boosts to receive.")) } @@ -553,7 +553,7 @@ private func createGiveawayControllerEntries( let channels = [peerId] + state.channels for channelId in channels { if let channel = peers[channelId] { - entries.append(.channel(index, presentationData.theme, channel, channel.id == peerId ? state.subscriptions : nil, false)) + entries.append(.channel(index, presentationData.theme, channel, channel.id == peerId ? state.subscriptions * 4 : nil, false)) } index += 1 } diff --git a/submodules/PremiumUI/Sources/PremiumBoostScreen.swift b/submodules/PremiumUI/Sources/PremiumBoostScreen.swift index ac46b5f30a..7c553d8ab9 100644 --- a/submodules/PremiumUI/Sources/PremiumBoostScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumBoostScreen.swift @@ -182,11 +182,12 @@ public func PremiumBoostScreen( } } else { if isPremium { + let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with({ $0 })) let controller = textAlertController( sharedContext: context.sharedContext, updatedPresentationData: nil, title: "More Boosts Needed", - text: "To boost **\(peer.compactDisplayTitle)**, get more boosts by gifting **Telegram Premium** to a friend.", + text: "To boost **\(peer.compactDisplayTitle)** again, gift **Telegram Premium** to a friend and get **\(premiumConfiguration.boostsPerGiftCount)** additional boosts.", actions: [ TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {}) ], diff --git a/submodules/PremiumUI/Sources/PremiumGiftCodeScreen.swift b/submodules/PremiumUI/Sources/PremiumGiftCodeScreen.swift index ad2fb7f0a2..359dbbe29e 100644 --- a/submodules/PremiumUI/Sources/PremiumGiftCodeScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumGiftCodeScreen.swift @@ -20,44 +20,48 @@ import AvatarNode import TextFormat import TelegramStringFormatting import UndoUI +import InvisibleInkDustNode private final class PremiumGiftCodeSheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext - let giftCode: PremiumGiftCodeInfo + let subject: PremiumGiftCodeScreen.Subject let action: () -> Void let cancel: (Bool) -> Void let openPeer: (EnginePeer) -> Void let openMessage: (EngineMessage.Id) -> Void let copyLink: (String) -> Void let shareLink: (String) -> Void + let displayHiddenTooltip: () -> Void init( context: AccountContext, - giftCode: PremiumGiftCodeInfo, + subject: PremiumGiftCodeScreen.Subject, action: @escaping () -> Void, cancel: @escaping (Bool) -> Void, openPeer: @escaping (EnginePeer) -> Void, openMessage: @escaping (EngineMessage.Id) -> Void, copyLink: @escaping (String) -> Void, - shareLink: @escaping (String) -> Void + shareLink: @escaping (String) -> Void, + displayHiddenTooltip: @escaping () -> Void ) { self.context = context - self.giftCode = giftCode + self.subject = subject self.action = action self.cancel = cancel self.openPeer = openPeer self.openMessage = openMessage self.copyLink = copyLink self.shareLink = shareLink + self.displayHiddenTooltip = displayHiddenTooltip } static func ==(lhs: PremiumGiftCodeSheetContent, rhs: PremiumGiftCodeSheetContent) -> Bool { if lhs.context !== rhs.context { return false } - if lhs.giftCode != rhs.giftCode { + if lhs.subject != rhs.subject { return false } return true @@ -72,15 +76,23 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent { var cachedCloseImage: (UIImage, PresentationTheme)? - init(context: AccountContext, giftCode: PremiumGiftCodeInfo) { + init(context: AccountContext, subject: PremiumGiftCodeScreen.Subject) { self.context = context super.init() var peerIds: [EnginePeer.Id] = [] - peerIds.append(giftCode.fromPeerId) - if let toPeerId = giftCode.toPeerId { - peerIds.append(toPeerId) + switch subject { + case let .giftCode(giftCode): + peerIds.append(giftCode.fromPeerId) + if let toPeerId = giftCode.toPeerId { + peerIds.append(toPeerId) + } + case let .boost(channelId, boost): + peerIds.append(channelId) + if let peerId = boost.peer?.id { + peerIds.append(peerId) + } } self.disposable = (context.engine.data.get( @@ -111,7 +123,7 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent { } func makeState() -> State { - return State(context: self.context, giftCode: self.giftCode) + return State(context: self.context, subject: self.subject) } static var body: Body { @@ -133,7 +145,7 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent { let accountContext = context.component.context let state = context.state - let giftCode = component.giftCode + let subject = component.subject let sideInset: CGFloat = 16.0 + environment.safeInsets.left let textSideInset: CGFloat = 32.0 + environment.safeInsets.left @@ -161,17 +173,60 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent { let descriptionText: String let additionalText: String let buttonText: String - if let usedDate = giftCode.usedDate { - let dateString = stringForMediumDate(timestamp: usedDate, strings: strings, dateTimeFormat: dateTimeFormat) - titleText = "Used Gift Link" - descriptionText = "This link was used to activate a **Telegram Premium** subscription." - additionalText = "This link was used on \(dateString)." - buttonText = strings.Common_OK - } else { + + let link: String? + let date: Int32 + let fromPeer: EnginePeer? + var toPeerId: EnginePeer.Id? + let toPeer: EnginePeer? + let months: Int32 + + var gloss = false + switch subject { + case let .giftCode(giftCode): + gloss = !giftCode.isUsed + if let usedDate = giftCode.usedDate { + let dateString = stringForMediumDate(timestamp: usedDate, strings: strings, dateTimeFormat: dateTimeFormat) + titleText = "Used Gift Link" + descriptionText = "This link was used to activate a **Telegram Premium** subscription." + additionalText = "This link was used on \(dateString)." + buttonText = strings.Common_OK + } else { + titleText = "Gift Link" + descriptionText = "This link allows you to activate a **Telegram Premium** subscription." + additionalText = "You can also [send this link]() to a friend as a gift." + buttonText = "Use Link" + } + link = "https://t.me/giftcode/\(giftCode.slug)" + date = giftCode.date + fromPeer = state.peerMap[giftCode.fromPeerId] + toPeerId = giftCode.toPeerId + if let toPeerId = giftCode.toPeerId { + toPeer = state.peerMap[toPeerId] + } else { + toPeer = nil + } + months = giftCode.months + case let .boost(channelId, boost): titleText = "Gift Link" - descriptionText = "This link allows you to activate a **Telegram Premium** subscription." - additionalText = "You can also [send this link]() to a friend as a gift." - buttonText = "Use Link" + if let peer = boost.peer, !boost.flags.contains(.isUnclaimed) { + toPeer = boost.peer + descriptionText = "This link allows \(peer.compactDisplayTitle) to activate a **Telegram Premium** subscription." + } else { + toPeer = nil + descriptionText = "This link allows to activate a **Telegram Premium** subscription." + } + if boost.slug == nil { + additionalText = "This link hasn't been used yet." + } else { + additionalText = "" + } + buttonText = strings.Common_OK + link = nil + date = boost.date + toPeerId = boost.peer?.id + fromPeer = state.peerMap[channelId] + months = Int32(round(Float(boost.expires - boost.date) / (86400.0 * 30.0))) } let title = title.update( @@ -213,14 +268,17 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent { transition: .immediate ) - let link = "https://t.me/giftcode/\(giftCode.slug)" let linkButton = linkButton.update( component: Button( content: AnyComponent( LinkButtonContentComponent(theme: environment.theme, text: link) ), action: { - component.copyLink(link) + if let link { + component.copyLink(link) + } else { + component.displayHiddenTooltip() + } } ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), @@ -231,8 +289,7 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent { let tableTextColor = theme.list.itemPrimaryTextColor let tableLinkColor = theme.list.itemAccentColor var tableItems: [TableComponent.Item] = [] - - let fromPeer = state.peerMap[giftCode.fromPeerId] + tableItems.append(.init( id: "from", title: "From", @@ -250,8 +307,7 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent { ) ) )) - if let toPeerId = giftCode.toPeerId { - let toPeer = state.peerMap[toPeerId] + if let toPeer { tableItems.append(.init( id: "to", title: "To", @@ -259,8 +315,8 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent { Button( content: AnyComponent(PeerCellComponent(context: context.component.context, textColor: tableLinkColor, peer: toPeer)), action: { - if let peer = toPeer, peer.id != accountContext.account.peerId { - component.openPeer(peer) + if toPeer.id != accountContext.account.peerId { + component.openPeer(toPeer) Queue.mainQueue().after(1.0, { component.cancel(false) }) @@ -269,7 +325,7 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent { ) ) )) - } else if giftCode.isGiveaway { + } else if toPeerId == nil { tableItems.append(.init( id: "to", title: "To", @@ -278,12 +334,7 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent { ) )) } - let giftTitle: String - if giftCode.months == 12 { - giftTitle = "Telegram Premium for 1 year" - } else { - giftTitle = "Telegram Premium for \(giftCode.months) months" - } + let giftTitle = "Telegram Premium for \(months) months" tableItems.append(.init( id: "gift", title: "Gift", @@ -292,35 +343,37 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent { ) )) - let giftReason: String - if giftCode.toPeerId == nil { - giftReason = "Incomplete Giveaway" - } else { - giftReason = giftCode.isGiveaway ? "Giveaway" : "You were selected by the channel" - } - tableItems.append(.init( - id: "reason", - title: "Reason", - component: AnyComponent( - Button( - content: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: giftReason, font: tableFont, textColor: giftCode.messageId != nil ? tableLinkColor : tableTextColor)))), - isEnabled: true, - action: { - if let messageId = giftCode.messageId { - component.openMessage(messageId) + if case let .giftCode(giftCode) = component.subject { + let giftReason: String + if giftCode.toPeerId == nil { + giftReason = "Incomplete Giveaway" + } else { + giftReason = giftCode.isGiveaway ? "Giveaway" : "You were selected by the channel" + } + tableItems.append(.init( + id: "reason", + title: "Reason", + component: AnyComponent( + Button( + content: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: giftReason, font: tableFont, textColor: giftCode.messageId != nil ? tableLinkColor : tableTextColor)))), + isEnabled: true, + action: { + if let messageId = giftCode.messageId { + component.openMessage(messageId) + } + Queue.mainQueue().after(1.0) { + component.cancel(false) + } } - Queue.mainQueue().after(1.0) { - component.cancel(false) - } - } + ) ) - ) - )) + )) + } tableItems.append(.init( id: "date", title: "Date", component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: giftCode.date, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor))) + MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: date, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor))) ) )) @@ -348,7 +401,9 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent { } }, tapAction: { attributes, _ in - component.shareLink("https://t.me/giftcode/\(giftCode.slug)") + if let link { + component.shareLink(link) + } } ), availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), @@ -363,15 +418,15 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent { fontSize: 17.0, height: 50.0, cornerRadius: 10.0, - gloss: !giftCode.isUsed, + gloss: gloss, iconName: nil, animationName: nil, iconPosition: .left, action: { - if giftCode.isUsed { - component.cancel(true) - } else { + if gloss { component.action() + } else { + component.cancel(true) } } ), @@ -430,36 +485,39 @@ private final class PremiumGiftCodeSheetComponent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext - let giftCode: PremiumGiftCodeInfo + let subject: PremiumGiftCodeScreen.Subject let action: () -> Void let openPeer: (EnginePeer) -> Void let openMessage: (EngineMessage.Id) -> Void let copyLink: (String) -> Void let shareLink: (String) -> Void + let displayHiddenTooltip: () -> Void init( context: AccountContext, - giftCode: PremiumGiftCodeInfo, + subject: PremiumGiftCodeScreen.Subject, action: @escaping () -> Void, openPeer: @escaping (EnginePeer) -> Void, openMessage: @escaping (EngineMessage.Id) -> Void, copyLink: @escaping (String) -> Void, - shareLink: @escaping (String) -> Void + shareLink: @escaping (String) -> Void, + displayHiddenTooltip: @escaping () -> Void ) { self.context = context - self.giftCode = giftCode + self.subject = subject self.action = action self.openPeer = openPeer self.openMessage = openMessage self.copyLink = copyLink self.shareLink = shareLink + self.displayHiddenTooltip = displayHiddenTooltip } static func ==(lhs: PremiumGiftCodeSheetComponent, rhs: PremiumGiftCodeSheetComponent) -> Bool { if lhs.context !== rhs.context { return false } - if lhs.giftCode != rhs.giftCode { + if lhs.subject != rhs.subject { return false } return true @@ -477,7 +535,7 @@ private final class PremiumGiftCodeSheetComponent: CombinedComponent { component: SheetComponent( content: AnyComponent(PremiumGiftCodeSheetContent( context: context.component.context, - giftCode: context.component.giftCode, + subject: context.component.subject, action: context.component.action, cancel: { animate in if animate { @@ -493,7 +551,8 @@ private final class PremiumGiftCodeSheetComponent: CombinedComponent { openPeer: context.component.openPeer, openMessage: context.component.openMessage, copyLink: context.component.copyLink, - shareLink: context.component.shareLink + shareLink: context.component.shareLink, + displayHiddenTooltip: context.component.displayHiddenTooltip )), backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), animateOut: animateOut @@ -534,6 +593,11 @@ private final class PremiumGiftCodeSheetComponent: CombinedComponent { } public class PremiumGiftCodeScreen: ViewControllerComponentContainer { + public enum Subject: Equatable { + case giftCode(PremiumGiftCodeInfo) + case boost(EnginePeer.Id, ChannelBoostersContext.State.Boost) + } + private let context: AccountContext public var disposed: () -> Void = {} @@ -541,7 +605,7 @@ public class PremiumGiftCodeScreen: ViewControllerComponentContainer { public init( context: AccountContext, - giftCode: PremiumGiftCodeInfo, + subject: PremiumGiftCodeScreen.Subject, forceDark: Bool = false, action: @escaping () -> Void, openPeer: @escaping (EnginePeer) -> Void = { _ in }, @@ -551,9 +615,27 @@ public class PremiumGiftCodeScreen: ViewControllerComponentContainer { self.context = context var copyLinkImpl: ((String) -> Void)? - super.init(context: context, component: PremiumGiftCodeSheetComponent(context: context, giftCode: giftCode, action: action, openPeer: openPeer, openMessage: openMessage, copyLink: { link in - copyLinkImpl?(link) - }, shareLink: shareLink), navigationBarAppearance: .none, statusBarStyle: .ignore, theme: forceDark ? .dark : .default) + var displayHiddenTooltipImpl: (() -> Void)? + super.init( + context: context, + component: PremiumGiftCodeSheetComponent( + context: context, + subject: subject, + action: action, + openPeer: openPeer, + openMessage: openMessage, + copyLink: { link in + copyLinkImpl?(link) + }, + shareLink: shareLink, + displayHiddenTooltip: { + displayHiddenTooltipImpl?() + } + ), + navigationBarAppearance: .none, + statusBarStyle: .ignore, + theme: forceDark ? .dark : .default + ) self.navigationPresentation = .flatModal @@ -563,9 +645,21 @@ public class PremiumGiftCodeScreen: ViewControllerComponentContainer { guard let self else { return } + self.dismissAllTooltips() + let presentationData = context.sharedContext.currentPresentationData.with { $0 } self.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, position: .top, action: { _ in return true }), in: .window(.root)) } + + displayHiddenTooltipImpl = { [weak self] in + guard let self else { + return + } + self.dismissAllTooltips() + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: "Only the recipient can see the code.", timeout: nil, customUndoText: nil), elevatedLayout: false, position: .top, action: { _ in return true }), in: .window(.root)) + } } required public init(coder aDecoder: NSCoder) { @@ -581,15 +675,34 @@ public class PremiumGiftCodeScreen: ViewControllerComponentContainer { self.view.disablesInteractiveModalDismiss = true } + + public override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + self.dismissAllTooltips() + } + + fileprivate func dismissAllTooltips() { + self.window?.forEachController({ controller in + if let controller = controller as? UndoOverlayController { + controller.dismiss() + } + }) + self.forEachController({ controller in + if let controller = controller as? UndoOverlayController { + controller.dismiss() + } + return true + }) + } } private final class LinkButtonContentComponent: CombinedComponent { let theme: PresentationTheme - let text: String + let text: String? public init( theme: PresentationTheme, - text: String + text: String? ) { self.theme = theme self.text = text @@ -609,6 +722,7 @@ private final class LinkButtonContentComponent: CombinedComponent { let background = Child(RoundedRectangle.self) let text = Child(MultilineTextComponent.self) let icon = Child(BundleIconComponent.self) + let dust = Child(DustComponent.self) return { context in let component = context.component @@ -621,36 +735,48 @@ private final class LinkButtonContentComponent: CombinedComponent { transition: context.transition ) - let text = text.update( - component: MultilineTextComponent( - text: .plain(NSAttributedString( - string: component.text.replacingOccurrences(of: "https://", with: ""), - font: Font.regular(17.0), - textColor: component.theme.list.itemPrimaryTextColor, - paragraphAlignment: .natural - )), - horizontalAlignment: .center, - maximumNumberOfLines: 1 - ), - availableSize: CGSize(width: context.availableSize.width - sideInset - sideInset, height: CGFloat.greatestFiniteMagnitude), - transition: .immediate - ) - - let icon = icon.update( - component: BundleIconComponent(name: "Chat/Context Menu/Copy", tintColor: component.theme.list.itemAccentColor), - availableSize: context.availableSize, - transition: context.transition - ) - context.add(background .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) ) - context.add(text - .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) - ) - context.add(icon - .position(CGPoint(x: context.availableSize.width - icon.size.width / 2.0 - 14.0, y: context.availableSize.height / 2.0)) - ) + + if let _ = component.text { + let text = text.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: (component.text ?? "").replacingOccurrences(of: "https://", with: ""), + font: Font.regular(17.0), + textColor: component.theme.list.itemPrimaryTextColor, + paragraphAlignment: .natural + )), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width - sideInset - sideInset, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + + let icon = icon.update( + component: BundleIconComponent(name: "Chat/Context Menu/Copy", tintColor: component.theme.list.itemAccentColor), + availableSize: context.availableSize, + transition: context.transition + ) + context.add(icon + .position(CGPoint(x: context.availableSize.width - icon.size.width / 2.0 - 14.0, y: context.availableSize.height / 2.0)) + ) + context.add(text + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + } else { + let dust = dust.update( + component: DustComponent(color: component.theme.list.itemSecondaryTextColor), + availableSize: CGSize(width: context.availableSize.width * 0.8, height: context.availableSize.height * 0.54), + transition: context.transition + ) + context.add(dust + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + } + return context.availableSize } } @@ -955,3 +1081,53 @@ private final class PeerCellComponent: Component { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } + +private final class DustComponent: Component { + let color: UIColor + + init(color: UIColor) { + self.color = color + } + + static func ==(lhs: DustComponent, rhs: DustComponent) -> Bool { + if lhs.color != rhs.color { + return false + } + return true + } + + final class View: UIView { + private let dustView = InvisibleInkDustView(textNode: nil, enableAnimations: true) + + private var component: DustComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.addSubview(self.dustView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: DustComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.state = state + + let rects: [CGRect] = [CGRect(origin: .zero, size: availableSize).insetBy(dx: 5.0, dy: 5.0)] + self.dustView.update(size: availableSize, color: component.color, textColor: component.color, rects: rects, wordRects: rects) + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/PremiumUI/Sources/ReplaceBoostScreen.swift b/submodules/PremiumUI/Sources/ReplaceBoostScreen.swift index 568c184d45..4091d712b8 100644 --- a/submodules/PremiumUI/Sources/ReplaceBoostScreen.swift +++ b/submodules/PremiumUI/Sources/ReplaceBoostScreen.swift @@ -104,7 +104,6 @@ private final class ReplaceBoostScreenComponent: CombinedComponent { } static var body: Body { -// let closeButton = Child(Button.self) let header = Child(ReplaceBoostHeaderComponent.self) let description = Child(MultilineTextComponent.self) let boostsBackground = Child(RoundedRectangle.self) @@ -117,7 +116,6 @@ private final class ReplaceBoostScreenComponent: CombinedComponent { let theme = environment.theme let strings = environment.strings -// let topInset: CGFloat = environment.navigationHeight + 22.0 let textSideInset: CGFloat = 32.0 let sideInset: CGFloat = 16.0 + environment.safeInsets.left @@ -168,8 +166,13 @@ private final class ReplaceBoostScreenComponent: CombinedComponent { return (TelegramTextAttributes.URL, contents) }) - let channelName = state.peer?.compactDisplayTitle ?? "" - let descriptionString = "To boost **\(channelName)**, reassign a previous boost or gift **Telegram Premium** to a friend to get **3** additional boosts." + let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.component.context.currentAppConfiguration.with({ $0 })) + + var channelName = state.peer?.compactDisplayTitle ?? "" + if channelName.count > 48 { + channelName = "\(channelName.prefix(48))..." + } + let descriptionString = "To boost **\(channelName)**, reassign a previous boost or gift **Telegram Premium** to a friend to get **\(premiumConfiguration.boostsPerGiftCount)** additional boosts." let description = description.update( component: MultilineTextComponent( @@ -254,7 +257,7 @@ private final class ReplaceBoostScreenComponent: CombinedComponent { let boosts = boosts.update( component: List(boostItems), environment: {}, - availableSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0), + availableSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100000.0), transition: context.transition ) @@ -272,9 +275,7 @@ private final class ReplaceBoostScreenComponent: CombinedComponent { .position(CGPoint(x: availableSize.width / 2.0, y: 226 + boosts.size.height / 2.0)) ) - let contentSize = CGSize(width: availableSize.width, height: 226.0 + boosts.size.height) - - return contentSize + return CGSize(width: availableSize.width, height: 226.0 + boosts.size.height + environment.safeInsets.bottom + 91.0) } } } @@ -293,6 +294,8 @@ public class ReplaceBoostScreen: ViewController { let hostView: ComponentHostView private let footerView: FooterView + private var footerHeight: CGFloat = 0.0 + private var bottomOffset: CGFloat = 1000.0 private(set) var isExpanded = false private var panGestureRecognizer: UIPanGestureRecognizer? @@ -350,6 +353,7 @@ public class ReplaceBoostScreen: ViewController { } self.controller?.replaceBoosts?(self.selectedSlots) } + self.footerView.updateBackgroundAlpha(1.0, transition: .immediate) } override func didLoad() { @@ -382,9 +386,22 @@ public class ReplaceBoostScreen: ViewController { return true } + private func updateFooterAlpha() { + guard let (layout, _) = self.currentLayout else { + return + } + let contentFrame = self.scrollView.convert(self.hostView.frame, to: self.view) + let bottomOffset = contentFrame.maxY - layout.size.height + + let backgroundAlpha: CGFloat = min(30.0, max(0.0, bottomOffset)) / 30.0 + self.footerView.updateBackgroundAlpha(backgroundAlpha, transition: .immediate) + } + func scrollViewDidScroll(_ scrollView: UIScrollView) { let contentOffset = self.scrollView.contentOffset.y self.controller?.navigationBar?.updateBackgroundAlpha(min(30.0, contentOffset) / 30.0, transition: .immediate) + + self.updateFooterAlpha() } func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { @@ -429,6 +446,7 @@ public class ReplaceBoostScreen: ViewController { } func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: Transition) { + let hadLayout = self.currentLayout != nil self.currentLayout = (layout, navigationHeight) if let controller = self.controller, let navigationBar = controller.navigationBar, navigationBar.view.superview !== self.wrappingView { @@ -541,9 +559,11 @@ public class ReplaceBoostScreen: ViewController { let footerInsets = UIEdgeInsets(top: 0.0, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.safeInsets.right) transition.setFrame(view: self.footerView, frame: CGRect(origin: CGPoint(x: 0.0, y: -topInset), size: layout.size)) - let _ = self.footerView.update(size: layout.size, insets: footerInsets, theme: self.presentationData.theme, count: Int32(self.selectedSlots.count)) + self.footerHeight = self.footerView.update(size: layout.size, insets: footerInsets, theme: self.presentationData.theme, count: Int32(self.selectedSlots.count)) - self.footerView.updateBackgroundAlpha(0.0, transition: .immediate) + if !hadLayout { + self.updateFooterAlpha() + } } private var didPlayAppearAnimation = false @@ -574,7 +594,7 @@ public class ReplaceBoostScreen: ViewController { factor = 0.15 } if self.scrollView.contentSize.height > 0.0 && self.scrollView.contentSize.height < layout.size.height / 2.0 { - return layout.size.height - self.scrollView.contentSize.height - layout.intrinsicInsets.bottom - 154.0 + return layout.size.height - self.scrollView.contentSize.height - layout.intrinsicInsets.bottom - 30.0 } else { return floor(max(layout.size.width, layout.size.height) * factor) } @@ -673,6 +693,8 @@ public class ReplaceBoostScreen: ViewController { self.bounds = bounds self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate) + + self.updateFooterAlpha() case .ended: guard let (currentTopInset, panOffset, scrollView, listNode) = self.panGestureArguments else { return @@ -760,10 +782,14 @@ public class ReplaceBoostScreen: ViewController { self.bounds = bounds self.layer.animateBounds(from: previousBounds, to: self.bounds, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) } + + self.updateFooterAlpha() case .cancelled: self.panGestureArguments = nil self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(.animated(duration: 0.3, curve: .easeInOut))) + + self.updateFooterAlpha() default: break } @@ -890,6 +916,11 @@ public class ReplaceBoostScreen: ViewController { self.node.updateIsVisible(isVisible: true) } + override open func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + self.dismissAllTooltips() + } + override open func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) diff --git a/submodules/StatisticsUI/Sources/ChannelStatsController.swift b/submodules/StatisticsUI/Sources/ChannelStatsController.swift index 5c46b22f65..22e1f4084a 100644 --- a/submodules/StatisticsUI/Sources/ChannelStatsController.swift +++ b/submodules/StatisticsUI/Sources/ChannelStatsController.swift @@ -22,7 +22,7 @@ import ShareController import ItemListPeerActionItem import PremiumUI -private let maxUsersDisplayedLimit: Int32 = 50 +private let maxUsersDisplayedLimit: Int32 = 5 private final class ChannelStatsControllerArguments { let context: AccountContext @@ -31,20 +31,20 @@ private final class ChannelStatsControllerArguments { let contextAction: (MessageId, ASDisplayNode, ContextGesture?) -> Void let copyBoostLink: (String) -> Void let shareBoostLink: (String) -> Void - let openPeer: (EnginePeer) -> Void + let openBoost: (ChannelBoostersContext.State.Boost) -> Void let expandBoosters: () -> Void let openGifts: () -> Void let createPrepaidGiveaway: (PrepaidGiveaway) -> Void let updateGiftsSelected: (Bool) -> Void - init(context: AccountContext, loadDetailedGraph: @escaping (StatsGraph, Int64) -> Signal, openMessage: @escaping (MessageId) -> Void, contextAction: @escaping (MessageId, ASDisplayNode, ContextGesture?) -> Void, copyBoostLink: @escaping (String) -> Void, shareBoostLink: @escaping (String) -> Void, openPeer: @escaping (EnginePeer) -> Void, expandBoosters: @escaping () -> Void, openGifts: @escaping () -> Void, createPrepaidGiveaway: @escaping (PrepaidGiveaway) -> Void, updateGiftsSelected: @escaping (Bool) -> Void) { + init(context: AccountContext, loadDetailedGraph: @escaping (StatsGraph, Int64) -> Signal, openMessage: @escaping (MessageId) -> Void, contextAction: @escaping (MessageId, ASDisplayNode, ContextGesture?) -> Void, copyBoostLink: @escaping (String) -> Void, shareBoostLink: @escaping (String) -> Void, openBoost: @escaping (ChannelBoostersContext.State.Boost) -> Void, expandBoosters: @escaping () -> Void, openGifts: @escaping () -> Void, createPrepaidGiveaway: @escaping (PrepaidGiveaway) -> Void, updateGiftsSelected: @escaping (Bool) -> Void) { self.context = context self.loadDetailedGraph = loadDetailedGraph self.openMessageStats = openMessage self.contextAction = contextAction self.copyBoostLink = copyBoostLink self.shareBoostLink = shareBoostLink - self.openPeer = openPeer + self.openBoost = openBoost self.expandBoosters = expandBoosters self.openGifts = openGifts self.createPrepaidGiveaway = createPrepaidGiveaway @@ -118,7 +118,7 @@ private enum StatsEntry: ItemListNodeEntry { case boostersTitle(PresentationTheme, String) case boostersPlaceholder(PresentationTheme, String) case boosterTabs(PresentationTheme, String, String, Bool) - case booster(Int32, PresentationTheme, PresentationDateTimeFormat, EnginePeer?, Int32, ChannelBoostersContext.State.Boost.Flags, Int32, Int32) + case booster(Int32, PresentationTheme, PresentationDateTimeFormat, ChannelBoostersContext.State.Boost) case boostersExpand(PresentationTheme, String) case boostersInfo(PresentationTheme, String) @@ -232,7 +232,7 @@ private enum StatsEntry: ItemListNodeEntry { return 2102 case .boosterTabs: return 2103 - case let .booster(index, _, _, _, _, _, _, _): + case let .booster(index, _, _, _): return 2104 + index case .boostersExpand: return 10000 @@ -439,8 +439,8 @@ private enum StatsEntry: ItemListNodeEntry { } else { return false } - case let .booster(lhsIndex, lhsTheme, lhsDateTimeFormat, lhsPeer, lhsCount, lhsFlags, lhsDate, lhsExpires): - if case let .booster(rhsIndex, rhsTheme, rhsDateTimeFormat, rhsPeer, rhsCount, rhsFlags, rhsDate, rhsExpires) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsPeer == rhsPeer, lhsCount == rhsCount, lhsFlags == rhsFlags, lhsDate == rhsDate, lhsExpires == rhsExpires { + case let .booster(lhsIndex, lhsTheme, lhsDateTimeFormat, lhsBoost): + if case let .booster(rhsIndex, rhsTheme, rhsDateTimeFormat, rhsBoost) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsBoost == rhsBoost { return true } else { return false @@ -548,38 +548,49 @@ private enum StatsEntry: ItemListNodeEntry { return BoostsTabsItem(theme: presentationData.theme, boostsText: boostText, giftsText: giftText, selectedTab: giftSelected ? .gifts : .boosts, sectionId: self.section, selectionUpdated: { tab in arguments.updateGiftsSelected(tab == .gifts) }) - case let .booster(_, _, _, peer, count, flags, date, expires): - let expiresValue = stringForDate(timestamp: expires, strings: presentationData.strings) + case let .booster(_, _, _, boost): + let count = boost.multiplier + let expiresValue = stringForDate(timestamp: boost.expires, strings: presentationData.strings) let expiresString: String - let durationMonths = Int32(round(Float(expires - date) / (86400.0 * 30.0))) + let durationMonths = Int32(round(Float(boost.expires - boost.date) / (86400.0 * 30.0))) let durationString = "\(durationMonths)m" let title: String let icon: GiftOptionItem.Icon var label: String? - if flags.contains(.isGiveaway) { + if boost.flags.contains(.isGiveaway) { label = "🏆 Giveaway" - } else if flags.contains(.isGift) { + } else if boost.flags.contains(.isGift) { label = "🎁 Gift" } - if let peer { + + let color: GiftOptionItem.Icon.Color + if durationMonths > 11 { + color = .red + } else if durationMonths > 5 { + color = .blue + } else { + color = .green + } + + if boost.flags.contains(.isUnclaimed) { + title = "Unclaimed" + icon = .image(color: color, name: "Premium/Unclaimed") + expiresString = "\(durationString) • \(expiresValue)" + } else if let peer = boost.peer { title = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) icon = .peer(peer) - expiresString = presentationData.strings.Stats_Boosts_ExpiresOn(expiresValue).string - } else { - let color: GiftOptionItem.Icon.Color - if durationMonths > 11 { - color = .red - } else if durationMonths > 5 { - color = .blue + if let _ = label { + expiresString = expiresValue } else { - color = .green + expiresString = presentationData.strings.Stats_Boosts_ExpiresOn(expiresValue).string } - if flags.contains(.isUnclaimed) { + } else { + if boost.flags.contains(.isUnclaimed) { title = "Unclaimed" icon = .image(color: color, name: "Premium/Unclaimed") - } else if flags.contains(.isGiveaway) { + } else if boost.flags.contains(.isGiveaway) { title = "To be distributed" icon = .image(color: color, name: "Premium/ToBeDistributed") } else { @@ -588,9 +599,9 @@ private enum StatsEntry: ItemListNodeEntry { } expiresString = "\(durationString) • \(expiresValue)" } - return GiftOptionItem(presentationData: presentationData, context: arguments.context, icon: icon, title: title, titleFont: .bold, titleBadge: count > 1 ? "\(count)" : nil, subtitle: expiresString, label: label.flatMap { .semitransparent($0) }, sectionId: self.section, action: peer != nil && peer?.id != arguments.context.account.peerId ? { - arguments.openPeer(peer!) - } : nil) + return GiftOptionItem(presentationData: presentationData, context: arguments.context, icon: icon, title: title, titleFont: .bold, titleBadge: count > 1 ? "\(count)" : nil, subtitle: expiresString, label: label.flatMap { .semitransparent($0) }, sectionId: self.section, action: { + arguments.openBoost(boost) + }) case let .boostersExpand(theme, title): return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.downArrowImage(theme), title: title, sectionId: self.section, editing: false, action: { arguments.expandBoosters() @@ -626,7 +637,7 @@ private enum StatsEntry: ItemListNodeEntry { default: color = .blue } - return GiftOptionItem(presentationData: presentationData, context: arguments.context, icon: .image(color: color, name: "Premium/Giveaway"), title: title, titleFont: .bold, titleBadge: "\(prepaidGiveaway.quantity)", subtitle: subtitle, label: nil, sectionId: self.section, action: { + return GiftOptionItem(presentationData: presentationData, context: arguments.context, icon: .image(color: color, name: "Premium/Giveaway"), title: title, titleFont: .bold, titleBadge: "\(prepaidGiveaway.quantity * 4)", subtitle: subtitle, label: nil, sectionId: self.section, action: { arguments.createPrepaidGiveaway(prepaidGiveaway) }) } @@ -824,12 +835,12 @@ private func channelStatsControllerEntries(state: ChannelStatsControllerState, p } for booster in boosters { - entries.append(.booster(boosterIndex, presentationData.theme, presentationData.dateTimeFormat, booster.peer, booster.multiplier, booster.flags, booster.date, booster.expires)) + entries.append(.booster(boosterIndex, presentationData.theme, presentationData.dateTimeFormat, booster)) boosterIndex += 1 } if !effectiveExpanded { - entries.append(.boostersExpand(presentationData.theme, presentationData.strings.PeopleNearby_ShowMorePeople(Int32(selectedState.count) - maxUsersDisplayedLimit))) + entries.append(.boostersExpand(presentationData.theme, presentationData.strings.Stats_Boosts_ShowMoreBoosts(Int32(selectedState.count) - maxUsersDisplayedLimit))) } } @@ -899,7 +910,6 @@ public func channelStatsController(context: AccountContext, updatedPresentationD var presentImpl: ((ViewController) -> Void)? var pushImpl: ((ViewController) -> Void)? - var navigateToProfileImpl: ((EnginePeer) -> Void)? let arguments = ChannelStatsControllerArguments(context: context, loadDetailedGraph: { graph, x -> Signal in return statsContext.loadDetailedGraph(graph, x: x) @@ -954,8 +964,9 @@ public func channelStatsController(context: AccountContext, updatedPresentationD } presentImpl?(shareController) }, - openPeer: { peer in - navigateToProfileImpl?(peer) + openBoost: { boost in + let controller = PremiumGiftCodeScreen(context: context, subject: .boost(peerId, boost), action: {}) + pushImpl?(controller) }, expandBoosters: { updateState { $0.withUpdatedBoostersExpanded(true) } @@ -1087,11 +1098,6 @@ public func channelStatsController(context: AccountContext, updatedPresentationD pushImpl = { [weak controller] c in controller?.push(c) } - navigateToProfileImpl = { [weak controller] peer in - if let navigationController = controller?.navigationController as? NavigationController, let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: peer.largeProfileImage != nil, fromChat: false, requestsContext: nil) { - navigationController.pushViewController(controller) - } - } return controller } diff --git a/submodules/TelegramCore/Sources/State/ChannelBoost.swift b/submodules/TelegramCore/Sources/State/ChannelBoost.swift index 3a6295d86f..d8f029873a 100644 --- a/submodules/TelegramCore/Sources/State/ChannelBoost.swift +++ b/submodules/TelegramCore/Sources/State/ChannelBoost.swift @@ -10,6 +10,14 @@ public struct MyBoostStatus: Equatable { public let date: Int32 public let expires: Int32 public let cooldownUntil: Int32? + + public init(slot: Int32, peer: EnginePeer?, date: Int32, expires: Int32, cooldownUntil: Int32?) { + self.slot = slot + self.peer = peer + self.date = date + self.expires = expires + self.cooldownUntil = cooldownUntil + } } public let boosts: [Boost] @@ -175,7 +183,7 @@ private final class ChannelBoostersContextImpl { var result: [ChannelBoostersContext.State.Boost] = [] for boost in cachedResult.boosts { let peer = boost.peerId.flatMap { transaction.getPeer($0) } - result.append(ChannelBoostersContext.State.Boost(flags: ChannelBoostersContext.State.Boost.Flags(rawValue: boost.flags), id: boost.id, peer: peer.flatMap { EnginePeer($0) }, date: boost.date, expires: boost.expires, multiplier: boost.multiplier)) + result.append(ChannelBoostersContext.State.Boost(flags: ChannelBoostersContext.State.Boost.Flags(rawValue: boost.flags), id: boost.id, peer: peer.flatMap { EnginePeer($0) }, date: boost.date, expires: boost.expires, multiplier: boost.multiplier, slug: boost.slug)) } return (result, cachedResult.count, true) } else { @@ -256,7 +264,6 @@ private final class ChannelBoostersContextImpl { switch boost { case let .boost(flags, id, userId, giveawayMessageId, date, expires, usedGiftSlug, multiplier): let _ = giveawayMessageId - let _ = usedGiftSlug var boostFlags: ChannelBoostersContext.State.Boost.Flags = [] var boostPeer: EnginePeer? if let userId = userId { @@ -274,7 +281,7 @@ private final class ChannelBoostersContextImpl { if (flags & (1 << 3)) != 0 { boostFlags.insert(.isUnclaimed) } - resultBoosts.append(ChannelBoostersContext.State.Boost(flags: boostFlags, id: id, peer: boostPeer, date: date, expires: expires, multiplier: multiplier ?? 1)) + resultBoosts.append(ChannelBoostersContext.State.Boost(flags: boostFlags, id: id, peer: boostPeer, date: date, expires: expires, multiplier: multiplier ?? 1, slug: usedGiftSlug)) } } if populateCache { @@ -357,6 +364,7 @@ public final class ChannelBoostersContext { public var date: Int32 public var expires: Int32 public var multiplier: Int32 + public var slug: String? } public var boosts: [Boost] public var isLoadingMore: Bool @@ -418,6 +426,7 @@ private final class CachedChannelBoosters: Codable { case date case expires case multiplier + case slug } var flags: Int32 @@ -426,14 +435,16 @@ private final class CachedChannelBoosters: Codable { var date: Int32 var expires: Int32 var multiplier: Int32 + var slug: String? - init(flags: Int32, id: String, peerId: EnginePeer.Id?, date: Int32, expires: Int32, multiplier: Int32) { + init(flags: Int32, id: String, peerId: EnginePeer.Id?, date: Int32, expires: Int32, multiplier: Int32, slug: String?) { self.flags = flags self.id = id self.peerId = peerId self.date = date self.expires = expires self.multiplier = multiplier + self.slug = slug } init(from decoder: Decoder) throws { @@ -445,6 +456,7 @@ private final class CachedChannelBoosters: Codable { self.date = try container.decode(Int32.self, forKey: .date) self.expires = try container.decode(Int32.self, forKey: .expires) self.multiplier = try container.decode(Int32.self, forKey: .multiplier) + self.slug = try container.decodeIfPresent(String.self, forKey: .slug) } func encode(to encoder: Encoder) throws { @@ -456,6 +468,7 @@ private final class CachedChannelBoosters: Codable { try container.encode(self.date, forKey: .date) try container.encode(self.expires, forKey: .expires) try container.encode(self.multiplier, forKey: .multiplier) + try container.encodeIfPresent(self.slug, forKey: .slug) } } @@ -469,7 +482,7 @@ private final class CachedChannelBoosters: Codable { } init(boosts: [ChannelBoostersContext.State.Boost], count: Int32) { - self.boosts = boosts.map { CachedBoost(flags: $0.flags.rawValue, id: $0.id, peerId: $0.peer?.id, date: $0.date, expires: $0.expires, multiplier: $0.multiplier) } + self.boosts = boosts.map { CachedBoost(flags: $0.flags.rawValue, id: $0.id, peerId: $0.peer?.id, date: $0.date, expires: $0.expires, multiplier: $0.multiplier, slug: $0.slug) } self.count = count } diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index e9f59997e2..f87ae12873 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -896,7 +896,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur var dismissImpl: (() -> Void)? let controller = PremiumGiftCodeScreen( context: context, - giftCode: giftCode, + subject: .giftCode(giftCode), forceDark: forceDark, action: { dismissImpl?()