Swiftgram/submodules/StatisticsUI/Sources/BoostHeaderItem.swift
2024-06-12 23:04:04 +04:00

540 lines
21 KiB
Swift

import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import Markdown
import ComponentFlow
import PremiumUI
import MultilineTextComponent
import BundleIconComponent
import PlainButtonComponent
import AccountContext
final class BoostHeaderItem: ItemListControllerHeaderItem {
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let status: ChannelBoostStatus?
let title: String
let text: String
let openBoost: () -> Void
let createGiveaway: () -> Void
let openFeatures: () -> Void
let back: () -> Void
let updateStatusBar: (StatusBarStyle) -> Void
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, status: ChannelBoostStatus?, title: String, text: String, openBoost: @escaping () -> Void, createGiveaway: @escaping () -> Void, openFeatures: @escaping () -> Void, back: @escaping () -> Void, updateStatusBar: @escaping (StatusBarStyle) -> Void) {
self.context = context
self.theme = theme
self.strings = strings
self.status = status
self.title = title
self.text = text
self.openBoost = openBoost
self.createGiveaway = createGiveaway
self.openFeatures = openFeatures
self.back = back
self.updateStatusBar = updateStatusBar
}
func isEqual(to: ItemListControllerHeaderItem) -> Bool {
if let item = to as? BoostHeaderItem {
return self.theme === item.theme && self.title == item.title && self.text == item.text && self.status == item.status
} else {
return false
}
}
func node(current: ItemListControllerHeaderItemNode?) -> ItemListControllerHeaderItemNode {
if let current = current as? BoostHeaderItemNode {
current.item = self
return current
} else {
return BoostHeaderItemNode(item: self)
}
}
}
private let titleFont = Font.semibold(17.0)
final class BoostHeaderItemNode: ItemListControllerHeaderItemNode {
private let backgroundNode: NavigationBackgroundNode
private let separatorNode: ASDisplayNode
private let whiteTitleNode: ImmediateTextNode
private let titleNode: ImmediateTextNode
private let backButton = PeerInfoHeaderNavigationButton()
private var hostView: ComponentHostView<Empty>?
private var component: AnyComponent<Empty>?
private var validLayout: ContainerViewLayout?
fileprivate var item: BoostHeaderItem {
didSet {
self.updateItem()
if let layout = self.validLayout {
let _ = self.updateLayout(layout: layout, transition: .animated(duration: 0.2, curve: .easeInOut))
}
}
}
init(item: BoostHeaderItem) {
self.item = item
self.backgroundNode = NavigationBackgroundNode(color: item.theme.rootController.navigationBar.blurredBackgroundColor)
self.backgroundNode.alpha = 0.0
self.separatorNode = ASDisplayNode()
self.separatorNode.alpha = 0.0
self.whiteTitleNode = ImmediateTextNode()
self.whiteTitleNode.isUserInteractionEnabled = false
self.whiteTitleNode.contentMode = .left
self.whiteTitleNode.contentsScale = UIScreen.main.scale
self.titleNode = ImmediateTextNode()
self.titleNode.alpha = 0.0
self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.separatorNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.whiteTitleNode)
self.addSubnode(self.backButton)
self.updateItem()
self.backButton.action = { [weak self] _, _ in
if let self {
self.item.back()
}
}
}
override func didLoad() {
super.didLoad()
let hostView = ComponentHostView<Empty>()
self.hostView = hostView
self.view.insertSubview(hostView, at: 0)
if let layout = self.validLayout, let component = self.component {
let navigationBarHeight: CGFloat = 44.0
let statusBarHeight = layout.statusBarHeight ?? 0.0
let containerSize = CGSize(width: layout.size.width, height: navigationBarHeight + statusBarHeight + 266.0)
let size = hostView.update(
transition: .immediate,
component: component,
environment: {},
containerSize: containerSize
)
hostView.frame = CGRect(origin: CGPoint(x: 0.0, y: -self.contentOffset), size: size)
}
}
func updateItem() {
self.backgroundNode.updateColor(color: self.item.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate)
self.separatorNode.backgroundColor = self.item.theme.rootController.navigationBar.separatorColor
self.titleNode.attributedText = NSAttributedString(string: self.item.title, font: titleFont, textColor: self.item.theme.list.itemPrimaryTextColor, paragraphAlignment: .center)
self.whiteTitleNode.attributedText = NSAttributedString(string: self.item.title, font: titleFont, textColor: .white, paragraphAlignment: .center)
}
private var contentOffset: CGFloat = 0.0
override func updateContentOffset(_ contentOffset: CGFloat, transition: ContainedViewLayoutTransition) {
guard let layout = self.validLayout else {
return
}
self.contentOffset = contentOffset
let topPanelAlpha = min(20.0, max(0.0, contentOffset - 44.0)) / 20.0
transition.updateAlpha(node: self.backgroundNode, alpha: topPanelAlpha)
transition.updateAlpha(node: self.separatorNode, alpha: topPanelAlpha)
transition.updateAlpha(node: self.titleNode, alpha: topPanelAlpha)
transition.updateAlpha(node: self.whiteTitleNode, alpha: 1.0 - topPanelAlpha)
let scrolledUp = topPanelAlpha < 0.5
self.backButton.updateContentsColor(backgroundColor: scrolledUp ? UIColor(white: 1.0, alpha: 0.2) : .clear, contentsColor: scrolledUp ? .white : self.item.theme.rootController.navigationBar.accentTextColor, canBeExpanded: !scrolledUp, transition: .animated(duration: 0.2, curve: .easeInOut))
if scrolledUp {
self.item.updateStatusBar(.White)
} else {
self.item.updateStatusBar(.Ignore)
}
if let hostView = self.hostView {
hostView.center = CGPoint(x: layout.size.width / 2.0, y: hostView.frame.height / 2.0 - contentOffset)
}
}
override func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) -> CGFloat {
let isFirstTime = self.validLayout == nil
let leftInset: CGFloat = 24.0
let navigationBarHeight: CGFloat = 44.0
let statusBarHeight = layout.statusBarHeight ?? 0.0
let constrainedSize = CGSize(width: layout.size.width - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude)
let titleSize = self.titleNode.updateLayout(constrainedSize)
let _ = self.whiteTitleNode.updateLayout(constrainedSize)
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: .zero, size: CGSize(width: layout.size.width, height: statusBarHeight + navigationBarHeight)))
transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: statusBarHeight + navigationBarHeight), size: CGSize(width: layout.size.width, height: UIScreenPixel)))
self.backgroundNode.update(size: CGSize(width: layout.size.width, height: statusBarHeight + navigationBarHeight), transition: transition)
let component = AnyComponent(BoostHeaderComponent(
strings: self.item.strings,
text: self.item.text,
status: self.item.status,
insets: layout.safeInsets,
openBoost: self.item.openBoost,
createGiveaway: self.item.createGiveaway,
openFeatures: self.item.openFeatures
))
let containerSize = CGSize(width: layout.size.width, height: navigationBarHeight + statusBarHeight + 266.0)
if let hostView = self.hostView {
let size = hostView.update(
transition: ComponentTransition(transition),
component: component,
environment: {},
containerSize: containerSize
)
hostView.frame = CGRect(origin: CGPoint(x: 0.0, y: -self.contentOffset), size: size)
}
self.titleNode.bounds = CGRect(origin: .zero, size: titleSize)
self.titleNode.position = CGPoint(x: layout.size.width / 2.0, y: statusBarHeight + navigationBarHeight / 2.0)
self.whiteTitleNode.bounds = self.titleNode.bounds
self.whiteTitleNode.position = self.titleNode.position
let backSize = self.backButton.update(key: .back, presentationData: self.item.context.sharedContext.currentPresentationData.with { $0 }, height: 44.0)
self.backButton.frame = CGRect(origin: CGPoint(x: layout.safeInsets.left + 16.0, y: statusBarHeight), size: backSize)
self.component = component
self.validLayout = layout
if isFirstTime {
self.backButton.updateContentsColor(backgroundColor: UIColor(white: 1.0, alpha: 0.2), contentsColor: .white, canBeExpanded: false, transition: .immediate)
}
return containerSize.height
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
if let hostView = self.hostView, hostView.frame.contains(point) {
return true
} else {
return super.point(inside: point, with: event)
}
}
}
private final class BoostHeaderComponent: CombinedComponent {
let strings: PresentationStrings
let text: String
let status: ChannelBoostStatus?
let insets: UIEdgeInsets
let openBoost: () -> Void
let createGiveaway: () -> Void
let openFeatures: () -> Void
public init(
strings: PresentationStrings,
text: String,
status: ChannelBoostStatus?,
insets: UIEdgeInsets,
openBoost: @escaping () -> Void,
createGiveaway: @escaping () -> Void,
openFeatures: @escaping () -> Void
) {
self.strings = strings
self.text = text
self.status = status
self.insets = insets
self.openBoost = openBoost
self.createGiveaway = createGiveaway
self.openFeatures = openFeatures
}
public static func ==(lhs: BoostHeaderComponent, rhs: BoostHeaderComponent) -> Bool {
if lhs.strings !== rhs.strings {
return false
}
if lhs.text != rhs.text {
return false
}
if lhs.status != rhs.status {
return false
}
if lhs.insets != rhs.insets {
return false
}
return true
}
public static var body: Body {
let background = Child(PremiumGradientBackgroundComponent.self)
let stars = Child(BoostHeaderBackgroundComponent.self)
let progress = Child(PremiumLimitDisplayComponent.self)
let text = Child(MultilineTextComponent.self)
let boostButton = Child(PlainButtonComponent.self)
let giveawayButton = Child(PlainButtonComponent.self)
let featuresButton = Child(PlainButtonComponent.self)
return { context in
let size = context.availableSize
let component = context.component
let sideInset: CGFloat = 16.0 + component.insets.left
let background = background.update(
component: PremiumGradientBackgroundComponent(
colors: [
UIColor(rgb: 0x0077ff),
UIColor(rgb: 0x6b93ff),
UIColor(rgb: 0x8878ff),
UIColor(rgb: 0xe46ace)
],
cornerRadius: 0.0,
topOverscroll: true
),
availableSize: size,
transition: context.transition
)
context.add(background
.position(CGPoint(x: size.width / 2.0, y: size.height / 2.0))
)
let stars = stars.update(
component: BoostHeaderBackgroundComponent(
isVisible: true,
hasIdleAnimations: true
),
availableSize: size,
transition: context.transition
)
context.add(stars
.position(CGPoint(x: size.width / 2.0, y: size.height / 2.0 + 10.0))
)
let boosts: Int
let level = component.status?.level ?? 0
let position: CGFloat
if let status = component.status {
if let nextLevelBoosts = status.nextLevelBoosts {
position = CGFloat(status.boosts - status.currentLevelBoosts) / CGFloat(nextLevelBoosts - status.currentLevelBoosts)
} else {
position = 1.0
}
boosts = status.boosts
} else {
boosts = 0
position = 0.0
}
let inactiveText = component.strings.ChannelBoost_Level("\(level)").string
let activeText = component.strings.ChannelBoost_Level("\(level + 1)").string
let progress = progress.update(
component: PremiumLimitDisplayComponent(
inactiveColor: UIColor.white.withAlphaComponent(0.2),
activeColors: [.white, .white],
inactiveTitle: inactiveText,
inactiveValue: "",
inactiveTitleColor: .white,
activeTitle: "",
activeValue: activeText,
activeTitleColor: UIColor(rgb: 0x6f8fff),
badgeIconName: "Premium/Boost",
badgeText: "\(boosts)",
badgePosition: position,
badgeGraphPosition: position,
invertProgress: true,
isPremiumDisabled: false
),
availableSize: CGSize(width: size.width - sideInset * 2.0, height: size.height),
transition: context.transition
)
context.add(progress
.position(CGPoint(x: size.width / 2.0, y: size.height / 2.0 - 36.0))
)
let font = Font.regular(15.0)
let boldFont = Font.semibold(15.0)
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: font, textColor: .white), bold: MarkdownAttributeSet(font: boldFont, textColor: .white), link: MarkdownAttributeSet(font: font, textColor: .white), linkAttribute: { _ in return nil})
let text = text.update(
component: MultilineTextComponent(
text: .markdown(text: component.text, attributes: markdownAttributes),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.2
),
availableSize: CGSize(width: size.width - sideInset * 3.0, height: size.height),
transition: context.transition
)
context.add(text
.position(CGPoint(x: size.width / 2.0, y: size.height - 114.0))
)
let minButtonWidth: CGFloat = 112.0
// let buttonSpacing = (size.width - sideInset * 2.0 - minButtonWidth * 3.0) / 2.0
let buttonHeight: CGFloat = 58.0
let boostButton = boostButton.update(
component: PlainButtonComponent(
content: AnyComponent(
BoostButtonComponent(
iconName: "Premium/Boosts/Boost",
title: component.strings.ChannelBoost_Header_Boost
)
),
effectAlignment: .center,
action: {
component.openBoost()
}
),
availableSize: CGSize(width: minButtonWidth, height: buttonHeight),
transition: context.transition
)
context.add(boostButton
.position(CGPoint(x: sideInset + minButtonWidth / 2.0, y: size.height - 45.0))
)
let giveawayButton = giveawayButton.update(
component: PlainButtonComponent(
content: AnyComponent(
BoostButtonComponent(
iconName: "Premium/Boosts/Giveaway",
title: component.strings.ChannelBoost_Header_Giveaway
)
),
effectAlignment: .center,
action: {
component.createGiveaway()
}
),
availableSize: CGSize(width: minButtonWidth, height: buttonHeight),
transition: context.transition
)
context.add(giveawayButton
.position(CGPoint(x: context.availableSize.width / 2.0, y: size.height - 45.0))
)
let featuresButton = featuresButton.update(
component: PlainButtonComponent(
content: AnyComponent(
BoostButtonComponent(
iconName: "Premium/Boosts/Features",
title: component.strings.ChannelBoost_Header_Features
)
),
effectAlignment: .center,
action: {
component.openFeatures()
}
),
availableSize: CGSize(width: minButtonWidth, height: buttonHeight),
transition: context.transition
)
context.add(featuresButton
.position(CGPoint(x: context.availableSize.width - sideInset - minButtonWidth / 2.0, y: size.height - 45.0))
)
return background.size
}
}
}
private final class BoostButtonComponent: CombinedComponent {
let iconName: String
let title: String
public init(
iconName: String,
title: String
) {
self.iconName = iconName
self.title = title
}
public static func ==(lhs: BoostButtonComponent, rhs: BoostButtonComponent) -> Bool {
if lhs.iconName != rhs.iconName {
return false
}
if lhs.title != rhs.title {
return false
}
return true
}
public static var body: Body {
let background = Child(RoundedRectangle.self)
let icon = Child(BundleIconComponent.self)
let title = Child(MultilineTextComponent.self)
return { context in
let size = context.availableSize
let component = context.component
let background = background.update(
component: RoundedRectangle(
color: UIColor.white.withAlphaComponent(0.2),
cornerRadius: 10.0
),
availableSize: context.availableSize,
transition: context.transition
)
context.add(background
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
)
let icon = icon.update(
component: BundleIconComponent(
name: component.iconName,
tintColor: .white
),
availableSize: context.availableSize,
transition: context.transition
)
context.add(icon
.position(CGPoint(x: size.width / 2.0, y: 21.0))
)
let title = title.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(
string: component.title,
font: Font.regular(11.0),
textColor: .white
)),
horizontalAlignment: .center,
maximumNumberOfLines: 1
),
availableSize: CGSize(width: size.width - 16.0, height: size.height),
transition: context.transition
)
context.add(title
.position(CGPoint(x: size.width / 2.0, y: size.height - 16.0))
)
return background.size
}
}
}