mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
540 lines
21 KiB
Swift
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: Transition(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
|
|
}
|
|
}
|
|
}
|