Swiftgram/submodules/PremiumUI/Sources/PremiumDemoScreen.swift
Ilya Laktyushin 02e06779ef Drawing
2022-12-03 21:57:32 +04:00

1228 lines
52 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SwiftSignalKit
import AccountContext
import TelegramPresentationData
import PresentationDataUtils
import ComponentFlow
import ViewControllerComponent
import SheetComponent
import MultilineTextComponent
import BundleIconComponent
import SolidRoundedButtonComponent
import Markdown
import TelegramUIPreferences
final class GradientBackgroundComponent: Component {
public let colors: [UIColor]
public init(
colors: [UIColor]
) {
self.colors = colors
}
public static func ==(lhs: GradientBackgroundComponent, rhs: GradientBackgroundComponent) -> Bool {
if lhs.colors != rhs.colors {
return false
}
return true
}
public final class View: UIView {
private let clipLayer: CALayer
private let gradientLayer: CAGradientLayer
private var component: GradientBackgroundComponent?
override init(frame: CGRect) {
self.clipLayer = CALayer()
self.clipLayer.cornerRadius = 10.0
self.clipLayer.masksToBounds = true
self.gradientLayer = CAGradientLayer()
super.init(frame: frame)
self.layer.addSublayer(self.clipLayer)
self.clipLayer.addSublayer(gradientLayer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: GradientBackgroundComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.clipLayer.frame = CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: availableSize.height + 10.0))
self.gradientLayer.frame = CGRect(origin: .zero, size: availableSize)
var locations: [NSNumber] = []
let delta = 1.0 / CGFloat(component.colors.count - 1)
for i in 0 ..< component.colors.count {
locations.append((delta * CGFloat(i)) as NSNumber)
}
self.gradientLayer.locations = locations
self.gradientLayer.colors = component.colors.reversed().map { $0.cgColor }
self.gradientLayer.type = .radial
self.gradientLayer.startPoint = CGPoint(x: 1.0, y: 0.0)
self.gradientLayer.endPoint = CGPoint(x: -2.0, y: 3.0)
self.component = component
self.setupGradientAnimations()
return availableSize
}
private func setupGradientAnimations() {
if let _ = self.gradientLayer.animation(forKey: "movement") {
} else {
let previousValue = self.gradientLayer.endPoint
let value: CGFloat
if previousValue.x < -0.5 {
value = 0.5
} else {
value = 2.0
}
let newValue = CGPoint(x: -value, y: 1.0 + value)
// let secondNewValue = CGPoint(x: 3.0 - value, y: -2.0 + value)
self.gradientLayer.endPoint = newValue
CATransaction.begin()
let animation = CABasicAnimation(keyPath: "endPoint")
animation.duration = 4.5
animation.fromValue = previousValue
animation.toValue = newValue
CATransaction.setCompletionBlock { [weak self] in
self?.setupGradientAnimations()
}
self.gradientLayer.add(animation, forKey: "movement")
// let secondPreviousValue = self.gradientLayer.startPoint
// let secondAnimation = CABasicAnimation(keyPath: "startPoint")
// secondAnimation.duration = 4.5
// secondAnimation.fromValue = secondPreviousValue
// secondAnimation.toValue = secondNewValue
//
// self.gradientLayer.add(secondAnimation, forKey: "movement2")
CATransaction.commit()
}
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
final class DemoPageEnvironment: Equatable {
public let isDisplaying: Bool
public let isCentral: Bool
public let position: CGFloat
public init(isDisplaying: Bool, isCentral: Bool, position: CGFloat) {
self.isDisplaying = isDisplaying
self.isCentral = isCentral
self.position = position
}
public static func ==(lhs: DemoPageEnvironment, rhs: DemoPageEnvironment) -> Bool {
if lhs.isDisplaying != rhs.isDisplaying {
return false
}
if lhs.isCentral != rhs.isCentral {
return false
}
if lhs.position != rhs.position {
return false
}
return true
}
}
final class PageComponent<ChildEnvironment: Equatable>: CombinedComponent {
typealias EnvironmentType = ChildEnvironment
private let content: AnyComponent<ChildEnvironment>
private let title: String
private let text: String
private let textColor: UIColor
init(
content: AnyComponent<ChildEnvironment>,
title: String,
text: String,
textColor: UIColor
) {
self.content = content
self.title = title
self.text = text
self.textColor = textColor
}
static func ==(lhs: PageComponent<ChildEnvironment>, rhs: PageComponent<ChildEnvironment>) -> Bool {
if lhs.content != rhs.content {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.text != rhs.text {
return false
}
if lhs.textColor != rhs.textColor {
return false
}
return true
}
static var body: Body {
let children = ChildMap(environment: ChildEnvironment.self, keyedBy: AnyHashable.self)
let title = Child(MultilineTextComponent.self)
let text = Child(MultilineTextComponent.self)
return { context in
let availableSize = context.availableSize
let component = context.component
let sideInset: CGFloat = 16.0
let textSideInset: CGFloat = 24.0
let textColor = component.textColor
let textFont = Font.regular(17.0)
let boldTextFont = Font.semibold(17.0)
let content = children["main"].update(
component: component.content,
environment: {
context.environment[ChildEnvironment.self]
},
availableSize: CGSize(width: availableSize.width, height: availableSize.width),
transition: context.transition
)
let title = title.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(
string: component.title,
font: boldTextFont,
textColor: component.textColor,
paragraphAlignment: .center
)),
horizontalAlignment: .center,
maximumNumberOfLines: 1
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
let markdownAttributes = MarkdownAttributes(
body: MarkdownAttributeSet(font: textFont, textColor: textColor),
bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor),
link: MarkdownAttributeSet(font: textFont, textColor: textColor),
linkAttribute: { _ in
return nil
}
)
let text = text.update(
component: MultilineTextComponent(
text: .markdown(text: component.text, attributes: markdownAttributes),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.0
),
availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height),
transition: .immediate
)
context.add(title
.position(CGPoint(x: context.availableSize.width / 2.0, y: content.size.height + 40.0))
)
context.add(text
.position(CGPoint(x: context.availableSize.width / 2.0, y: content.size.height + 60.0 + text.size.height / 2.0))
)
context.add(content
.position(CGPoint(x: content.size.width / 2.0, y: content.size.height / 2.0))
)
return availableSize
}
}
}
final class DemoPagerComponent: Component {
public final class Item: Equatable {
public let content: AnyComponentWithIdentity<DemoPageEnvironment>
public init(_ content: AnyComponentWithIdentity<DemoPageEnvironment>) {
self.content = content
}
public static func ==(lhs: Item, rhs: Item) -> Bool {
if lhs.content != rhs.content {
return false
}
return true
}
}
let items: [Item]
let index: Int
let updated: (CGFloat, Int) -> Void
public init(
items: [Item],
index: Int = 0,
updated: @escaping (CGFloat, Int) -> Void
) {
self.items = items
self.index = index
self.updated = updated
}
public static func ==(lhs: DemoPagerComponent, rhs: DemoPagerComponent) -> Bool {
if lhs.items != rhs.items {
return false
}
return true
}
final class View: UIView, UIScrollViewDelegate {
private let scrollView: UIScrollView
private var itemViews: [AnyHashable: ComponentHostView<DemoPageEnvironment>] = [:]
private var component: DemoPagerComponent?
override init(frame: CGRect) {
self.scrollView = UIScrollView(frame: frame)
self.scrollView.isPagingEnabled = true
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.showsVerticalScrollIndicator = false
self.scrollView.alwaysBounceHorizontal = false
self.scrollView.bounces = false
self.scrollView.layer.cornerRadius = 10.0
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.scrollView.contentInsetAdjustmentBehavior = .never
}
super.init(frame: frame)
self.scrollView.delegate = self
self.addSubview(self.scrollView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private var ignoreContentOffsetChange = false
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard let component = self.component, !self.ignoreContentOffsetChange else {
return
}
self.ignoreContentOffsetChange = true
let _ = self.update(component: component, availableSize: self.bounds.size, transition: .immediate)
component.updated(self.scrollView.contentOffset.x / (self.scrollView.contentSize.width - self.scrollView.frame.width), component.items.count)
self.ignoreContentOffsetChange = false
}
func update(component: DemoPagerComponent, availableSize: CGSize, transition: Transition) -> CGSize {
var validIds: [AnyHashable] = []
let firstTime = self.itemViews.isEmpty
let contentSize = CGSize(width: availableSize.width * CGFloat(component.items.count), height: availableSize.height)
if self.scrollView.contentSize != contentSize {
self.scrollView.contentSize = contentSize
}
let scrollFrame = CGRect(origin: .zero, size: availableSize)
if self.scrollView.frame != scrollFrame {
self.scrollView.frame = scrollFrame
}
if firstTime {
self.scrollView.contentOffset = CGPoint(x: CGFloat(component.index) * availableSize.width, y: 0.0)
component.updated(self.scrollView.contentOffset.x / (self.scrollView.contentSize.width - self.scrollView.frame.width), component.items.count)
}
let viewportCenter = self.scrollView.contentOffset.x + availableSize.width * 0.5
var i = 0
for item in component.items {
let itemFrame = CGRect(origin: CGPoint(x: availableSize.width * CGFloat(i), y: 0.0), size: availableSize)
let isDisplaying = itemFrame.intersects(self.scrollView.bounds)
let centerDelta = itemFrame.midX - viewportCenter
let position = centerDelta / (availableSize.width * 0.75)
i += 1
if abs(position) > 1.5 {
continue
}
validIds.append(item.content.id)
let itemView: ComponentHostView<DemoPageEnvironment>
var itemTransition = transition
if let current = self.itemViews[item.content.id] {
itemView = current
} else {
itemTransition = transition.withAnimation(.none)
itemView = ComponentHostView<DemoPageEnvironment>()
self.itemViews[item.content.id] = itemView
if item.content.id == (PremiumDemoScreen.Subject.fasterDownload as AnyHashable) {
self.scrollView.insertSubview(itemView, at: 0)
} else {
self.scrollView.addSubview(itemView)
}
}
let environment = DemoPageEnvironment(isDisplaying: isDisplaying, isCentral: abs(centerDelta) < CGFloat.ulpOfOne, position: position)
let _ = itemView.update(
transition: itemTransition,
component: item.content.component,
environment: { environment },
containerSize: availableSize
)
itemView.frame = itemFrame
}
var removeIds: [AnyHashable] = []
for (id, itemView) in self.itemViews {
if !validIds.contains(id) {
removeIds.append(id)
itemView.removeFromSuperview()
}
}
for id in removeIds {
self.itemViews.removeValue(forKey: id)
}
self.component = component
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
public final class DemoAnimateInTransition {
}
private final class DemoSheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let subject: PremiumDemoScreen.Subject
let source: PremiumDemoScreen.Source
let order: [PremiumPerk]
let action: () -> Void
let dismiss: () -> Void
init(
context: AccountContext,
subject: PremiumDemoScreen.Subject,
source: PremiumDemoScreen.Source,
order: [PremiumPerk]?,
action: @escaping () -> Void,
dismiss: @escaping () -> Void
) {
self.context = context
self.subject = subject
self.source = source
self.order = order ?? [.moreUpload, .fasterDownload, .voiceToText, .noAds, .uniqueReactions, .premiumStickers, .animatedEmoji, .advancedChatManagement, .profileBadge, .animatedUserpics, .appIcons]
self.action = action
self.dismiss = dismiss
}
static func ==(lhs: DemoSheetContent, rhs: DemoSheetContent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.subject != rhs.subject {
return false
}
if lhs.source != rhs.source {
return false
}
if lhs.order != rhs.order {
return false
}
return true
}
final class State: ComponentState {
private let context: AccountContext
var cachedCloseImage: UIImage?
var isPremium: Bool?
var reactions: [AvailableReactions.Reaction]?
var stickers: [TelegramMediaFile]?
var appIcons: [PresentationAppIcon]?
var disposable: Disposable?
var promoConfiguration: PremiumPromoConfiguration?
init(context: AccountContext) {
self.context = context
self.appIcons = context.sharedContext.applicationBindings.getAvailableAlternateIcons().filter { $0.isPremium }
super.init()
let accountSpecificReactionOverrides: [ExperimentalUISettings.AccountReactionOverrides.Item]
if self.context.sharedContext.immediateExperimentalUISettings.enableReactionOverrides, let value = self.context.sharedContext.immediateExperimentalUISettings.accountReactionEffectOverrides.first(where: { $0.accountId == self.context.account.id.int64 }) {
accountSpecificReactionOverrides = value.items
} else {
accountSpecificReactionOverrides = []
}
let reactionOverrideMessages = self.context.engine.data.get(
EngineDataMap(accountSpecificReactionOverrides.map(\.messageId).map(TelegramEngine.EngineData.Item.Messages.Message.init))
)
let accountSpecificStickerOverrides: [ExperimentalUISettings.AccountReactionOverrides.Item]
if self.context.sharedContext.immediateExperimentalUISettings.enableReactionOverrides, let value = self.context.sharedContext.immediateExperimentalUISettings.accountStickerEffectOverrides.first(where: { $0.accountId == self.context.account.id.int64 }) {
accountSpecificStickerOverrides = value.items
} else {
accountSpecificStickerOverrides = []
}
let stickerOverrideMessages = self.context.engine.data.get(
EngineDataMap(accountSpecificStickerOverrides.map(\.messageId).map(TelegramEngine.EngineData.Item.Messages.Message.init))
)
let stickersKey: PostboxViewKey = .orderedItemList(id: Namespaces.OrderedItemList.CloudPremiumStickers)
self.disposable = (combineLatest(
queue: Queue.mainQueue(),
self.context.engine.stickers.availableReactions(),
self.context.account.postbox.combinedView(keys: [stickersKey])
|> map { views -> [OrderedItemListEntry]? in
if let view = views.views[stickersKey] as? OrderedItemListView {
return view.items
} else {
return nil
}
}
|> filter { items in
return items != nil
}
|> take(1),
self.context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId),
TelegramEngine.EngineData.Item.Configuration.PremiumPromo()
),
reactionOverrideMessages,
stickerOverrideMessages
)
|> map { reactions, items, data, reactionOverrideMessages, stickerOverrideMessages -> ([AvailableReactions.Reaction], [TelegramMediaFile], Bool?, PremiumPromoConfiguration?) in
var reactionOverrides: [MessageReaction.Reaction: TelegramMediaFile] = [:]
for item in accountSpecificReactionOverrides {
if let maybeMessage = reactionOverrideMessages[item.messageId], let message = maybeMessage {
for media in message.media {
if let file = media as? TelegramMediaFile, file.fileId == item.mediaId {
reactionOverrides[item.key] = file
}
}
}
}
var stickerOverrides: [MessageReaction.Reaction: TelegramMediaFile] = [:]
for item in accountSpecificStickerOverrides {
if let maybeMessage = stickerOverrideMessages[item.messageId], let message = maybeMessage {
for media in message.media {
if let file = media as? TelegramMediaFile, file.fileId == item.mediaId {
stickerOverrides[item.key] = file
}
}
}
}
if let reactions = reactions {
var result: [TelegramMediaFile] = []
if let items = items {
for item in items {
if let mediaItem = item.contents.get(RecentMediaItem.self) {
result.append(mediaItem.media)
}
}
}
return (reactions.reactions.filter({ $0.isPremium }).map { reaction -> AvailableReactions.Reaction in
var aroundAnimation = reaction.aroundAnimation
if let replacementFile = reactionOverrides[reaction.value] {
aroundAnimation = replacementFile
}
return AvailableReactions.Reaction(
isEnabled: reaction.isEnabled,
isPremium: reaction.isPremium,
value: reaction.value,
title: reaction.title,
staticIcon: reaction.staticIcon,
appearAnimation: reaction.appearAnimation,
selectAnimation: reaction.selectAnimation,
activateAnimation: reaction.activateAnimation,
effectAnimation: reaction.effectAnimation,
aroundAnimation: aroundAnimation,
centerAnimation: reaction.centerAnimation
)
}, result.map { file -> TelegramMediaFile in
for attribute in file.attributes {
switch attribute {
case let .Sticker(displayText, _, _):
if let replacementFile = stickerOverrides[.builtin(displayText)], let dimensions = replacementFile.dimensions {
let _ = dimensions
return TelegramMediaFile(
fileId: file.fileId,
partialReference: file.partialReference,
resource: file.resource,
previewRepresentations: file.previewRepresentations,
videoThumbnails: [TelegramMediaFile.VideoThumbnail(dimensions: dimensions, resource: replacementFile.resource)],
immediateThumbnailData: file.immediateThumbnailData,
mimeType: file.mimeType,
size: file.size,
attributes: file.attributes
)
}
default:
break
}
}
return file
}, data.0?.isPremium ?? false, data.1)
} else {
return ([], [], nil, nil)
}
}).start(next: { [weak self] reactions, stickers, isPremium, promoConfiguration in
guard let strongSelf = self else {
return
}
strongSelf.reactions = reactions
strongSelf.stickers = stickers
strongSelf.isPremium = isPremium
strongSelf.promoConfiguration = promoConfiguration
if !reactions.isEmpty && !stickers.isEmpty {
strongSelf.updated(transition: Transition(.immediate).withUserData(DemoAnimateInTransition()))
}
})
}
deinit {
self.disposable?.dispose()
}
}
func makeState() -> State {
return State(context: self.context)
}
static var body: Body {
let closeButton = Child(Button.self)
let background = Child(GradientBackgroundComponent.self)
let pager = Child(DemoPagerComponent.self)
let button = Child(SolidRoundedButtonComponent.self)
return { context in
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
let component = context.component
let theme = environment.theme
let strings = environment.strings
let state = context.state
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
let background = background.update(
component: GradientBackgroundComponent(colors: [
UIColor(rgb: 0x0077ff),
UIColor(rgb: 0x6b93ff),
UIColor(rgb: 0x8878ff),
UIColor(rgb: 0xe46ace)
]),
availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.width),
transition: .immediate
)
context.add(background
.position(CGPoint(x: context.availableSize.width / 2.0, y: background.size.height / 2.0))
)
let closeImage: UIImage
if let image = state.cachedCloseImage {
closeImage = image
} else {
closeImage = generateCloseButtonImage(backgroundColor: .clear, foregroundColor: UIColor(rgb: 0xffffff))!
state.cachedCloseImage = closeImage
}
var isStandalone = false
if case .other = component.source {
isStandalone = true
}
if let stickers = state.stickers, let appIcons = state.appIcons, let configuration = state.promoConfiguration {
let textColor = theme.actionSheet.primaryTextColor
var availableItems: [PremiumPerk: DemoPagerComponent.Item] = [:]
availableItems[.moreUpload] = DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.moreUpload,
component: AnyComponent(
PageComponent(
content: AnyComponent(PhoneDemoComponent(
context: component.context,
position: .bottom,
videoFile: configuration.videos["more_upload"],
decoration: .dataRain
)),
title: strings.Premium_UploadSize,
text: strings.Premium_UploadSizeInfo,
textColor: textColor
)
)
)
)
availableItems[.fasterDownload] = DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.fasterDownload,
component: AnyComponent(
PageComponent(
content: AnyComponent(PhoneDemoComponent(
context: component.context,
position: .top,
videoFile: configuration.videos["faster_download"],
decoration: .fasterStars
)),
title: strings.Premium_FasterSpeed,
text: isStandalone ? strings.Premium_FasterSpeedStandaloneInfo : strings.Premium_FasterSpeedInfo,
textColor: textColor
)
)
)
)
availableItems[.voiceToText] = DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.voiceToText,
component: AnyComponent(
PageComponent(
content: AnyComponent(PhoneDemoComponent(
context: component.context,
position: .top,
videoFile: configuration.videos["voice_to_text"],
decoration: .badgeStars
)),
title: strings.Premium_VoiceToText,
text: isStandalone ? strings.Premium_VoiceToTextStandaloneInfo : strings.Premium_VoiceToTextInfo,
textColor: textColor
)
)
)
)
availableItems[.noAds] = DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.noAds,
component: AnyComponent(
PageComponent(
content: AnyComponent(PhoneDemoComponent(
context: component.context,
position: .bottom,
videoFile: configuration.videos["no_ads"],
decoration: .swirlStars
)),
title: strings.Premium_NoAds,
text: isStandalone ? strings.Premium_NoAdsStandaloneInfo : strings.Premium_NoAdsInfo,
textColor: textColor
)
)
)
)
availableItems[.uniqueReactions] = DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.uniqueReactions,
component: AnyComponent(
PageComponent(
content: AnyComponent(PhoneDemoComponent(
context: component.context,
position: .top,
videoFile: configuration.videos["infinite_reactions"],
decoration: .swirlStars
)),
title: strings.Premium_InfiniteReactions,
text: strings.Premium_InfiniteReactionsInfo,
textColor: textColor
)
)
)
)
availableItems[.premiumStickers] = DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.premiumStickers,
component: AnyComponent(
PageComponent(
content: AnyComponent(
StickersCarouselComponent(
context: component.context,
stickers: stickers
)
),
title: strings.Premium_Stickers,
text: strings.Premium_StickersInfo,
textColor: textColor
)
)
)
)
availableItems[.emojiStatus] = DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.emojiStatus,
component: AnyComponent(
PageComponent(
content: AnyComponent(PhoneDemoComponent(
context: component.context,
position: .top,
videoFile: configuration.videos["emoji_status"],
decoration: .badgeStars
)),
title: strings.Premium_EmojiStatus,
text: strings.Premium_EmojiStatusInfo,
textColor: textColor
)
)
)
)
availableItems[.advancedChatManagement] = DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.advancedChatManagement,
component: AnyComponent(
PageComponent(
content: AnyComponent(PhoneDemoComponent(
context: component.context,
position: .top,
videoFile: configuration.videos["advanced_chat_management"],
decoration: .swirlStars
)),
title: strings.Premium_ChatManagement,
text: isStandalone ? strings.Premium_ChatManagementStandaloneInfo : strings.Premium_ChatManagementInfo,
textColor: textColor
)
)
)
)
availableItems[.profileBadge] = DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.profileBadge,
component: AnyComponent(
PageComponent(
content: AnyComponent(PhoneDemoComponent(
context: component.context,
position: .top,
videoFile: configuration.videos["profile_badge"],
decoration: .badgeStars
)),
title: strings.Premium_Badge,
text: strings.Premium_BadgeInfo,
textColor: textColor
)
)
)
)
availableItems[.animatedUserpics] = DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.animatedUserpics,
component: AnyComponent(
PageComponent(
content: AnyComponent(PhoneDemoComponent(
context: component.context,
position: .top,
videoFile: configuration.videos["animated_userpics"],
decoration: .swirlStars
)),
title: strings.Premium_Avatar,
text: strings.Premium_AvatarInfo,
textColor: textColor
)
)
)
)
availableItems[.appIcons] = DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.appIcons,
component: AnyComponent(
PageComponent(
content: AnyComponent(AppIconsDemoComponent(
context: component.context,
appIcons: appIcons
)),
title: isStandalone ? strings.Premium_AppIconStandalone : strings.Premium_AppIcon,
text: isStandalone ? strings.Premium_AppIconStandaloneInfo :strings.Premium_AppIconInfo,
textColor: textColor
)
)
)
)
availableItems[.animatedEmoji] = DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.animatedEmoji,
component: AnyComponent(
PageComponent(
content: AnyComponent(PhoneDemoComponent(
context: component.context,
position: .bottom,
videoFile: configuration.videos["animated_emoji"],
decoration: .emoji
)),
title: strings.Premium_AnimatedEmoji,
text: isStandalone ? strings.Premium_AnimatedEmojiStandaloneInfo : strings.Premium_AnimatedEmojiInfo,
textColor: textColor
)
)
)
)
var items: [DemoPagerComponent.Item] = component.order.compactMap { availableItems[$0] }
let index: Int
switch component.source {
case .intro, .gift:
index = items.firstIndex(where: { (component.subject as AnyHashable) == $0.content.id }) ?? 0
case .other:
items = items.filter { item in
return item.content.id == (component.subject as AnyHashable)
}
index = 0
}
let pager = pager.update(
component: DemoPagerComponent(
items: items,
index: index,
updated: { _, _ in }
),
availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.width + 154.0),
transition: context.transition
)
context.add(pager
.position(CGPoint(x: context.availableSize.width / 2.0, y: pager.size.height / 2.0))
)
}
let closeButton = closeButton.update(
component: Button(
content: AnyComponent(ZStack([
AnyComponentWithIdentity(
id: "background",
component: AnyComponent(
BlurredRectangle(
color: UIColor(rgb: 0x888888, alpha: 0.1),
radius: 15.0
)
)
),
AnyComponentWithIdentity(
id: "icon",
component: AnyComponent(
Image(image: closeImage)
)
),
])),
action: { [weak component] in
component?.dismiss()
}
),
availableSize: CGSize(width: 30.0, height: 30.0),
transition: .immediate
)
context.add(closeButton
.position(CGPoint(x: context.availableSize.width - environment.safeInsets.left - closeButton.size.width, y: 28.0))
)
let buttonText: String
var buttonAnimationName: String?
if state.isPremium == true {
buttonText = strings.Common_OK
} else {
switch component.source {
case let .intro(price):
buttonText = strings.Premium_SubscribeFor(price ?? "").string
case let .gift(price):
buttonText = strings.Premium_Gift_GiftSubscription(price ?? "").string
case .other:
switch component.subject {
case .fasterDownload:
buttonText = strings.Premium_FasterSpeed_Proceed
case .advancedChatManagement:
buttonText = strings.Premium_ChatManagement_Proceed
case .uniqueReactions:
buttonText = strings.Premium_Reactions_Proceed
buttonAnimationName = "premium_unlock"
case .premiumStickers:
buttonText = strings.Premium_Stickers_Proceed
buttonAnimationName = "premium_unlock"
case .appIcons:
buttonText = strings.Premium_AppIcons_Proceed
buttonAnimationName = "premium_unlock"
case .noAds:
buttonText = strings.Premium_NoAds_Proceed
case .animatedEmoji:
buttonText = strings.Premium_AnimatedEmoji_Proceed
buttonAnimationName = "premium_unlock"
default:
buttonText = strings.Common_OK
}
}
}
let button = button.update(
component: SolidRoundedButtonComponent(
title: buttonText,
theme: SolidRoundedButtonComponent.Theme(
backgroundColor: .black,
backgroundColors: [
UIColor(rgb: 0x0077ff),
UIColor(rgb: 0x6b93ff),
UIColor(rgb: 0x8878ff),
UIColor(rgb: 0xe46ace)
],
foregroundColor: .white
),
font: .bold,
fontSize: 17.0,
height: 50.0,
cornerRadius: 11.0,
gloss: state.isPremium != true,
animationName: isStandalone ? buttonAnimationName : nil,
iconPosition: .right,
iconSpacing: 4.0,
action: { [weak component, weak state] in
guard let component = component else {
return
}
component.dismiss()
if let state = state, state.isPremium == false {
component.action()
}
}
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0),
transition: context.transition
)
var contentHeight: CGFloat = context.availableSize.width + 146.0
if case .other = component.source {
contentHeight -= 40.0
if [.advancedChatManagement, .fasterDownload].contains(component.subject) {
contentHeight += 20.0
}
}
let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + 20.0), size: button.size)
context.add(button
.position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY))
)
let bottomPanelPadding: CGFloat = 12.0
let bottomInset: CGFloat
if case .regular = environment.metrics.widthClass {
bottomInset = bottomPanelPadding
} else {
bottomInset = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding
}
return CGSize(width: context.availableSize.width, height: buttonFrame.maxY + bottomInset)
}
}
}
private final class DemoSheetComponent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let subject: PremiumDemoScreen.Subject
let source: PremiumDemoScreen.Source
let order: [PremiumPerk]?
let action: () -> Void
init(context: AccountContext, subject: PremiumDemoScreen.Subject, source: PremiumDemoScreen.Source, order: [PremiumPerk]?, action: @escaping () -> Void) {
self.context = context
self.subject = subject
self.source = source
self.order = order
self.action = action
}
static func ==(lhs: DemoSheetComponent, rhs: DemoSheetComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.subject != rhs.subject {
return false
}
if lhs.source != rhs.source {
return false
}
if lhs.order != rhs.order {
return false
}
return true
}
static var body: Body {
let sheet = Child(SheetComponent<(EnvironmentType)>.self)
let animateOut = StoredActionSlot(Action<Void>.self)
return { context in
let environment = context.environment[EnvironmentType.self]
let controller = environment.controller
let sheet = sheet.update(
component: SheetComponent<EnvironmentType>(
content: AnyComponent<EnvironmentType>(DemoSheetContent(
context: context.component.context,
subject: context.component.subject,
source: context.component.source,
order: context.component.order,
action: context.component.action,
dismiss: {
animateOut.invoke(Action { _ in
if let controller = controller() {
controller.dismiss(completion: nil)
}
})
}
)),
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
animateOut: animateOut
),
environment: {
environment
SheetComponentEnvironment(
isDisplaying: environment.value.isVisible,
isCentered: environment.metrics.widthClass == .regular,
hasInputHeight: !environment.inputHeight.isZero,
dismiss: { 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)
}
}
}
)
},
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 PremiumDemoScreen: ViewControllerComponentContainer {
public enum Subject {
case doubleLimits
case moreUpload
case fasterDownload
case voiceToText
case noAds
case uniqueReactions
case premiumStickers
case advancedChatManagement
case profileBadge
case animatedUserpics
case appIcons
case animatedEmoji
case emojiStatus
}
public enum Source: Equatable {
case intro(String?)
case gift(String?)
case other
}
var disposed: () -> Void = {}
private var didSetReady = false
private let _ready = Promise<Bool>()
public override var ready: Promise<Bool> {
return self._ready
}
public convenience init(context: AccountContext, subject: PremiumDemoScreen.Subject, source: PremiumDemoScreen.Source = .other, action: @escaping () -> Void) {
self.init(context: context, subject: subject, source: source, order: nil, action: action)
}
init(context: AccountContext, subject: PremiumDemoScreen.Subject, source: PremiumDemoScreen.Source = .other, order: [PremiumPerk]?, action: @escaping () -> Void) {
super.init(context: context, component: DemoSheetComponent(context: context, subject: subject, source: source, order: order, action: action), navigationBarAppearance: .none)
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
self.navigationPresentation = .flatModal
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.disposed()
}
public override func viewDidLoad() {
super.viewDidLoad()
self.view.disablesInteractiveModalDismiss = true
}
public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
if !self.didSetReady {
self.didSetReady = true
if let view = self.node.hostView.findTaggedView(tag: PhoneDemoComponent.View.Tag()) as? PhoneDemoComponent.View {
self._ready.set(view.ready)
} else {
self._ready.set(.single(true) |> delay(0.1, queue: Queue.mainQueue()))
}
}
}
}