import Foundation import UIKit import Display import AsyncDisplayKit import TelegramCore import SwiftSignalKit import AccountContext import TelegramPresentationData import TelegramUIPreferences import PresentationDataUtils import ComponentFlow import ViewControllerComponent import MultilineTextComponent import BalancedTextComponent import BundleIconComponent import Markdown import TextFormat import ButtonComponent import SolidRoundedButtonComponent import BlurredBackgroundComponent import UndoUI import ConfettiEffect import PremiumPeerShortcutComponent import ScrollComponent import ResizableSheetComponent import GlassBarButtonComponent private final class SheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext let peerId: EnginePeer.Id let peer: EnginePeer? let memberPeer: EnginePeer? let mode: PremiumBoostLevelsScreen.Mode let status: ChannelBoostStatus? let boostState: InternalBoostState.DisplayData? let bottomInset: CGFloat let boost: () -> Void let copyLink: (String) -> Void let dismiss: () -> Void let openStats: (() -> Void)? let openGift: (() -> Void)? let openPeer: ((EnginePeer) -> Void)? let updated: () -> Void init( context: AccountContext, peerId: EnginePeer.Id, peer: EnginePeer?, memberPeer: EnginePeer?, mode: PremiumBoostLevelsScreen.Mode, status: ChannelBoostStatus?, boostState: InternalBoostState.DisplayData?, bottomInset: CGFloat, boost: @escaping () -> Void, copyLink: @escaping (String) -> Void, dismiss: @escaping () -> Void, openStats: (() -> Void)?, openGift: (() -> Void)?, openPeer: ((EnginePeer) -> Void)?, updated: @escaping () -> Void ) { self.context = context self.peerId = peerId self.peer = peer self.memberPeer = memberPeer self.mode = mode self.status = status self.boostState = boostState self.bottomInset = bottomInset self.boost = boost self.copyLink = copyLink self.dismiss = dismiss self.openStats = openStats self.openGift = openGift self.openPeer = openPeer self.updated = updated } static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool { if lhs.context !== rhs.context { return false } if lhs.peerId != rhs.peerId { return false } if lhs.peer != rhs.peer { return false } if lhs.memberPeer != rhs.memberPeer { return false } if lhs.mode != rhs.mode { return false } if lhs.status != rhs.status { return false } if lhs.boostState != rhs.boostState { return false } if lhs.bottomInset != rhs.bottomInset { return false } return true } final class State: ComponentState { var cachedChevronImage: (UIImage, PresentationTheme)? var cachedIconImage: UIImage? } func makeState() -> State { return State() } static var body: Body { let iconBackground = Child(Image.self) let icon = Child(BundleIconComponent.self) let peerShortcut = Child(Button.self) let text = Child(BalancedTextComponent.self) let alternateText = Child(List.self) let limit = Child(PremiumLimitDisplayComponent.self) let linkButton = Child(SolidRoundedButtonComponent.self) let boostButton = Child(ButtonComponent.self) let copyButton = Child(ButtonComponent.self) let orLeftLine = Child(Rectangle.self) let orRightLine = Child(Rectangle.self) let orText = Child(MultilineTextComponent.self) let giftText = Child(BalancedTextComponent.self) let levels = Child(List.self) return { context in let component = context.component let environment = context.environment[ViewControllerComponentContainer.Environment.self].value let theme = environment.theme let strings = environment.strings let state = context.state let premiumConfiguration = PremiumConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 }) let sideInset: CGFloat = 16.0 let textSideInset: CGFloat = 32.0 let iconName = "Premium/Boost" let peerName = component.peer?.compactDisplayTitle ?? "" var isGroup = false if case let .channel(channel) = component.peer, case .group = channel.info { isGroup = true } let level: Int let boosts: Int let remaining: Int? let progress: CGFloat let myBoostCount: Int if let boostState = component.boostState { level = Int(boostState.level) boosts = Int(boostState.boosts) if let nextLevelBoosts = boostState.nextLevelBoosts { remaining = max(0, Int(nextLevelBoosts - boostState.boosts)) progress = max(0.0, min(1.0, CGFloat(boostState.boosts - boostState.currentLevelBoosts) / CGFloat(nextLevelBoosts - boostState.currentLevelBoosts))) } else { remaining = nil progress = 1.0 } myBoostCount = Int(boostState.myBoostCount) } else if let status = component.status { level = status.level boosts = status.boosts if let nextLevelBoosts = status.nextLevelBoosts { remaining = max(0, nextLevelBoosts - status.boosts) progress = max(0.0, min(1.0, CGFloat(status.boosts - status.currentLevelBoosts) / CGFloat(nextLevelBoosts - status.currentLevelBoosts))) } else { remaining = nil progress = 1.0 } myBoostCount = 0 } else { level = 0 boosts = 0 remaining = nil progress = 0.0 myBoostCount = 0 } var textString = "" var isCurrent = false switch component.mode { case let .owner(subject): if let remaining { var needsSecondParagraph = true if let subject { let requiredLevel = subject.requiredLevel(group: isGroup, context: context.component.context, configuration: premiumConfiguration) let storiesString = strings.ChannelBoost_StoriesPerDay(Int32(level) + 1) let valueString = strings.ChannelBoost_MoreBoosts(Int32(remaining)) switch subject { case .stories: if level == 0 { textString = isGroup ? strings.GroupBoost_EnableStoriesText(valueString).string : strings.ChannelBoost_EnableStoriesText(valueString).string } else { textString = isGroup ? strings.GroupBoost_IncreaseLimitText(valueString, storiesString).string : strings.ChannelBoost_IncreaseLimitText(valueString, storiesString).string } needsSecondParagraph = isGroup case let .channelReactions(reactionCount): textString = strings.ChannelBoost_CustomReactionsText("\(reactionCount)", "\(reactionCount)").string needsSecondParagraph = false case .nameColors: textString = strings.ChannelBoost_EnableNameColorLevelText("\(requiredLevel)").string case .nameIcon: textString = strings.ChannelBoost_EnableNameIconLevelText("\(requiredLevel)").string case .profileColors: textString = isGroup ? strings.GroupBoost_EnableProfileColorLevelText("\(requiredLevel)").string : strings.ChannelBoost_EnableProfileColorLevelText("\(requiredLevel)").string case .profileIcon: textString = isGroup ? strings.GroupBoost_EnableProfileIconLevelText("\(requiredLevel)").string : strings.ChannelBoost_EnableProfileIconLevelText("\(premiumConfiguration.minChannelProfileIconLevel)").string case .emojiStatus: textString = isGroup ? strings.GroupBoost_EnableEmojiStatusLevelText("\(requiredLevel)").string : strings.ChannelBoost_EnableEmojiStatusLevelText("\(requiredLevel)").string case .wallpaper: textString = isGroup ? strings.GroupBoost_EnableWallpaperLevelText("\(requiredLevel)").string : strings.ChannelBoost_EnableWallpaperLevelText("\(requiredLevel)").string case .customWallpaper: textString = isGroup ? strings.GroupBoost_EnableCustomWallpaperLevelText("\(requiredLevel)").string : strings.ChannelBoost_EnableCustomWallpaperLevelText("\(requiredLevel)").string case .audioTranscription: textString = "" case .emojiPack: textString = strings.GroupBoost_EnableEmojiPackLevelText("\(requiredLevel)").string case .noAds: textString = strings.ChannelBoost_EnableNoAdsLevelText("\(requiredLevel)").string case .wearGift: textString = strings.ChannelBoost_WearGiftLevelText("\(requiredLevel)").string case .autoTranslate: textString = strings.ChannelBoost_AutoTranslateLevelText("\(requiredLevel)").string } } else { let boostsString = strings.ChannelBoost_MoreBoostsNeeded_Boosts(Int32(remaining)) if myBoostCount > 0 { if remaining == 0 { textString = isGroup ? strings.GroupBoost_MoreBoostsNeeded_Boosted_Level_Text("\(level + 1)").string : strings.ChannelBoost_MoreBoostsNeeded_Boosted_Level_Text("\(level + 1)").string } else { textString = strings.ChannelBoost_MoreBoostsNeeded_Boosted_Text(boostsString).string } } else { textString = strings.ChannelBoost_MoreBoostsNeeded_Text(peerName, boostsString).string } } if needsSecondParagraph { textString += " \(isGroup ? strings.GroupBoost_PremiumUsersCanBoost : strings.ChannelBoost_PremiumUsersCanBoost)" } } else { textString = strings.ChannelBoost_MaxLevelReached_Text(peerName, "\(level)").string } case let .user(mode): switch mode { case let .groupPeer(_, peerBoostCount): let memberName = component.memberPeer?.compactDisplayTitle ?? "" let timesString = strings.GroupBoost_MemberBoosted_Times(Int32(peerBoostCount)) let memberString = strings.GroupBoost_MemberBoosted(memberName, timesString).string if myBoostCount > 0 { if let remaining, remaining != 0 { let boostsString = strings.ChannelBoost_MoreBoostsNeeded_Boosts(Int32(remaining)) textString = "\(memberString) \(strings.ChannelBoost_MoreBoostsNeeded_Boosted_Text(boostsString).string)" } else { textString = memberString } } else { textString = "\(memberString) \(strings.GroupBoost_MemberBoosted_BoostForBadge(peerName).string)" } isCurrent = true case let .unrestrict(unrestrictCount): let timesString = strings.GroupBoost_BoostToUnrestrict_Times(Int32(unrestrictCount)) textString = strings.GroupBoost_BoostToUnrestrict(timesString, peerName).string isCurrent = true default: if let remaining { let boostsString = strings.ChannelBoost_MoreBoostsNeeded_Boosts(Int32(remaining)) if myBoostCount > 0 { if remaining == 0 { textString = isGroup ? strings.GroupBoost_MoreBoostsNeeded_Boosted_Level_Text("\(level + 1)").string : strings.ChannelBoost_MoreBoostsNeeded_Boosted_Level_Text("\(level + 1)").string } else { textString = strings.ChannelBoost_MoreBoostsNeeded_Boosted_Text(boostsString).string } } else { textString = strings.ChannelBoost_MoreBoostsNeeded_Text(peerName, boostsString).string } } else { textString = strings.ChannelBoost_MaxLevelReached_Text(peerName, "\(level)").string } isCurrent = mode == .current } case .features: textString = isGroup ? strings.GroupBoost_AdditionalFeaturesText : strings.ChannelBoost_AdditionalFeaturesText } let defaultTitle = strings.ChannelBoost_Level("\(level)").string let defaultValue = "" let premiumValue = strings.ChannelBoost_Level("\(level + 1)").string let premiumTitle = "" var contentSize: CGSize = CGSize(width: context.availableSize.width, height: 56.0) let textFont = Font.regular(15.0) let boldTextFont = Font.semibold(15.0) let textColor = theme.actionSheet.primaryTextColor let linkColor = theme.actionSheet.controlAccentColor 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) }) let gradientColors = [ UIColor(rgb: 0x0077ff), UIColor(rgb: 0x6b93ff), UIColor(rgb: 0x8878ff), UIColor(rgb: 0xe46ace) ] if case let .user(mode) = component.mode, case .external = mode, let peer = component.peer { contentSize.height += 10.0 let peerShortcut = peerShortcut.update( component: Button( content: AnyComponent( PremiumPeerShortcutComponent( context: component.context, theme: theme, peer: peer ) ), action: { component.dismiss() Queue.mainQueue().after(0.35) { component.openPeer?(peer) } } ), availableSize: CGSize(width: context.availableSize.width - 32.0, height: context.availableSize.height), transition: .immediate ) context.add(peerShortcut .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + peerShortcut.size.height / 2.0)) ) contentSize.height += peerShortcut.size.height + 2.0 } if case .features = component.mode { contentSize.height -= 14.0 let iconSize = CGSize(width: 90.0, height: 90.0) let gradientImage: UIImage if let current = state.cachedIconImage { gradientImage = current } else { gradientImage = generateFilledCircleImage(diameter: iconSize.width, color: theme.actionSheet.controlAccentColor)! context.state.cachedIconImage = gradientImage } let iconBackground = iconBackground.update( component: Image(image: gradientImage), availableSize: iconSize, transition: .immediate ) context.add(iconBackground .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + iconBackground.size.height / 2.0)) ) let icon = icon.update( component: BundleIconComponent( name: "Premium/BoostLarge", tintColor: .white ), availableSize: CGSize(width: 90.0, height: 90.0), transition: .immediate ) context.add(icon .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + iconBackground.size.height / 2.0)) ) contentSize.height += iconSize.height contentSize.height += 52.0 } else { let limit = limit.update( component: PremiumLimitDisplayComponent( inactiveColor: theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.3), activeColors: gradientColors, inactiveTitle: defaultTitle, inactiveValue: defaultValue, inactiveTitleColor: theme.list.itemPrimaryTextColor, activeTitle: premiumTitle, activeValue: premiumValue, activeTitleColor: .white, badgeIconName: iconName, badgeText: "\(boosts)", badgePosition: progress, badgeGraphPosition: progress, invertProgress: true, isPremiumDisabled: false ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height), transition: context.transition ) context.add(limit .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + limit.size.height / 2.0)) ) contentSize.height += limit.size.height + 23.0 } if myBoostCount > 0 { let alternateTitle = isCurrent ? strings.ChannelBoost_YouBoostedChannelText(peerName).string : strings.ChannelBoost_YouBoostedOtherChannelText var alternateBadge: String? if myBoostCount > 1 { alternateBadge = "X\(myBoostCount)" } let alternateText = alternateText.update( component: List( [ AnyComponentWithIdentity( id: "title", component: AnyComponent( BoostedTitleContent(text: NSAttributedString(string: alternateTitle, font: Font.semibold(15.0), textColor: textColor), badge: alternateBadge) ) ), AnyComponentWithIdentity( id: "text", component: AnyComponent( BalancedTextComponent( text: .markdown(text: textString, attributes: markdownAttributes), horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.1 ) ) ) ], centerAlignment: true ), availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), transition: .immediate ) context.add(alternateText .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + alternateText.size.height / 2.0)) .appear(ComponentTransition.Appear({ _, view, transition in transition.animatePosition(view: view, from: CGPoint(x: 0.0, y: 64.0), to: .zero, additive: true) transition.animateAlpha(view: view, from: 0.0, to: 1.0) })) .disappear(ComponentTransition.Disappear({ view, transition, completion in view.superview?.sendSubviewToBack(view) transition.animatePosition(view: view, from: .zero, to: CGPoint(x: 0.0, y: -64.0), additive: true) transition.setAlpha(view: view, alpha: 0.0, completion: { _ in completion() }) })) ) contentSize.height += alternateText.size.height + 20.0 } else { let text = text.update( component: BalancedTextComponent( text: .markdown(text: textString, attributes: markdownAttributes), horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.2 ), availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), transition: .immediate ) context.add(text .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + text.size.height / 2.0)) .appear(ComponentTransition.Appear({ _, view, transition in transition.animatePosition(view: view, from: CGPoint(x: 0.0, y: 64.0), to: .zero, additive: true) transition.animateAlpha(view: view, from: 0.0, to: 1.0) })) .disappear(ComponentTransition.Disappear({ view, transition, completion in view.superview?.sendSubviewToBack(view) transition.animatePosition(view: view, from: .zero, to: CGPoint(x: 0.0, y: -64.0), additive: true) transition.setAlpha(view: view, alpha: 0.0, completion: { _ in completion() }) })) ) contentSize.height += text.size.height + 20.0 } if case .owner = component.mode, let status = component.status { contentSize.height += 7.0 let linkButton = linkButton.update( component: SolidRoundedButtonComponent( title: status.url.replacingOccurrences(of: "https://", with: ""), theme: SolidRoundedButtonComponent.Theme( backgroundColor: theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.3), backgroundColors: [], foregroundColor: theme.list.itemPrimaryTextColor ), font: .regular, fontSize: 17.0, height: 52.0, cornerRadius: 26.0, action: { component.copyLink(status.url) component.dismiss() } ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), transition: context.transition ) context.add(linkButton .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + linkButton.size.height / 2.0)) ) contentSize.height += linkButton.size.height + 16.0 let boostButton = boostButton.update( component: ButtonComponent( background: ButtonComponent.Background( style: .glass, color: theme.list.itemCheckColors.fillColor, foreground: theme.list.itemCheckColors.foregroundColor, pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8) ), content: AnyComponentWithIdentity(id: 0, component: AnyComponent(Text(text: strings.ChannelBoost_Boost, font: Font.semibold(17.0), color: theme.list.itemCheckColors.foregroundColor))), action: { component.boost() } ), availableSize: CGSize(width: (context.availableSize.width - 8.0 - sideInset * 2.0) / 2.0, height: 52.0), transition: context.transition ) let copyButton = copyButton.update( component: ButtonComponent( background: ButtonComponent.Background( style: .glass, color: theme.list.itemCheckColors.fillColor, foreground: theme.list.itemCheckColors.foregroundColor, pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8) ), content: AnyComponentWithIdentity(id: 0, component: AnyComponent(Text(text: strings.ChannelBoost_Copy, font: Font.semibold(17.0), color: theme.list.itemCheckColors.foregroundColor))), action: { component.copyLink(status.url) component.dismiss() } ), availableSize: CGSize(width: (context.availableSize.width - 8.0 - sideInset * 2.0) / 2.0, height: 52.0), transition: context.transition ) let boostButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: contentSize.height), size: boostButton.size) context.add(boostButton .position(boostButtonFrame.center) ) let copyButtonFrame = CGRect(origin: CGPoint(x: context.availableSize.width - sideInset - copyButton.size.width, y: contentSize.height), size: copyButton.size) context.add(copyButton .position(copyButtonFrame.center) ) contentSize.height += boostButton.size.height if premiumConfiguration.giveawayGiftsPurchaseAvailable { let orText = orText.update( component: MultilineTextComponent(text: .plain(NSAttributedString(string: strings.ChannelBoost_Or, font: Font.regular(15.0), textColor: textColor.withAlphaComponent(0.8), paragraphAlignment: .center))), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height), transition: .immediate ) context.add(orText .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + 27.0)) ) let orLeftLine = orLeftLine.update( component: Rectangle(color: theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.3)), availableSize: CGSize(width: 90.0, height: 1.0 - UIScreenPixel), transition: .immediate ) context.add(orLeftLine .position(CGPoint(x: context.availableSize.width / 2.0 - orText.size.width / 2.0 - 11.0 - 45.0, y: contentSize.height + 27.0)) ) let orRightLine = orRightLine.update( component: Rectangle(color: theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.3)), availableSize: CGSize(width: 90.0, height: 1.0 - UIScreenPixel), transition: .immediate ) context.add(orRightLine .position(CGPoint(x: context.availableSize.width / 2.0 + orText.size.width / 2.0 + 11.0 + 45.0, y: contentSize.height + 27.0)) ) if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== theme { state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: linkColor)!, theme) } let giftString = isGroup ? strings.Premium_Group_BoostByGiveawayDescription : strings.Premium_BoostByGiveawayDescription let giftAttributedString = parseMarkdownIntoAttributedString(giftString, attributes: markdownAttributes).mutableCopy() as! NSMutableAttributedString if let range = giftAttributedString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 { giftAttributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: giftAttributedString.string)) } let giftText = giftText.update( component: BalancedTextComponent( text: .plain(giftAttributedString), horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.1, highlightColor: linkColor.withAlphaComponent(0.1), highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), highlightAction: { _ in return nil }, tapAction: { _, _ in component.openGift?() } ), availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), transition: .immediate ) context.add(giftText .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + 50.0 + giftText.size.height / 2.0)) ) contentSize.height += giftText.size.height + 50.0 + 23.0 } } var nextLevels: ClosedRange? if level < 10 { nextLevels = Int32(level) + 1 ... 10 } var levelItems: [AnyComponentWithIdentity] = [] var nameColorsAtLevel: [(Int32, Int32)] = [] var nameColorsCountMap: [Int32: Int32] = [:] for color in context.component.context.peerNameColors.displayOrder { if let level = context.component.context.peerNameColors.nameColorsChannelMinRequiredBoostLevel[color] { if let current = nameColorsCountMap[level] { nameColorsCountMap[level] = current + 1 } else { nameColorsCountMap[level] = 1 } } } for (key, value) in nameColorsCountMap { nameColorsAtLevel.append((key, value)) } var profileColorsAtLevel: [(Int32, Int32)] = [] var profileColorsCountMap: [Int32: Int32] = [:] for color in context.component.context.peerNameColors.profileDisplayOrder { if let level = isGroup ? context.component.context.peerNameColors.profileColorsGroupMinRequiredBoostLevel[color] : context.component.context.peerNameColors.profileColorsChannelMinRequiredBoostLevel[color] { if let current = profileColorsCountMap[level] { profileColorsCountMap[level] = current + 1 } else { profileColorsCountMap[level] = 1 } } } for (key, value) in profileColorsCountMap { profileColorsAtLevel.append((key, value)) } var isFeatures = false if case .features = component.mode { isFeatures = true } func layoutLevel(_ level: Int32) { var perks: [LevelSectionComponent.Perk] = [] if !isGroup && level >= requiredBoostSubjectLevel(subject: .autoTranslate, group: isGroup, context: component.context, configuration: premiumConfiguration) { perks.append(.autoTranslate) } perks.append(.story(level)) if !isGroup { perks.append(.reaction(level)) } var nameColorsCount: Int32 = 0 for (colorLevel, count) in nameColorsAtLevel { if level >= colorLevel && colorLevel == 1 { nameColorsCount = count } } if !isGroup && nameColorsCount > 0 { perks.append(.nameColor(nameColorsCount)) } var profileColorsCount: Int32 = 0 for (colorLevel, count) in profileColorsAtLevel { if level >= colorLevel { profileColorsCount += count } } if profileColorsCount > 0 { perks.append(.profileColor(profileColorsCount)) } if isGroup && level >= requiredBoostSubjectLevel(subject: .emojiPack, group: isGroup, context: component.context, configuration: premiumConfiguration) { perks.append(.emojiPack) } if level >= requiredBoostSubjectLevel(subject: .profileIcon, group: isGroup, context: component.context, configuration: premiumConfiguration) { perks.append(.profileIcon) } if isGroup && level >= requiredBoostSubjectLevel(subject: .audioTranscription, group: isGroup, context: component.context, configuration: premiumConfiguration) { perks.append(.audioTranscription) } var linkColorsCount: Int32 = 0 for (colorLevel, count) in nameColorsAtLevel { if level >= colorLevel { linkColorsCount += count } } if !isGroup && linkColorsCount > 0 { perks.append(.linkColor(linkColorsCount)) } if !isGroup && level >= requiredBoostSubjectLevel(subject: .nameIcon, group: isGroup, context: component.context, configuration: premiumConfiguration) { perks.append(.linkIcon) } if level >= requiredBoostSubjectLevel(subject: .emojiStatus, group: isGroup, context: component.context, configuration: premiumConfiguration) { perks.append(.emojiStatus) } if level >= requiredBoostSubjectLevel(subject: .wallpaper, group: isGroup, context: component.context, configuration: premiumConfiguration) { perks.append(.wallpaper(8)) } if level >= requiredBoostSubjectLevel(subject: .customWallpaper, group: isGroup, context: component.context, configuration: premiumConfiguration) { perks.append(.customWallpaper) } if !isGroup && level >= requiredBoostSubjectLevel(subject: .noAds, group: isGroup, context: component.context, configuration: premiumConfiguration) { perks.append(.noAds) } levelItems.append( AnyComponentWithIdentity( id: level, component: AnyComponent( LevelSectionComponent( theme: theme, strings: strings, level: level, isFirst: !isFeatures && levelItems.isEmpty, perks: perks.reversed(), isGroup: isGroup ) ) ) ) } if let nextLevels { for level in nextLevels { layoutLevel(level) } } if !isGroup { let noAdsLevel = requiredBoostSubjectLevel(subject: .noAds, group: false, context: component.context, configuration: premiumConfiguration) if let nextLevels, noAdsLevel <= nextLevels.upperBound { } else if level < noAdsLevel { layoutLevel(noAdsLevel) } } if !levelItems.isEmpty { let levels = levels.update( component: List(levelItems), availableSize: CGSize(width: context.availableSize.width, height: 100000.0), transition: context.transition ) context.add(levels .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + levels.size.height / 2.0 )) ) contentSize.height += levels.size.height + 80.0 contentSize.height += 60.0 } contentSize.height += component.bottomInset return contentSize } } } private final class PremiumBoostLevelsSheetComponent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext let peerId: EnginePeer.Id let mode: PremiumBoostLevelsScreen.Mode let status: ChannelBoostStatus? let myBoostStatus: MyBoostStatus? let replacedBoosts: (Int32, [EnginePeer])? let openStats: (() -> Void)? let openGift: (() -> Void)? let openPeer: ((EnginePeer) -> Void)? let forceDark: Bool init( context: AccountContext, peerId: EnginePeer.Id, mode: PremiumBoostLevelsScreen.Mode, status: ChannelBoostStatus?, myBoostStatus: MyBoostStatus?, replacedBoosts: (Int32, [EnginePeer])?, openStats: (() -> Void)?, openGift: (() -> Void)?, openPeer: ((EnginePeer) -> Void)?, forceDark: Bool ) { self.context = context self.peerId = peerId self.mode = mode self.status = status self.myBoostStatus = myBoostStatus self.replacedBoosts = replacedBoosts self.openStats = openStats self.openGift = openGift self.openPeer = openPeer self.forceDark = forceDark } static func ==(lhs: PremiumBoostLevelsSheetComponent, rhs: PremiumBoostLevelsSheetComponent) -> Bool { return true } final class State: ComponentState { private let context: AccountContext private let peerId: EnginePeer.Id fileprivate let mode: PremiumBoostLevelsScreen.Mode fileprivate let status: ChannelBoostStatus? private let myBoostStatus: MyBoostStatus? private let openPeer: ((EnginePeer) -> Void)? private let forceDark: Bool weak var controller: PremiumBoostLevelsScreen? private(set) var peer: EnginePeer? private(set) var memberPeer: EnginePeer? private var peerDisposable: Disposable? fileprivate var currentMyBoostCount: Int32 = 0 private var myBoostCount: Int32 = 0 private var availableBoosts: [MyBoostStatus.Boost] = [] private var occupiedBoosts: [MyBoostStatus.Boost] = [] private let updatedState = Promise() private(set) var boostState: InternalBoostState.DisplayData? init( context: AccountContext, peerId: EnginePeer.Id, mode: PremiumBoostLevelsScreen.Mode, status: ChannelBoostStatus?, myBoostStatus: MyBoostStatus?, replacedBoosts: (Int32, [EnginePeer])?, openPeer: ((EnginePeer) -> Void)?, forceDark: Bool ) { self.context = context self.peerId = peerId self.mode = mode self.status = status self.myBoostStatus = myBoostStatus self.openPeer = openPeer self.forceDark = forceDark super.init() var userId: EnginePeer.Id? if case let .user(mode) = mode, case let .groupPeer(peerId, _) = mode { userId = peerId } var peerIds: [EnginePeer.Id] = [peerId] if let userId { peerIds.append(userId) } self.peerDisposable = (context.engine.data.get( EngineDataMap(peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))) ) |> deliverOnMainQueue).startStrict(next: { [weak self] peers in guard let self else { return } if let maybePeer = peers[peerId] { self.peer = maybePeer } if let userId, let maybePeer = peers[userId] { self.memberPeer = maybePeer } self.updated() }) if let status, let myBoostStatus { var myBoostCount: Int32 = 0 var currentMyBoostCount: Int32 = 0 var availableBoosts: [MyBoostStatus.Boost] = [] var occupiedBoosts: [MyBoostStatus.Boost] = [] for boost in myBoostStatus.boosts { if let boostPeer = boost.peer { if boostPeer.id == peerId { myBoostCount += 1 } else { occupiedBoosts.append(boost) } } else { availableBoosts.append(boost) } } let boosts = max(Int32(status.boosts), myBoostCount) let initialState = InternalBoostState(level: Int32(status.level), currentLevelBoosts: Int32(status.currentLevelBoosts), nextLevelBoosts: status.nextLevelBoosts.flatMap(Int32.init), boosts: boosts) self.boostState = initialState.displayData(myBoostCount: myBoostCount, currentMyBoostCount: 0, replacedBoosts: replacedBoosts?.0) self.updatedState.set(.single(InternalBoostState(level: Int32(status.level), currentLevelBoosts: Int32(status.currentLevelBoosts), nextLevelBoosts: status.nextLevelBoosts.flatMap(Int32.init), boosts: boosts + 1))) if let (replacedBoosts, sourcePeers) = replacedBoosts { currentMyBoostCount += 1 self.boostState = initialState.displayData(myBoostCount: myBoostCount, currentMyBoostCount: 1) Queue.mainQueue().justDispatch { self.updated(transition: .easeInOut(duration: 0.2)) } Queue.mainQueue().after(0.3) { guard let controller = self.controller else { return } let presentationData = context.sharedContext.currentPresentationData.with { $0 } var groupCount: Int32 = 0 var channelCount: Int32 = 0 for peer in sourcePeers { if case let .channel(channel) = peer { switch channel.info { case .broadcast: channelCount += 1 case .group: groupCount += 1 } } } let otherText: String if channelCount > 0 && groupCount == 0 { otherText = presentationData.strings.ReassignBoost_OtherChannels(channelCount) } else if groupCount > 0 && channelCount == 0 { otherText = presentationData.strings.ReassignBoost_OtherGroups(groupCount) } else { otherText = presentationData.strings.ReassignBoost_OtherGroupsAndChannels(Int32(sourcePeers.count)) } let text = presentationData.strings.ReassignBoost_Success(presentationData.strings.ReassignBoost_Boosts(replacedBoosts), otherText).string let undoController = UndoOverlayController(presentationData: presentationData, content: .universal(animation: "BoostReplace", scale: 0.066, colors: [:], title: nil, text: text, customUndoText: nil, timeout: 4.0), elevatedLayout: false, position: .top, action: { _ in return true }) controller.present(undoController, in: .current) } } self.availableBoosts = availableBoosts self.occupiedBoosts = occupiedBoosts self.myBoostCount = myBoostCount self.currentMyBoostCount = currentMyBoostCount } } deinit { self.peerDisposable?.dispose() } func updateBoostState() { guard let controller = self.controller else { return } let context = self.context let peerId = self.peerId let isGroup = self.isGroup ?? false let mode = self.mode let status = self.status let isPremium = context.isPremium let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with({ $0 })) let canBoostAgain = premiumConfiguration.boostsPerGiftCount > 0 let presentationData = context.sharedContext.currentPresentationData.with { $0 } let forceDark = self.forceDark let boostStatusUpdated = controller.boostStatusUpdated if let _ = status?.nextLevelBoosts { if let availableBoost = self.availableBoosts.first { self.currentMyBoostCount += 1 self.myBoostCount += 1 let _ = (context.engine.peers.applyChannelBoost(peerId: peerId, slots: [availableBoost.slot]) |> deliverOnMainQueue).startStandalone(next: { [weak self] myBoostStatus in self?.updatedState.set(context.engine.peers.getChannelBoostStatus(peerId: peerId) |> beforeNext { boostStatus in if let boostStatus, let myBoostStatus { Queue.mainQueue().async { boostStatusUpdated(boostStatus, myBoostStatus) } } } |> map { status in if let status { return InternalBoostState(level: Int32(status.level), currentLevelBoosts: Int32(status.currentLevelBoosts), nextLevelBoosts: status.nextLevelBoosts.flatMap(Int32.init), boosts: Int32(status.boosts + 1)) } else { return nil } }) }) let _ = (self.updatedState.get() |> take(1) |> deliverOnMainQueue).startStandalone(next: { [weak self] state in guard let self, let state else { return } self.boostState = state.displayData(myBoostCount: self.myBoostCount, currentMyBoostCount: self.currentMyBoostCount) self.updated(transition: .easeInOut(duration: 0.2)) self.animateSuccess() }) self.availableBoosts.removeFirst() } else if !self.occupiedBoosts.isEmpty, let myBoostStatus = self.myBoostStatus { if canBoostAgain { let navigationController = controller.navigationController let openPeer = self.openPeer var dismissReplaceImpl: (() -> Void)? let replaceController = ReplaceBoostScreen(context: context, peerId: peerId, myBoostStatus: myBoostStatus, replaceBoosts: { slots in var sourcePeerIds = Set() var sourcePeers: [EnginePeer] = [] for boost in myBoostStatus.boosts { if slots.contains(boost.slot) { if let peer = boost.peer { if !sourcePeerIds.contains(peer.id) { sourcePeerIds.insert(peer.id) sourcePeers.append(peer) } } } } let _ = context.engine.peers.applyChannelBoost(peerId: peerId, slots: slots).startStandalone(completed: { let _ = combineLatest( queue: Queue.mainQueue(), context.engine.peers.getChannelBoostStatus(peerId: peerId), context.engine.peers.getMyBoostStatus() ).startStandalone(next: { boostStatus, myBoostStatus in dismissReplaceImpl?() if let boostStatus, let myBoostStatus { boostStatusUpdated(boostStatus, myBoostStatus) } let levelsController = PremiumBoostLevelsScreen( context: context, peerId: peerId, mode: mode, status: boostStatus, myBoostStatus: myBoostStatus, replacedBoosts: (Int32(slots.count), sourcePeers), openStats: nil, openGift: nil, openPeer: openPeer, forceDark: forceDark ) levelsController.boostStatusUpdated = boostStatusUpdated if let navigationController { navigationController.pushViewController(levelsController, animated: true) } }) }) }) if let navigationController = controller.navigationController { controller.dismiss(animated: true) navigationController.pushViewController(replaceController, animated: true) } dismissReplaceImpl = { [weak replaceController] in replaceController?.dismiss(animated: true) } } else if let boost = self.occupiedBoosts.first, let occupiedPeer = boost.peer { if let cooldown = boost.cooldownUntil { let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) let timeout = cooldown - currentTime let valueText = timeIntervalString(strings: presentationData.strings, value: timeout, usage: .afterTime, preferLowerValue: false) let alertController = textAlertController( sharedContext: context.sharedContext, updatedPresentationData: nil, title: presentationData.strings.ChannelBoost_Error_BoostTooOftenTitle, text: isGroup ? presentationData.strings.GroupBoost_Error_BoostTooOftenText(valueText).string : presentationData.strings.ChannelBoost_Error_BoostTooOftenText(valueText).string, actions: [ TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {}) ], parseMarkdown: true ) controller.present(alertController, in: .window(.root)) } else { let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) |> deliverOnMainQueue).start(next: { [weak controller] peer in guard let peer, let controller else { return } let replaceController = replaceBoostConfirmationController(context: context, fromPeers: [occupiedPeer], toPeer: peer, commit: { [weak self] in self?.currentMyBoostCount += 1 self?.myBoostCount += 1 let _ = (context.engine.peers.applyChannelBoost(peerId: peerId, slots: [boost.slot]) |> deliverOnMainQueue).startStandalone(completed: { [weak self] in guard let self else { return } let _ = (self.updatedState.get() |> take(1) |> deliverOnMainQueue).startStandalone(next: { [weak self] state in guard let self, let state else { return } self.boostState = state.displayData(myBoostCount: self.myBoostCount, currentMyBoostCount: self.currentMyBoostCount) self.updated(transition: .easeInOut(duration: 0.2)) self.animateSuccess() }) }) }) controller.present(replaceController, in: .window(.root)) }) } } else { controller.dismiss(animated: true, completion: nil) } } else { if isPremium { if !canBoostAgain { controller.dismissAnimated() } else { let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) |> deliverOnMainQueue).start(next: { [weak controller] peer in guard let peer, let controller else { return } let alertController = textAlertController( sharedContext: context.sharedContext, updatedPresentationData: nil, title: presentationData.strings.ChannelBoost_MoreBoosts_Title, text: presentationData.strings.ChannelBoost_MoreBoosts_Text(peer.compactDisplayTitle, "\(premiumConfiguration.boostsPerGiftCount)").string, actions: [ TextAlertAction(type: .defaultAction, title: presentationData.strings.ChannelBoost_MoreBoosts_Gift, action: { [weak controller] in if let navigationController = controller?.navigationController { controller?.dismissAnimated() Queue.mainQueue().after(0.4) { let giftController = context.sharedContext.makePremiumGiftController(context: context, source: .channelBoost, completion: nil) navigationController.pushViewController(giftController, animated: true) } } }), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Close, action: {}) ], actionLayout: .vertical, parseMarkdown: true ) controller.present(alertController, in: .window(.root)) }) } } else { let alertController = textAlertController( sharedContext: context.sharedContext, updatedPresentationData: nil, title: presentationData.strings.ChannelBoost_Error_PremiumNeededTitle, text: isGroup ? presentationData.strings.GroupBoost_Error_PremiumNeededText : presentationData.strings.ChannelBoost_Error_PremiumNeededText, actions: [ TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Yes, action: { [weak controller] in if let navigationController = controller?.navigationController { controller?.dismissAnimated() let premiumController = context.sharedContext.makePremiumIntroController(context: context, source: .channelBoost(peerId), forceDark: forceDark, dismissed: nil) navigationController.pushViewController(premiumController, animated: true) } }) ], parseMarkdown: true ) controller.present(alertController, in: .window(.root)) } } } else { controller.dismissAnimated() } } func copyLink(_ link: String) { guard let controller = self.controller else { return } UIPasteboard.general.string = link let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } if let previousController = controller.navigationController?.viewControllers.reversed().first(where: { $0 !== controller }) as? ViewController { previousController.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.ChannelBoost_BoostLinkCopied), elevatedLayout: true, position: .top, animateInAsReplacement: false, action: { _ in return false }), in: .current) } } private func animateSuccess() { guard let controller = self.controller else { return } HapticFeedback().impact() controller.view.addSubview(ConfettiView(frame: controller.view.bounds)) } var isGroup: Bool? { var isGroup: Bool? if let peer = self.peer { if case let .channel(channel) = peer, case .group = channel.info { isGroup = true } else { isGroup = false } } return isGroup } } func makeState() -> State { return State(context: self.context, peerId: self.peerId, mode: self.mode, status: self.status, myBoostStatus: self.myBoostStatus, replacedBoosts: self.replacedBoosts, openPeer: self.openPeer, forceDark: self.forceDark) } static var body: Body { let sheet = Child(ResizableSheetComponent<(EnvironmentType)>.self) let animateOut = StoredActionSlot(Action.self) return { context in let environment = context.environment[EnvironmentType.self] let controller = environment.controller let state = context.state let theme = environment.theme.withModalBlocksBackground() let strings = environment.strings state.controller = controller() as? PremiumBoostLevelsScreen let dismiss: (Bool) -> Void = { animated in if animated { animateOut.invoke(Action { _ in if let controller = controller() { controller.dismiss(completion: nil) } }) } else { if let controller = controller() { controller.dismiss(completion: nil) } } } let titleString: String switch context.component.mode { case let .owner(subject): if let status = context.component.status, let _ = status.nextLevelBoosts { if let subject { switch subject { case .stories: if status.level == 0 { titleString = strings.ChannelBoost_EnableStories } else { titleString = strings.ChannelBoost_IncreaseLimit } case .nameColors: titleString = strings.ChannelBoost_NameColor case .nameIcon: titleString = strings.ChannelBoost_NameIcon case .profileColors: titleString = strings.ChannelBoost_ProfileColor case .profileIcon: titleString = strings.ChannelBoost_ProfileIcon case .channelReactions: titleString = strings.ChannelBoost_CustomReactions case .emojiStatus: titleString = strings.ChannelBoost_EmojiStatus case .wallpaper: titleString = strings.ChannelBoost_Wallpaper case .customWallpaper: titleString = strings.ChannelBoost_CustomWallpaper case .audioTranscription: titleString = strings.GroupBoost_AudioTranscription case .emojiPack: titleString = strings.GroupBoost_EmojiPack case .noAds: titleString = strings.ChannelBoost_NoAds case .wearGift: titleString = strings.ChannelBoost_WearGift case .autoTranslate: titleString = strings.ChannelBoost_AutoTranslate } } else { titleString = state.isGroup == true ? strings.GroupBoost_Title_Current : strings.ChannelBoost_Title_Current } } else { titleString = strings.ChannelBoost_MaxLevelReached } case let .user(mode): var remaining: Int? if let status = context.component.status, let nextLevelBoosts = status.nextLevelBoosts { remaining = nextLevelBoosts - status.boosts } if let _ = remaining { if case .current = mode { titleString = state.isGroup == true ? strings.GroupBoost_Title_Current : strings.ChannelBoost_Title_Current } else { titleString = state.isGroup == true ? strings.GroupBoost_Title_Other : strings.ChannelBoost_Title_Other } } else { titleString = strings.ChannelBoost_MaxLevelReached } case .features: titleString = "" //strings.GroupBoost_AdditionalFeatures } var rightItem: AnyComponent? if let openStats = context.component.openStats { rightItem = AnyComponent( GlassBarButtonComponent( size: CGSize(width: 44.0, height: 44.0), backgroundColor: nil, isDark: theme.overallDarkAppearance, state: .glass, component: AnyComponentWithIdentity(id: "stats", component: AnyComponent( BundleIconComponent( name: "Premium/Stats", tintColor: theme.chat.inputPanel.panelControlColor ) )), action: { _ in dismiss(true) Queue.mainQueue().after(0.35) { openStats() } } ) ) } var bottomButtonTitle: String? if case .user = state.mode, state.status?.nextLevelBoosts != nil { if state.currentMyBoostCount > 0 { bottomButtonTitle = strings.ChannelBoost_BoostAgain } else if state.isGroup == true { bottomButtonTitle = strings.GroupBoost_BoostGroup } else { bottomButtonTitle = strings.ChannelBoost_BoostChannel } } let bottomButtonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) let contentBottomInset: CGFloat if bottomButtonTitle != nil { contentBottomInset = bottomButtonInsets.bottom + 52.0 + 16.0 } else { contentBottomInset = 0.0 } let sheet = sheet.update( component: ResizableSheetComponent( content: AnyComponent(SheetContent( context: context.component.context, peerId: context.component.peerId, peer: state.peer, memberPeer: state.memberPeer, mode: context.component.mode, status: context.component.status, boostState: state.boostState, bottomInset: contentBottomInset, boost: { [weak state] in state?.updateBoostState() }, copyLink: { [weak state] link in state?.copyLink(link) }, dismiss: { dismiss(true) }, openStats: context.component.openStats, openGift: context.component.openGift, openPeer: context.component.openPeer, updated: { //updated() } )), titleItem: AnyComponent( MultilineTextComponent(text: .plain(NSAttributedString(string: titleString, font: Font.semibold(17.0), textColor: theme.list.itemPrimaryTextColor))) ), leftItem: AnyComponent( GlassBarButtonComponent( size: CGSize(width: 44.0, height: 44.0), backgroundColor: nil, isDark: theme.overallDarkAppearance, state: .glass, component: AnyComponentWithIdentity(id: "close", component: AnyComponent( BundleIconComponent( name: "Navigation/Close", tintColor: theme.chat.inputPanel.panelControlColor ) )), action: { _ in dismiss(true) } ) ), rightItem: rightItem, bottomItem: bottomButtonTitle.map { title in AnyComponent( ButtonComponent( background: ButtonComponent.Background( style: .glass, color: theme.list.itemCheckColors.fillColor, foreground: theme.list.itemCheckColors.foregroundColor, pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) ), content: AnyComponentWithIdentity( id: AnyHashable(0), component: AnyComponent( ButtonTextContentComponent( text: title, badge: 0, textColor: theme.list.itemCheckColors.foregroundColor, badgeBackground: theme.list.itemCheckColors.foregroundColor, badgeForeground: theme.list.itemCheckColors.fillColor ) ) ), action: { [weak state] in state?.updateBoostState() } ) ) }, backgroundColor: .color(theme.list.modalPlainBackgroundColor), animateOut: animateOut ), environment: { environment ResizableSheetComponentEnvironment( theme: theme, statusBarHeight: environment.statusBarHeight, safeInsets: environment.safeInsets, inputHeight: 0.0, metrics: environment.metrics, deviceMetrics: environment.deviceMetrics, isDisplaying: environment.value.isVisible, isCentered: environment.metrics.widthClass == .regular, screenSize: context.availableSize, regularMetricsSize: CGSize(width: 430.0, height: 900.0), dismiss: { animated in dismiss(animated) } ) }, availableSize: context.availableSize, transition: context.transition ) context.add(sheet .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) ) return context.availableSize } } } public class PremiumBoostLevelsScreen: ViewControllerComponentContainer { public enum Mode: Equatable { public enum UserMode: Equatable { case external case current case groupPeer(EnginePeer.Id, Int) case unrestrict(Int) } case user(mode: UserMode) case owner(subject: BoostSubject?) case features } private let context: AccountContext public var boostStatusUpdated: (ChannelBoostStatus, MyBoostStatus) -> Void = { _, _ in } public var disposed: () -> Void = {} public init( context: AccountContext, peerId: EnginePeer.Id, mode: Mode, status: ChannelBoostStatus?, myBoostStatus: MyBoostStatus? = nil, replacedBoosts: (Int32, [EnginePeer])? = nil, openStats: (() -> Void)? = nil, openGift: (() -> Void)? = nil, openPeer: ((EnginePeer) -> Void)? = nil, forceDark: Bool = false ) { self.context = context super.init( context: context, component: PremiumBoostLevelsSheetComponent( context: context, peerId: peerId, mode: mode, status: status, myBoostStatus: myBoostStatus, replacedBoosts: replacedBoosts, openStats: openStats, openGift: openGift, openPeer: openPeer, forceDark: forceDark ), navigationBarAppearance: .none, statusBarStyle: .ignore, theme: forceDark ? .dark : .default ) self.statusBar.statusBarStyle = .Ignore self.navigationPresentation = .flatModal self.blocksBackgroundWhenInOverlay = true self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) } required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { self.disposed() } public func dismissAnimated() { if let view = self.node.hostView.findTaggedView(tag: ResizableSheetComponent.View.Tag()) as? ResizableSheetComponent.View { view.dismissAnimated() } } } private struct InternalBoostState: Equatable { let level: Int32 let currentLevelBoosts: Int32 let nextLevelBoosts: Int32? let boosts: Int32 struct DisplayData: Equatable { let level: Int32 let boosts: Int32 let currentLevelBoosts: Int32 let nextLevelBoosts: Int32? let myBoostCount: Int32 } func displayData(myBoostCount: Int32, currentMyBoostCount: Int32, replacedBoosts: Int32? = nil) -> DisplayData { var currentLevel = self.level var nextLevelBoosts = self.nextLevelBoosts var currentLevelBoosts = self.currentLevelBoosts var boosts = self.boosts if let replacedBoosts { boosts = max(currentLevelBoosts, boosts - replacedBoosts) } if currentMyBoostCount > 0 && self.boosts == currentLevelBoosts { currentLevel = max(0, currentLevel - 1) nextLevelBoosts = currentLevelBoosts currentLevelBoosts = max(0, currentLevelBoosts - 1) } return DisplayData( level: currentLevel, boosts: boosts, currentLevelBoosts: currentLevelBoosts, nextLevelBoosts: nextLevelBoosts, myBoostCount: myBoostCount ) } } func requiredBoostSubjectLevel(subject: BoostSubject, group: Bool, context: AccountContext, configuration: PremiumConfiguration) -> Int32 { switch subject { case .stories: return 1 case let .channelReactions(reactionCount): return reactionCount case let .nameColors(colors): if let value = context.peerNameColors.nameColorsChannelMinRequiredBoostLevel[colors.rawValue] { return value } return 1 case .nameIcon: return configuration.minChannelNameIconLevel case let .profileColors(colors): if group { if let value = context.peerNameColors.profileColorsGroupMinRequiredBoostLevel[colors.rawValue] { return value } } else { return configuration.minChannelProfileColorLevel } return 1 case .profileIcon: return group ? configuration.minGroupProfileIconLevel : configuration.minChannelProfileIconLevel case .emojiStatus: return group ? configuration.minGroupEmojiStatusLevel : configuration.minChannelEmojiStatusLevel case .wallpaper: return group ? configuration.minGroupWallpaperLevel : configuration.minChannelWallpaperLevel case .customWallpaper: return group ? configuration.minGroupCustomWallpaperLevel : configuration.minChannelCustomWallpaperLevel case .audioTranscription: return configuration.minGroupAudioTranscriptionLevel case .emojiPack: return configuration.minGroupEmojiPackLevel case .noAds: return configuration.minChannelRestrictAdsLevel case .wearGift: return configuration.minChannelWearGiftLevel case .autoTranslate: return configuration.minChannelAutoTranslateLevel } } extension BoostSubject { public func requiredLevel(group: Bool, context: AccountContext, configuration: PremiumConfiguration) -> Int32 { return requiredBoostSubjectLevel(subject: self, group: group, context: context, configuration: configuration) } } private final class LevelHeaderComponent: CombinedComponent { let theme: PresentationTheme let text: String init( theme: PresentationTheme, text: String ) { self.theme = theme self.text = text } static func ==(lhs: LevelHeaderComponent, rhs: LevelHeaderComponent) -> Bool { if lhs.theme !== rhs.theme { return false } if lhs.text != rhs.text { return false } return true } static var body: Body { let background = Child(RoundedRectangle.self) let text = Child(MultilineTextComponent.self) let leftLine = Child(Rectangle.self) let rightLine = Child(Rectangle.self) return { context in let component = context.component let outerInset: CGFloat = 28.0 let innerInset: CGFloat = 9.0 let height: CGFloat = 50.0 let backgroundHeight: CGFloat = 34.0 let text = text.update( component: MultilineTextComponent( text: .plain(NSAttributedString(string: component.text, font: Font.semibold(15.0), textColor: .white)), horizontalAlignment: .center ), availableSize: context.availableSize, transition: .immediate ) let backgroundWidth: CGFloat = floor(text.size.width + 21.0) let background = background.update( component: RoundedRectangle(colors: [UIColor(rgb: 0x9076ff), UIColor(rgb: 0xbc6de8)], cornerRadius: backgroundHeight / 2.0, gradientDirection: .horizontal), availableSize: CGSize(width: backgroundWidth, height: backgroundHeight), transition: .immediate ) context.add(background .position(CGPoint(x: context.availableSize.width / 2.0, y: height / 2.0)) ) context.add(text .position(CGPoint(x: context.availableSize.width / 2.0, y: height / 2.0)) ) let remainingWidth = (context.availableSize.width - background.size.width) / 2.0 let lineSize = remainingWidth - outerInset - innerInset let lineWidth = 1.0 - UIScreenPixel let leftLine = leftLine.update( component: Rectangle( color: component.theme.actionSheet.secondaryTextColor.withMultipliedAlpha(0.5) ), availableSize: CGSize(width: lineSize, height: lineWidth), transition: .immediate ) context.add(leftLine .position(CGPoint(x: outerInset + lineSize / 2.0, y: height / 2.0)) ) let rightLine = rightLine.update( component: Rectangle( color: component.theme.actionSheet.secondaryTextColor.withMultipliedAlpha(0.5) ), availableSize: CGSize(width: lineSize, height: lineWidth), transition: .immediate ) context.add(rightLine .position(CGPoint(x: context.availableSize.width - outerInset - lineSize / 2.0, y: height / 2.0)) ) return CGSize(width: context.availableSize.width, height: height) } } } private final class LevelPerkComponent: CombinedComponent { let theme: PresentationTheme let iconName: String let text: String init( theme: PresentationTheme, iconName: String, text: String ) { self.theme = theme self.iconName = iconName self.text = text } static func ==(lhs: LevelPerkComponent, rhs: LevelPerkComponent) -> Bool { if lhs.theme !== rhs.theme { return false } if lhs.iconName != rhs.iconName { return false } if lhs.text != rhs.text { return false } return true } static var body: Body { let icon = Child(BundleIconComponent.self) let text = Child(MultilineTextComponent.self) return { context in let component = context.component let outerInset: CGFloat = 28.0 let height: CGFloat = 44.0 let icon = icon.update( component: BundleIconComponent( name: component.iconName, tintColor: component.theme.actionSheet.controlAccentColor ), availableSize: context.availableSize, transition: .immediate ) context.add(icon .position(CGPoint(x: outerInset + icon.size.width / 2.0, y: height / 2.0)) ) let text = text.update( component: MultilineTextComponent( text: .plain(NSAttributedString(string: component.text, font: Font.semibold(15.0), textColor: component.theme.actionSheet.primaryTextColor)), horizontalAlignment: .center ), availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height), transition: .immediate ) context.add(text .position(CGPoint(x: outerInset * 2.0 + 18.0 + text.size.width / 2.0, y: height / 2.0)) ) return CGSize(width: context.availableSize.width, height: height) } } } private final class LevelSectionComponent: CombinedComponent { enum Perk: Equatable { case story(Int32) case reaction(Int32) case nameColor(Int32) case profileColor(Int32) case profileIcon case linkColor(Int32) case linkIcon case emojiStatus case wallpaper(Int32) case customWallpaper case audioTranscription case emojiPack case noAds case wearGift case autoTranslate func title(strings: PresentationStrings, isGroup: Bool) -> String { switch self { case let .story(value): return strings.ChannelBoost_Table_StoriesPerDay(value) case let .reaction(value): return strings.ChannelBoost_Table_CustomReactions(value) case let .nameColor(value): return strings.ChannelBoost_Table_NameColor(value) case let .profileColor(value): return isGroup ? strings.ChannelBoost_Table_Group_ProfileColor(value) : strings.ChannelBoost_Table_ProfileColor(value) case .profileIcon: return isGroup ? strings.ChannelBoost_Table_Group_ProfileLogo : strings.ChannelBoost_Table_ProfileLogo case let .linkColor(value): return strings.ChannelBoost_Table_StyleForHeaders(value) case .linkIcon: return strings.ChannelBoost_Table_HeadersLogo case .emojiStatus: return strings.ChannelBoost_Table_EmojiStatus case let .wallpaper(value): return isGroup ? strings.ChannelBoost_Table_Group_Wallpaper(value) : strings.ChannelBoost_Table_Wallpaper(value) case .customWallpaper: return isGroup ? strings.ChannelBoost_Table_Group_CustomWallpaper : strings.ChannelBoost_Table_CustomWallpaper case .audioTranscription: return strings.GroupBoost_Table_Group_VoiceToText case .emojiPack: return strings.GroupBoost_Table_Group_EmojiPack case .noAds: return strings.ChannelBoost_Table_NoAds case .wearGift: return strings.ChannelBoost_Table_WearGift case .autoTranslate: return strings.ChannelBoost_Table_AutoTranslate } } var iconName: String { switch self { case .story: return "Premium/BoostPerk/Story" case .reaction: return "Premium/BoostPerk/Reaction" case .nameColor: return "Premium/BoostPerk/NameColor" case .profileColor: return "Premium/BoostPerk/CoverColor" case .profileIcon: return "Premium/BoostPerk/CoverLogo" case .linkColor: return "Premium/BoostPerk/LinkColor" case .linkIcon: return "Premium/BoostPerk/LinkLogo" case .emojiStatus: return "Premium/BoostPerk/EmojiStatus" case .wallpaper: return "Premium/BoostPerk/Wallpaper" case .customWallpaper: return "Premium/BoostPerk/CustomWallpaper" case .audioTranscription: return "Premium/BoostPerk/AudioTranscription" case .emojiPack: return "Premium/BoostPerk/EmojiPack" case .noAds: return "Premium/BoostPerk/NoAds" case .wearGift: return "Premium/BoostPerk/NoAds" case .autoTranslate: return "Chat/Title Panels/Translate" } } } let theme: PresentationTheme let strings: PresentationStrings let level: Int32 let isFirst: Bool let perks: [Perk] let isGroup: Bool init( theme: PresentationTheme, strings: PresentationStrings, level: Int32, isFirst: Bool, perks: [Perk], isGroup: Bool ) { self.theme = theme self.strings = strings self.level = level self.isFirst = isFirst self.perks = perks self.isGroup = isGroup } static func ==(lhs: LevelSectionComponent, rhs: LevelSectionComponent) -> Bool { if lhs.theme !== rhs.theme { return false } if lhs.level != rhs.level { return false } if lhs.isFirst != rhs.isFirst { return false } if lhs.perks != rhs.perks { return false } if lhs.isGroup != rhs.isGroup { return false } return true } static var body: Body { let header = Child(LevelHeaderComponent.self) let list = Child(List.self) return { context in let component = context.component let header = header.update( component: LevelHeaderComponent(theme: component.theme, text: component.isFirst ? component.strings.ChannelBoost_Table_LevelUnlocks(component.level) : component.strings.ChannelBoost_Table_Level(component.level)), availableSize: context.availableSize, transition: .immediate ) context.add(header .position(CGPoint(x: context.availableSize.width / 2.0, y: header.size.height / 2.0))) let items: [AnyComponentWithIdentity] = component.perks.enumerated().map { index, value in AnyComponentWithIdentity( id: index, component: AnyComponent( LevelPerkComponent( theme: component.theme, iconName: value.iconName, text: value.title(strings: component.strings, isGroup: component.isGroup) ) ) ) } let list = list.update( component: List(items), availableSize: CGSize(width: context.availableSize.width, height: 10000.0), transition: context.transition ) context.add(list .position(CGPoint(x: context.availableSize.width / 2.0, y: header.size.height + list.size.height / 2.0))) return CGSize(width: context.availableSize.width, height: header.size.height + list.size.height) } } }