mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +00:00
525 lines
21 KiB
Swift
525 lines
21 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import Display
|
|
import ComponentFlow
|
|
import TelegramCore
|
|
import TelegramPresentationData
|
|
import AppBundle
|
|
import AccountContext
|
|
import MultilineTextComponent
|
|
import MultilineTextWithEntitiesComponent
|
|
import EmojiTextAttachmentView
|
|
import TextFormat
|
|
import ItemShimmeringLoadingComponent
|
|
import AvatarNode
|
|
|
|
public final class GiftItemComponent: Component {
|
|
public enum Subject: Equatable {
|
|
case premium(Int32)
|
|
case starGift(Int64, TelegramMediaFile)
|
|
}
|
|
|
|
public struct Ribbon: Equatable {
|
|
public let text: String
|
|
public let color: UIColor
|
|
|
|
public init(text: String, color: UIColor) {
|
|
self.text = text
|
|
self.color = color
|
|
}
|
|
}
|
|
|
|
let context: AccountContext
|
|
let theme: PresentationTheme
|
|
let peer: EnginePeer?
|
|
let subject: Subject
|
|
let title: String?
|
|
let subtitle: String?
|
|
let price: String
|
|
let ribbon: Ribbon?
|
|
let isLoading: Bool
|
|
|
|
public init(
|
|
context: AccountContext,
|
|
theme: PresentationTheme,
|
|
peer: EnginePeer?,
|
|
subject: Subject,
|
|
title: String? = nil,
|
|
subtitle: String? = nil,
|
|
price: String,
|
|
ribbon: Ribbon? = nil,
|
|
isLoading: Bool = false
|
|
) {
|
|
self.context = context
|
|
self.theme = theme
|
|
self.peer = peer
|
|
self.subject = subject
|
|
self.title = title
|
|
self.subtitle = subtitle
|
|
self.price = price
|
|
self.ribbon = ribbon
|
|
self.isLoading = isLoading
|
|
}
|
|
|
|
public static func ==(lhs: GiftItemComponent, rhs: GiftItemComponent) -> Bool {
|
|
if lhs.context !== rhs.context {
|
|
return false
|
|
}
|
|
if lhs.theme !== rhs.theme {
|
|
return false
|
|
}
|
|
if lhs.peer != rhs.peer {
|
|
return false
|
|
}
|
|
if lhs.subject != rhs.subject {
|
|
return false
|
|
}
|
|
if lhs.title != rhs.title {
|
|
return false
|
|
}
|
|
if lhs.subtitle != rhs.subtitle {
|
|
return false
|
|
}
|
|
if lhs.price != rhs.price {
|
|
return false
|
|
}
|
|
if lhs.ribbon != rhs.ribbon {
|
|
return false
|
|
}
|
|
if lhs.isLoading != rhs.isLoading {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
public final class View: UIView {
|
|
private var component: GiftItemComponent?
|
|
private weak var componentState: EmptyComponentState?
|
|
|
|
private let backgroundLayer = SimpleLayer()
|
|
private var loadingBackground: ComponentView<Empty>?
|
|
|
|
private var avatarNode: AvatarNode?
|
|
private let title = ComponentView<Empty>()
|
|
private let subtitle = ComponentView<Empty>()
|
|
private let button = ComponentView<Empty>()
|
|
private let ribbon = UIImageView()
|
|
private let ribbonText = ComponentView<Empty>()
|
|
|
|
private var animationLayer: InlineStickerItemLayer?
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
|
|
self.layer.addSublayer(self.backgroundLayer)
|
|
|
|
self.backgroundLayer.cornerRadius = 10.0
|
|
if #available(iOS 13.0, *) {
|
|
self.backgroundLayer.cornerCurve = .circular
|
|
}
|
|
self.backgroundLayer.masksToBounds = true
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func update(component: GiftItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
|
self.component = component
|
|
self.componentState = state
|
|
|
|
let size = CGSize(width: availableSize.width, height: component.title != nil ? 178.0 : 154.0)
|
|
|
|
if component.isLoading {
|
|
let loadingBackground: ComponentView<Empty>
|
|
if let current = self.loadingBackground {
|
|
loadingBackground = current
|
|
} else {
|
|
loadingBackground = ComponentView<Empty>()
|
|
self.loadingBackground = loadingBackground
|
|
}
|
|
|
|
let _ = loadingBackground.update(
|
|
transition: transition,
|
|
component: AnyComponent(
|
|
ItemShimmeringLoadingComponent(color: component.theme.list.itemAccentColor, cornerRadius: 10.0)
|
|
),
|
|
environment: {},
|
|
containerSize: size
|
|
)
|
|
if let loadingBackgroundView = loadingBackground.view {
|
|
if loadingBackgroundView.layer.superlayer == nil {
|
|
self.layer.insertSublayer(loadingBackgroundView.layer, above: self.backgroundLayer)
|
|
}
|
|
loadingBackgroundView.frame = CGRect(origin: .zero, size: size)
|
|
}
|
|
} else if let loadingBackground = self.loadingBackground {
|
|
loadingBackground.view?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
|
|
loadingBackground.view?.layer.removeFromSuperlayer()
|
|
})
|
|
self.loadingBackground = nil
|
|
}
|
|
|
|
let emoji: ChatTextInputTextCustomEmojiAttribute?
|
|
var file: TelegramMediaFile?
|
|
var animationOffset: CGFloat = 0.0
|
|
switch component.subject {
|
|
case let .premium(months):
|
|
emoji = ChatTextInputTextCustomEmojiAttribute(
|
|
interactivelySelectedFromPackId: nil,
|
|
fileId: 0,
|
|
file: nil,
|
|
custom: .animation(name: "Gift\(months)")
|
|
)
|
|
case let .starGift(_, fileValue):
|
|
file = fileValue
|
|
emoji = ChatTextInputTextCustomEmojiAttribute(
|
|
interactivelySelectedFromPackId: nil,
|
|
fileId: fileValue.fileId.id,
|
|
file: fileValue
|
|
)
|
|
animationOffset = 16.0
|
|
}
|
|
|
|
let iconSize = CGSize(width: 88.0, height: 88.0)
|
|
if self.animationLayer == nil, let emoji {
|
|
let animationLayer = InlineStickerItemLayer(
|
|
context: .account(component.context),
|
|
userLocation: .other,
|
|
attemptSynchronousLoad: false,
|
|
emoji: emoji,
|
|
file: file,
|
|
cache: component.context.animationCache,
|
|
renderer: component.context.animationRenderer,
|
|
unique: false,
|
|
placeholderColor: component.theme.list.mediaPlaceholderColor,
|
|
pointSize: CGSize(width: iconSize.width * 2.0, height: iconSize.height * 2.0),
|
|
loopCount: 1
|
|
)
|
|
animationLayer.isVisibleForAnimations = true
|
|
self.animationLayer = animationLayer
|
|
self.layer.addSublayer(animationLayer)
|
|
}
|
|
|
|
if let animationLayer = self.animationLayer {
|
|
transition.setFrame(layer: animationLayer, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - iconSize.width) / 2.0), y: animationOffset), size: iconSize))
|
|
}
|
|
|
|
if let title = component.title {
|
|
let titleSize = self.title.update(
|
|
transition: transition,
|
|
component: AnyComponent(
|
|
MultilineTextComponent(
|
|
text: .plain(NSAttributedString(string: title, font: Font.semibold(15.0), textColor: component.theme.list.itemPrimaryTextColor)),
|
|
horizontalAlignment: .center
|
|
)
|
|
),
|
|
environment: {},
|
|
containerSize: availableSize
|
|
)
|
|
let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - titleSize.width) / 2.0), y: 94.0), size: titleSize)
|
|
if let titleView = self.title.view {
|
|
if titleView.superview == nil {
|
|
self.addSubview(titleView)
|
|
}
|
|
transition.setFrame(view: titleView, frame: titleFrame)
|
|
}
|
|
}
|
|
|
|
if let subtitle = component.subtitle {
|
|
let subtitleSize = self.subtitle.update(
|
|
transition: transition,
|
|
component: AnyComponent(
|
|
MultilineTextComponent(
|
|
text: .plain(NSAttributedString(string: subtitle, font: Font.regular(13.0), textColor: component.theme.list.itemPrimaryTextColor)),
|
|
horizontalAlignment: .center
|
|
)
|
|
),
|
|
environment: {},
|
|
containerSize: availableSize
|
|
)
|
|
let subtitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - subtitleSize.width) / 2.0), y: 112.0), size: subtitleSize)
|
|
if let subtitleView = self.subtitle.view {
|
|
if subtitleView.superview == nil {
|
|
self.addSubview(subtitleView)
|
|
}
|
|
transition.setFrame(view: subtitleView, frame: subtitleFrame)
|
|
}
|
|
}
|
|
|
|
let buttonSize = self.button.update(
|
|
transition: transition,
|
|
component: AnyComponent(
|
|
ButtonContentComponent(
|
|
context: component.context,
|
|
text: component.price,
|
|
color: component.price.containsEmoji ? UIColor(rgb: 0xd3720a) : component.theme.list.itemAccentColor,
|
|
isStars: component.price.containsEmoji)
|
|
),
|
|
environment: {},
|
|
containerSize: availableSize
|
|
)
|
|
let buttonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - buttonSize.width) / 2.0), y: size.height - buttonSize.height - 10.0), size: buttonSize)
|
|
if let buttonView = self.button.view {
|
|
if buttonView.superview == nil {
|
|
self.addSubview(buttonView)
|
|
}
|
|
transition.setFrame(view: buttonView, frame: buttonFrame)
|
|
}
|
|
|
|
if let ribbon = component.ribbon {
|
|
let ribbonTextSize = self.ribbonText.update(
|
|
transition: transition,
|
|
component: AnyComponent(
|
|
MultilineTextComponent(
|
|
text: .plain(NSAttributedString(string: ribbon.text, font: Font.semibold(11.0), textColor: .white)),
|
|
horizontalAlignment: .center
|
|
)
|
|
),
|
|
environment: {},
|
|
containerSize: availableSize
|
|
)
|
|
if let ribbonTextView = self.ribbonText.view {
|
|
if ribbonTextView.superview == nil {
|
|
self.addSubview(self.ribbon)
|
|
self.addSubview(ribbonTextView)
|
|
}
|
|
ribbonTextView.bounds = CGRect(origin: .zero, size: ribbonTextSize)
|
|
|
|
if self.ribbon.image == nil {
|
|
self.ribbon.image = generateGradientTintedImage(image: UIImage(bundleImageName: "Premium/GiftRibbon"), colors: [ribbon.color.withMultipliedBrightnessBy(1.1), ribbon.color.withMultipliedBrightnessBy(0.9)], direction: .diagonal)
|
|
}
|
|
if let ribbonImage = self.ribbon.image {
|
|
self.ribbon.frame = CGRect(origin: CGPoint(x: size.width - ribbonImage.size.width + 2.0, y: -2.0), size: ribbonImage.size)
|
|
}
|
|
ribbonTextView.transform = CGAffineTransform(rotationAngle: .pi / 4.0)
|
|
ribbonTextView.center = CGPoint(x: size.width - 20.0, y: 20.0)
|
|
}
|
|
} else {
|
|
if self.ribbonText.view?.superview != nil {
|
|
self.ribbon.removeFromSuperview()
|
|
self.ribbonText.view?.removeFromSuperview()
|
|
}
|
|
}
|
|
|
|
if let peer = component.peer {
|
|
let avatarNode: AvatarNode
|
|
if let current = self.avatarNode {
|
|
avatarNode = current
|
|
} else {
|
|
avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 8.0))
|
|
self.addSubview(avatarNode.view)
|
|
self.avatarNode = avatarNode
|
|
}
|
|
|
|
avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, displayDimensions: CGSize(width: 20.0, height: 20.0))
|
|
avatarNode.frame = CGRect(origin: CGPoint(x: 2.0, y: 2.0), size: CGSize(width: 20.0, height: 20.0))
|
|
}
|
|
|
|
self.backgroundLayer.backgroundColor = component.theme.list.itemBlocksBackgroundColor.cgColor
|
|
transition.setFrame(layer: self.backgroundLayer, frame: CGRect(origin: .zero, size: size))
|
|
|
|
return size
|
|
}
|
|
}
|
|
|
|
public func makeView() -> View {
|
|
return View(frame: CGRect())
|
|
}
|
|
|
|
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
|
}
|
|
}
|
|
|
|
private final class ButtonContentComponent: Component {
|
|
let context: AccountContext
|
|
let text: String
|
|
let color: UIColor
|
|
let isStars: Bool
|
|
|
|
public init(
|
|
context: AccountContext,
|
|
text: String,
|
|
color: UIColor,
|
|
isStars: Bool = false
|
|
) {
|
|
self.context = context
|
|
self.text = text
|
|
self.color = color
|
|
self.isStars = isStars
|
|
}
|
|
|
|
public static func ==(lhs: ButtonContentComponent, rhs: ButtonContentComponent) -> Bool {
|
|
if lhs.context !== rhs.context {
|
|
return false
|
|
}
|
|
if lhs.text != rhs.text {
|
|
return false
|
|
}
|
|
if lhs.color != rhs.color {
|
|
return false
|
|
}
|
|
if lhs.isStars != rhs.isStars {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
public final class View: UIView {
|
|
private var component: ButtonContentComponent?
|
|
private weak var componentState: EmptyComponentState?
|
|
|
|
private let backgroundLayer = SimpleLayer()
|
|
private let title = ComponentView<Empty>()
|
|
|
|
private var starsLayer: StarsButtonEffectLayer?
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
|
|
self.layer.addSublayer(self.backgroundLayer)
|
|
self.backgroundLayer.masksToBounds = true
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func update(component: ButtonContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
|
self.component = component
|
|
self.componentState = state
|
|
|
|
let attributedText = NSMutableAttributedString(string: component.text, font: Font.semibold(11.0), textColor: component.color)
|
|
let range = (attributedText.string as NSString).range(of: "⭐️")
|
|
if range.location != NSNotFound {
|
|
attributedText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range)
|
|
attributedText.addAttribute(.font, value: Font.semibold(15.0), range: range)
|
|
attributedText.addAttribute(.baselineOffset, value: 2.0, range: NSRange(location: range.upperBound, length: attributedText.length - range.upperBound))
|
|
}
|
|
|
|
let titleSize = self.title.update(
|
|
transition: transition,
|
|
component: AnyComponent(
|
|
MultilineTextWithEntitiesComponent(
|
|
context: component.context,
|
|
animationCache: component.context.animationCache,
|
|
animationRenderer: component.context.animationRenderer,
|
|
placeholderColor: .white,
|
|
text: .plain(attributedText)
|
|
)
|
|
),
|
|
environment: {},
|
|
containerSize: availableSize
|
|
)
|
|
|
|
let padding: CGFloat = 9.0
|
|
let size = CGSize(width: titleSize.width + padding * 2.0, height: 30.0)
|
|
|
|
if component.isStars {
|
|
let starsLayer: StarsButtonEffectLayer
|
|
if let current = self.starsLayer {
|
|
starsLayer = current
|
|
} else {
|
|
starsLayer = StarsButtonEffectLayer()
|
|
self.layer.addSublayer(starsLayer)
|
|
self.starsLayer = starsLayer
|
|
}
|
|
starsLayer.frame = CGRect(origin: .zero, size: size)
|
|
starsLayer.update(size: size)
|
|
} else {
|
|
self.starsLayer?.removeFromSuperlayer()
|
|
self.starsLayer = nil
|
|
}
|
|
|
|
let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize)
|
|
if let titleView = self.title.view {
|
|
if titleView.superview == nil {
|
|
self.addSubview(titleView)
|
|
}
|
|
transition.setFrame(view: titleView, frame: titleFrame)
|
|
}
|
|
|
|
let backgroundColor: UIColor
|
|
if component.color.rgb == 0xd3720a {
|
|
backgroundColor = UIColor(rgb: 0xffc83d, alpha: 0.2)
|
|
} else {
|
|
backgroundColor = component.color.withAlphaComponent(0.1)
|
|
}
|
|
|
|
self.backgroundLayer.backgroundColor = backgroundColor.cgColor
|
|
transition.setFrame(layer: self.backgroundLayer, frame: CGRect(origin: .zero, size: size))
|
|
self.backgroundLayer.cornerRadius = size.height / 2.0
|
|
|
|
return size
|
|
}
|
|
}
|
|
|
|
public func makeView() -> View {
|
|
return View(frame: CGRect())
|
|
}
|
|
|
|
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
|
}
|
|
}
|
|
|
|
private final class StarsButtonEffectLayer: SimpleLayer {
|
|
let emitterLayer = CAEmitterLayer()
|
|
|
|
override init() {
|
|
super.init()
|
|
|
|
self.addSublayer(self.emitterLayer)
|
|
}
|
|
|
|
override init(layer: Any) {
|
|
super.init(layer: layer)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
private func setup() {
|
|
let color = UIColor(rgb: 0xffbe27)
|
|
|
|
let emitter = CAEmitterCell()
|
|
emitter.name = "emitter"
|
|
emitter.contents = UIImage(bundleImageName: "Premium/Stars/Particle")?.cgImage
|
|
emitter.birthRate = 25.0
|
|
emitter.lifetime = 2.0
|
|
emitter.velocity = 12.0
|
|
emitter.velocityRange = 3
|
|
emitter.scale = 0.1
|
|
emitter.scaleRange = 0.08
|
|
emitter.alphaRange = 0.1
|
|
emitter.emissionRange = .pi * 2.0
|
|
emitter.setValue(3.0, forKey: "mass")
|
|
emitter.setValue(2.0, forKey: "massRange")
|
|
|
|
let staticColors: [Any] = [
|
|
color.withAlphaComponent(0.0).cgColor,
|
|
color.cgColor,
|
|
color.cgColor,
|
|
color.withAlphaComponent(0.0).cgColor
|
|
]
|
|
let staticColorBehavior = CAEmitterCell.createEmitterBehavior(type: "colorOverLife")
|
|
staticColorBehavior.setValue(staticColors, forKey: "colors")
|
|
emitter.setValue([staticColorBehavior], forKey: "emitterBehaviors")
|
|
|
|
self.emitterLayer.emitterCells = [emitter]
|
|
}
|
|
|
|
func update(size: CGSize) {
|
|
if self.emitterLayer.emitterCells == nil {
|
|
self.setup()
|
|
}
|
|
self.emitterLayer.emitterShape = .circle
|
|
self.emitterLayer.emitterSize = CGSize(width: size.width * 0.7, height: size.height * 0.7)
|
|
self.emitterLayer.emitterMode = .surface
|
|
self.emitterLayer.frame = CGRect(origin: .zero, size: size)
|
|
self.emitterLayer.emitterPosition = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
|
|
}
|
|
}
|