2025-04-12 02:19:32 +04:00

1187 lines
53 KiB
Swift

import Foundation
import UIKit
import Display
import ComponentFlow
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import AppBundle
import AccountContext
import MultilineTextComponent
import MultilineTextWithEntitiesComponent
import EmojiTextAttachmentView
import TextFormat
import ItemShimmeringLoadingComponent
import AvatarNode
import PeerInfoCoverComponent
import Markdown
public final class GiftItemComponent: Component {
public enum Subject: Equatable {
case premium(months: Int32, price: String)
case starGift(gift: StarGift.Gift, price: String)
case uniqueGift(gift: StarGift.UniqueGift, price: String?)
}
public struct Ribbon: Equatable {
public enum Color: Equatable {
case red
case blue
case purple
case green
case custom(Int32, Int32)
func colors(theme: PresentationTheme) -> [UIColor] {
switch self {
case .red:
if theme.overallDarkAppearance {
return [
UIColor(rgb: 0x522124),
UIColor(rgb: 0x653634)
]
} else {
return [
UIColor(rgb: 0xed1c26),
UIColor(rgb: 0xff5c55)
]
}
case .blue:
if theme.overallDarkAppearance {
return [
UIColor(rgb: 0x142e42),
UIColor(rgb: 0x354f5b)
]
} else {
return [
UIColor(rgb: 0x34a4fc),
UIColor(rgb: 0x6fd3ff)
]
}
case .purple:
return [
UIColor(rgb: 0x747bf6),
UIColor(rgb: 0xe367d8)
]
case .green:
return [
UIColor(rgb: 0x4bb121),
UIColor(rgb: 0x53d654)
]
case let .custom(topColor, _):
return [
UIColor(rgb: UInt32(bitPattern: topColor)).withMultiplied(hue: 0.97, saturation: 1.45, brightness: 0.89),
UIColor(rgb: UInt32(bitPattern: topColor)).withMultiplied(hue: 1.01, saturation: 1.22, brightness: 1.04)
]
}
}
}
public enum Font {
case generic
case larger
case monospaced
}
public let text: String
public let font: Font
public let color: Color
public let outline: UIColor?
public init(
text: String,
font: Font = .generic,
color: Color,
outline: UIColor? = nil
) {
self.text = text
self.font = font
self.color = color
self.outline = outline
}
}
public enum Peer: Equatable {
case peer(EnginePeer)
case anonymous
}
public enum Mode: Equatable {
case generic
case profile
case thumbnail
case preview
case grid
}
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let peer: GiftItemComponent.Peer?
let subject: GiftItemComponent.Subject
let title: String?
let subtitle: String?
let label: String?
let ribbon: Ribbon?
let resellPrice: Int64?
let isLoading: Bool
let isHidden: Bool
let isSoldOut: Bool
let isSelected: Bool
let isPinned: Bool
let isEditing: Bool
let mode: Mode
let action: (() -> Void)?
let contextAction: ((UIView, ContextGesture) -> Void)?
public init(
context: AccountContext,
theme: PresentationTheme,
strings: PresentationStrings,
peer: GiftItemComponent.Peer? = nil,
subject: GiftItemComponent.Subject,
title: String? = nil,
subtitle: String? = nil,
label: String? = nil,
ribbon: Ribbon? = nil,
resellPrice: Int64? = nil,
isLoading: Bool = false,
isHidden: Bool = false,
isSoldOut: Bool = false,
isSelected: Bool = false,
isPinned: Bool = false,
isEditing: Bool = false,
mode: Mode = .generic,
action: (() -> Void)? = nil,
contextAction: ((UIView, ContextGesture) -> Void)? = nil
) {
self.context = context
self.theme = theme
self.strings = strings
self.peer = peer
self.subject = subject
self.title = title
self.subtitle = subtitle
self.label = label
self.ribbon = ribbon
self.resellPrice = resellPrice
self.isLoading = isLoading
self.isHidden = isHidden
self.isSoldOut = isSoldOut
self.isSelected = isSelected
self.isPinned = isPinned
self.isEditing = isEditing
self.mode = mode
self.action = action
self.contextAction = contextAction
}
public static func ==(lhs: GiftItemComponent, rhs: GiftItemComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.peer != rhs.peer {
return false
}
if lhs.subject != rhs.subject {
return false
}
if lhs.label != rhs.label {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.subtitle != rhs.subtitle {
return false
}
if lhs.ribbon != rhs.ribbon {
return false
}
if lhs.resellPrice != rhs.resellPrice {
return false
}
if lhs.isLoading != rhs.isLoading {
return false
}
if lhs.isHidden != rhs.isHidden {
return false
}
if lhs.isSoldOut != rhs.isSoldOut {
return false
}
if lhs.isSelected != rhs.isSelected {
return false
}
if lhs.isPinned != rhs.isPinned {
return false
}
if lhs.isEditing != rhs.isEditing {
return false
}
if lhs.mode != rhs.mode {
return false
}
if (lhs.contextAction == nil) != (rhs.contextAction == nil) {
return false
}
return true
}
public final class View: ContextControllerSourceView {
private var component: GiftItemComponent?
private weak var componentState: EmptyComponentState?
private let containerButton = HighlightTrackingButton()
private let backgroundLayer = SimpleLayer()
private var loadingBackground: ComponentView<Empty>?
private let patternView = ComponentView<Empty>()
private var avatarNode: AvatarNode?
private let title = ComponentView<Empty>()
private let subtitle = ComponentView<Empty>()
private let button = ComponentView<Empty>()
private let label = ComponentView<Empty>()
private let ribbonOutline = UIImageView()
private let ribbon = UIImageView()
private let ribbonText = ComponentView<Empty>()
private var animationLayer: InlineStickerItemLayer?
private var selectionLayer: SimpleShapeLayer?
private var animationFile: TelegramMediaFile?
private var disposables = DisposableSet()
private var fetchedFiles = Set<Int64>()
private var iconBackground: UIVisualEffectView?
private var hiddenIcon: UIImageView?
private var pinnedIcon: UIImageView?
private var resellBackground: BlurredBackgroundView?
private let reselLabel = ComponentView<Empty>()
override init(frame: CGRect) {
super.init(frame: frame)
self.layer.addSublayer(self.backgroundLayer)
if #available(iOS 13.0, *) {
self.backgroundLayer.cornerCurve = .circular
}
self.backgroundLayer.masksToBounds = true
self.activated = { [weak self] gesture, _ in
guard let self, let component = self.component else {
gesture.cancel()
return
}
component.contextAction?(self, gesture)
}
self.containerButton.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.disposables.dispose()
}
@objc private func buttonPressed() {
self.component?.action?()
}
func update(component: GiftItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let isFirstTime = self.component == nil
let previousComponent = self.component
self.component = component
self.componentState = state
self.isGestureEnabled = component.contextAction != nil
var themeUpdated = false
if previousComponent?.theme !== component.theme {
themeUpdated = true
}
var size: CGSize
let iconSize: CGSize
let cornerRadius: CGFloat
switch component.mode {
case .generic:
size = CGSize(width: availableSize.width, height: component.title != nil ? 178.0 : 154.0)
if let _ = component.label {
size.height += 23.0
}
iconSize = CGSize(width: 88.0, height: 88.0)
cornerRadius = 10.0
case .profile:
size = availableSize
let side = floor(88.0 * availableSize.height / 116.0)
iconSize = CGSize(width: side, height: side)
cornerRadius = 10.0
case .thumbnail:
size = CGSize(width: availableSize.width, height: availableSize.width)
iconSize = CGSize(width: floor(size.width * 0.7), height: floor(size.width * 0.7))
cornerRadius = floor(availableSize.width * 0.2)
case .grid:
size = CGSize(width: availableSize.width, height: availableSize.width)
iconSize = CGSize(width: floor(size.width * 0.7), height: floor(size.width * 0.7))
cornerRadius = 10.0
case .preview:
size = availableSize
iconSize = CGSize(width: floor(size.width * 0.6), height: floor(size.width * 0.6))
cornerRadius = 4.0
}
var backgroundSize = size
if case .grid = component.mode {
backgroundSize = CGSize(width: backgroundSize.width - 4.0, height: backgroundSize.height - 4.0)
}
self.backgroundLayer.cornerRadius = cornerRadius
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
}
var animationFile: TelegramMediaFile?
var backgroundColor: UIColor?
var secondBackgroundColor: UIColor?
var patternColor: UIColor?
var patternFile: TelegramMediaFile?
var files: [Int64: TelegramMediaFile] = [:]
var placeholderColor = component.theme.list.mediaPlaceholderColor
let emoji: ChatTextInputTextCustomEmojiAttribute?
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(gift, _):
animationFile = gift.file
emoji = ChatTextInputTextCustomEmojiAttribute(
interactivelySelectedFromPackId: nil,
fileId: gift.file.fileId.id,
file: gift.file
)
animationOffset = 16.0
case let .uniqueGift(gift, _):
animationOffset = 16.0
for attribute in gift.attributes {
switch attribute {
case let .model(_, file, _):
animationFile = file
if !self.fetchedFiles.contains(file.fileId.id) {
self.disposables.add(freeMediaFileResourceInteractiveFetched(account: component.context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start())
self.fetchedFiles.insert(file.fileId.id)
}
case let .pattern(_, file, _):
patternFile = file
files[file.fileId.id] = file
case let .backdrop(_, _, innerColorValue, outerColorValue, patternColorValue, _, _):
backgroundColor = UIColor(rgb: UInt32(bitPattern: outerColorValue))
secondBackgroundColor = UIColor(rgb: UInt32(bitPattern: innerColorValue))
patternColor = UIColor(rgb: UInt32(bitPattern: patternColorValue))
if let backgroundColor {
placeholderColor = backgroundColor
}
default:
break
}
}
if let animationFile {
emoji = ChatTextInputTextCustomEmojiAttribute(
interactivelySelectedFromPackId: nil,
fileId: animationFile.fileId.id,
file: animationFile
)
} else {
emoji = nil
}
}
var animationTransition = transition
if self.animationLayer == nil || self.animationFile?.fileId != animationFile?.fileId, let emoji {
animationTransition = .immediate
self.animationFile = animationFile
if let animationLayer = self.animationLayer {
self.animationLayer = nil
animationLayer.removeFromSuperlayer()
}
let animationLayer = InlineStickerItemLayer(
context: .account(component.context),
userLocation: .other,
attemptSynchronousLoad: false,
emoji: emoji,
file: animationFile,
cache: component.context.animationCache,
renderer: component.context.animationRenderer,
unique: false,
placeholderColor: placeholderColor,
pointSize: CGSize(width: iconSize.width * 2.0, height: iconSize.height * 2.0),
loopCount: 1
)
animationLayer.isVisibleForAnimations = true
self.animationLayer = animationLayer
if let patternView = self.patternView.view {
self.layer.insertSublayer(animationLayer, above: patternView.layer)
} else {
self.layer.insertSublayer(animationLayer, above: self.backgroundLayer)
}
}
let animationFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - iconSize.width) / 2.0), y: component.mode == .generic ? animationOffset : floorToScreenPixels((size.height - iconSize.height) / 2.0)), size: iconSize)
if let animationLayer = self.animationLayer {
animationTransition.setFrame(layer: animationLayer, frame: animationFrame)
}
if let backgroundColor {
let _ = self.patternView.update(
transition: .immediate,
component: AnyComponent(PeerInfoCoverComponent(
context: component.context,
subject: .custom(backgroundColor, secondBackgroundColor, patternColor, patternFile?.fileId.id),
files: files,
isDark: false,
avatarCenter: CGPoint(x: backgroundSize.width / 2.0, y: animationFrame.midY),
avatarScale: 1.0,
defaultHeight: backgroundSize.height,
avatarTransitionFraction: 0.0,
patternTransitionFraction: 0.0
)),
environment: {},
containerSize: backgroundSize
)
if let backgroundView = self.patternView.view {
if backgroundView.superview == nil {
backgroundView.layer.cornerRadius = cornerRadius
if #available(iOS 13.0, *) {
backgroundView.layer.cornerCurve = .circular
}
backgroundView.clipsToBounds = true
self.insertSubview(backgroundView, at: 1)
}
backgroundView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - backgroundSize.width) / 2.0), y: floorToScreenPixels((size.height - backgroundSize.height) / 2.0)), size: backgroundSize)
}
}
if case .generic = component.mode {
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 buttonColor: UIColor
var starsColor: UIColor?
var tinted = false
let price: String
switch component.subject {
case let .premium(_, priceValue), let .starGift(_, priceValue):
if priceValue.containsEmoji {
buttonColor = component.theme.overallDarkAppearance ? UIColor(rgb: 0xffc337) : UIColor(rgb: 0xd3720a)
if !component.isSoldOut {
starsColor = UIColor(rgb: 0xffbe27)
}
} else {
buttonColor = component.theme.list.itemAccentColor
}
price = priceValue
case let .uniqueGift(_, priceValue):
buttonColor = UIColor.white
price = priceValue ?? component.strings.Gift_Options_Gift_Transfer
tinted = true
}
let buttonSize = self.button.update(
transition: transition,
component: AnyComponent(
ButtonContentComponent(
context: component.context,
text: price,
color: buttonColor,
tinted: tinted,
starsColor: starsColor
)
),
environment: {},
containerSize: availableSize
)
var bottomOffset: CGFloat = 10.0
if let _ = component.label {
bottomOffset += 23.0
}
let buttonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - buttonSize.width) / 2.0), y: size.height - buttonSize.height - bottomOffset), size: buttonSize)
if let buttonView = self.button.view {
if buttonView.superview == nil {
self.addSubview(buttonView)
}
transition.setFrame(view: buttonView, frame: buttonFrame)
}
if let label = component.label {
let labelColor = component.theme.overallDarkAppearance ? UIColor(rgb: 0xffc337) : UIColor(rgb: 0xd3720a)
let attributes = MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(11.0), textColor: labelColor),
bold: MarkdownAttributeSet(font: Font.semibold(11.0), textColor: labelColor),
link: MarkdownAttributeSet(font: Font.regular(11.0), textColor: labelColor),
linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
}
)
let labelText = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(label, attributes: attributes))
if let range = labelText.string.range(of: "#") {
labelText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: labelText.string))
}
let labelSize = self.label.update(
transition: transition,
component: AnyComponent(
MultilineTextWithEntitiesComponent(
context: component.context,
animationCache: component.context.animationCache,
animationRenderer: component.context.animationRenderer,
placeholderColor: .white,
text: .plain(labelText),
horizontalAlignment: .center
)
),
environment: {},
containerSize: availableSize
)
let labelFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - labelSize.width) / 2.0), y: 178.0), size: labelSize)
if let labelView = self.label.view {
if labelView.superview == nil {
self.addSubview(labelView)
}
transition.setFrame(view: labelView, frame: labelFrame)
}
}
}
if let ribbon = component.ribbon {
let ribbonFontSize: CGFloat
if case .profile = component.mode {
ribbonFontSize = 9.0
} else {
ribbonFontSize = 10.0
}
let ribbonFont: UIFont
switch ribbon.font {
case .generic:
ribbonFont = Font.semibold(ribbonFontSize)
case .larger:
ribbonFont = Font.semibold(10.0)
case .monospaced:
ribbonFont = Font.with(size: 10.0, design: .monospace, weight: .semibold)
}
let ribbonTextSize = self.ribbonText.update(
transition: transition,
component: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: ribbon.text, font: ribbonFont, 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 let _ = component.ribbon?.outline {
if self.ribbonOutline.image == nil || themeUpdated || previousComponent?.ribbon?.outline != component.ribbon?.outline {
self.ribbonOutline.image = ribbonOutlineImage
self.ribbonOutline.tintColor = component.ribbon?.outline
if self.ribbonOutline.superview == nil {
self.insertSubview(self.ribbonOutline, belowSubview: self.ribbon)
}
}
} else if self.ribbonOutline.superview != nil {
self.ribbonOutline.removeFromSuperview()
}
if self.ribbon.image == nil || themeUpdated || previousComponent?.ribbon?.color != component.ribbon?.color {
var direction: GradientImageDirection = .mirroredDiagonal
if case .custom = ribbon.color {
direction = .mirroredDiagonal
}
self.ribbon.image = generateGradientTintedImage(image: UIImage(bundleImageName: "Premium/GiftRibbon"), colors: ribbon.color.colors(theme: component.theme), direction: direction)
}
var ribbonOffset: CGPoint = CGPoint(x: 2.0, y: -2.0)
if case .grid = component.mode {
ribbonOffset = .zero
}
if let ribbonImage = self.ribbon.image {
self.ribbon.frame = CGRect(origin: CGPoint(x: size.width - ribbonImage.size.width + ribbonOffset.x, y: ribbonOffset.y), size: ribbonImage.size)
}
if let ribbonOutlineImage = self.ribbonOutline.image {
self.ribbonOutline.frame = ribbonOutlineImage.size.centered(around: self.ribbon.center.offsetBy(dx: 0.0, dy: 2.0))
}
ribbonTextView.transform = CGAffineTransform(rotationAngle: .pi / 4.0)
ribbonTextView.center = CGPoint(x: size.width - 22.0 + ribbonOffset.x, y: 22.0 + ribbonOffset.y)
}
} else {
if self.ribbonText.view?.superview != nil {
self.ribbonOutline.removeFromSuperview()
self.ribbon.removeFromSuperview()
self.ribbonText.view?.removeFromSuperview()
}
}
if let peer = component.peer, !component.isPinned {
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
}
switch peer {
case let .peer(peer):
avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, displayDimensions: CGSize(width: 20.0, height: 20.0))
case .anonymous:
avatarNode.setPeer(context: component.context, theme: component.theme, peer: nil, overrideImage: .anonymousSavedMessagesIcon(isColored: true))
}
avatarNode.frame = CGRect(origin: CGPoint(x: 5.0, y: 5.0), size: CGSize(width: 20.0, height: 20.0))
} else if let avatarNode = self.avatarNode {
self.avatarNode = nil
avatarNode.view.removeFromSuperview()
}
if let backgroundColor, let _ = secondBackgroundColor {
self.backgroundLayer.backgroundColor = backgroundColor.cgColor
} else {
self.backgroundLayer.backgroundColor = component.theme.list.itemBlocksBackgroundColor.cgColor
}
let backgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - backgroundSize.width) / 2.0), y: floorToScreenPixels((size.height - backgroundSize.height) / 2.0)), size: backgroundSize)
transition.setFrame(layer: self.backgroundLayer, frame: backgroundFrame)
transition.setFrame(view: self.containerButton, frame: CGRect(origin: .zero, size: size))
var iconBackgroundSize: CGSize?
if component.isEditing {
if !component.isPinned && backgroundColor != nil {
iconBackgroundSize = CGSize(width: 48.0, height: 48.0)
}
} else {
if component.isHidden {
iconBackgroundSize = CGSize(width: 30.0, height: 30.0)
}
}
if let iconBackgroundSize {
let iconBackground: UIVisualEffectView
var iconBackgroundTransition = transition
if let currentBackground = self.iconBackground {
iconBackground = currentBackground
} else {
iconBackgroundTransition = .immediate
let blurEffect: UIBlurEffect
if #available(iOS 13.0, *) {
blurEffect = UIBlurEffect(style: .systemThinMaterialDark)
} else {
blurEffect = UIBlurEffect(style: .dark)
}
iconBackground = UIVisualEffectView(effect: blurEffect)
iconBackground.clipsToBounds = true
self.iconBackground = iconBackground
self.addSubview(iconBackground)
if !isFirstTime {
iconBackground.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2)
iconBackground.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
iconBackgroundTransition.containedViewLayoutTransition.animateView {
iconBackground.frame = iconBackgroundSize.centered(around: animationFrame.center)
iconBackground.layer.cornerRadius = iconBackgroundSize.width / 2.0
}
} else if let iconBackground = self.iconBackground {
self.iconBackground = nil
iconBackground.layer.animateAlpha(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false, completion: { _ in
iconBackground.removeFromSuperview()
})
iconBackground.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
}
if component.isPinned || (component.isEditing && backgroundColor != nil) {
let pinnedIcon: UIImageView
if let currentIcon = self.pinnedIcon {
pinnedIcon = currentIcon
} else {
pinnedIcon = UIImageView(image: UIImage(bundleImageName: !component.isPinned ? "Peer Info/PinnedLargeIcon" : "Peer Info/PinnedIcon")?.withRenderingMode(.alwaysTemplate))
self.pinnedIcon = pinnedIcon
self.addSubview(pinnedIcon)
if !isFirstTime {
pinnedIcon.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2)
pinnedIcon.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
if component.isPinned {
pinnedIcon.frame = CGRect(origin: CGPoint(x: 3.0, y: 3.0), size: CGSize(width: 24.0, height: 24.0))
pinnedIcon.tintColor = backgroundColor == nil ? component.theme.list.itemSecondaryTextColor : .white
} else {
let iconSize = CGSize(width: 48.0, height: 48.0)
pinnedIcon.frame = iconSize.centered(around: animationFrame.center)
pinnedIcon.tintColor = .white
}
} else if let pinnedIcon = self.pinnedIcon {
self.pinnedIcon = nil
pinnedIcon.layer.animateAlpha(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false, completion: { _ in
pinnedIcon.removeFromSuperview()
})
pinnedIcon.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
}
if component.isHidden && !component.isEditing {
let hiddenIcon: UIImageView
if let currentIcon = self.hiddenIcon {
hiddenIcon = currentIcon
} else {
hiddenIcon = UIImageView(image: generateTintedImage(image: UIImage(bundleImageName: "Peer Info/HiddenIcon"), color: .white))
self.hiddenIcon = hiddenIcon
self.addSubview(hiddenIcon)
if !isFirstTime {
hiddenIcon.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2)
hiddenIcon.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
let iconSize = CGSize(width: 30.0, height: 30.0)
hiddenIcon.frame = iconSize.centered(around: animationFrame.center)
} else if let hiddenIcon = self.hiddenIcon {
self.hiddenIcon = nil
hiddenIcon.layer.animateAlpha(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false, completion: { _ in
hiddenIcon.removeFromSuperview()
})
hiddenIcon.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
}
if let resellPrice = component.resellPrice {
let labelColor = UIColor.white
let attributes = MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.semibold(11.0), textColor: labelColor),
bold: MarkdownAttributeSet(font: Font.semibold(11.0), textColor: labelColor),
link: MarkdownAttributeSet(font: Font.regular(11.0), textColor: labelColor),
linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
}
)
let labelText = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString("#\(resellPrice)", attributes: attributes))
if let range = labelText.string.range(of: "#") {
labelText.addAttribute(NSAttributedString.Key.font, value: Font.semibold(10.0), range: NSRange(range, in: labelText.string))
labelText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: true)), range: NSRange(range, in: labelText.string))
}
let resellSize = self.reselLabel.update(
transition: transition,
component: AnyComponent(
MultilineTextWithEntitiesComponent(
context: component.context,
animationCache: component.context.animationCache,
animationRenderer: component.context.animationRenderer,
placeholderColor: .white,
text: .plain(labelText),
horizontalAlignment: .center
)
),
environment: {},
containerSize: availableSize
)
let resellFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - resellSize.width) / 2.0), y: size.height - 20.0), size: resellSize)
let resellBackground: BlurredBackgroundView
var resellBackgroundTransition = transition
if let currentBackground = self.resellBackground {
resellBackground = currentBackground
} else {
resellBackgroundTransition = .immediate
resellBackground = BlurredBackgroundView(color: UIColor(rgb: 0x000000, alpha: 0.3), enableBlur: true) //UIVisualEffectView(effect: blurEffect)
resellBackground.clipsToBounds = true
self.resellBackground = resellBackground
self.addSubview(resellBackground)
}
let resellBackgroundFrame = resellFrame.insetBy(dx: -6.0, dy: -4.0)
resellBackgroundTransition.setFrame(view: resellBackground, frame: resellBackgroundFrame)
resellBackground.update(size: resellBackgroundFrame.size, cornerRadius: resellBackgroundFrame.size.height / 2.0, transition: resellBackgroundTransition.containedViewLayoutTransition)
if let resellLabelView = self.reselLabel.view {
if resellLabelView.superview == nil {
self.addSubview(resellLabelView)
}
transition.setFrame(view: resellLabelView, frame: resellFrame)
}
} else {
self.reselLabel.view?.removeFromSuperview()
if let resellBackground = self.resellBackground {
self.resellBackground = nil
resellBackground.removeFromSuperview()
}
}
if case .grid = component.mode {
let lineWidth: CGFloat = 2.0
let selectionFrame = backgroundFrame.insetBy(dx: 3.0, dy: 3.0)
if component.isSelected {
let selectionLayer: SimpleShapeLayer
if let current = self.selectionLayer {
selectionLayer = current
} else {
selectionLayer = SimpleShapeLayer()
self.selectionLayer = selectionLayer
if self.ribbon.layer.superlayer != nil {
self.layer.insertSublayer(selectionLayer, below: self.ribbon.layer)
} else {
self.layer.addSublayer(selectionLayer)
}
selectionLayer.fillColor = UIColor.clear.cgColor
selectionLayer.strokeColor = UIColor.white.cgColor
selectionLayer.lineWidth = lineWidth
selectionLayer.frame = selectionFrame
selectionLayer.path = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0), cornerWidth: 6.0, cornerHeight: 6.0, transform: nil)
if !transition.animation.isImmediate {
let initialPath = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: 0.0, dy: 0.0), cornerWidth: 6.0, cornerHeight: 6.0, transform: nil)
selectionLayer.animate(from: initialPath, to: selectionLayer.path as AnyObject, keyPath: "path", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2)
selectionLayer.animateShapeLineWidth(from: 0.0, to: lineWidth, duration: 0.2)
}
}
} else if let selectionLayer = self.selectionLayer {
self.selectionLayer = nil
let targetPath = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: 0.0, dy: 0.0), cornerWidth: 6.0, cornerHeight: 6.0, transform: nil)
selectionLayer.animate(from: selectionLayer.path, to: targetPath, keyPath: "path", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2, removeOnCompletion: false)
selectionLayer.animateShapeLineWidth(from: selectionLayer.lineWidth, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
selectionLayer.removeFromSuperlayer()
})
}
}
if let _ = component.action {
self.addSubview(self.containerButton)
self.containerButton.isUserInteractionEnabled = true
} else {
self.containerButton.isUserInteractionEnabled = false
}
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 tinted: Bool
let starsColor: UIColor?
public init(
context: AccountContext,
text: String,
color: UIColor,
tinted: Bool = false,
starsColor: UIColor? = nil
) {
self.context = context
self.text = text
self.color = color
self.tinted = tinted
self.starsColor = starsColor
}
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.tinted != rhs.tinted {
return false
}
if lhs.starsColor != rhs.starsColor {
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: component.tinted)), 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 let starsColor = component.starsColor {
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(color: starsColor, 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(color: UIColor) {
let emitter = CAEmitterCell()
emitter.name = "emitter"
emitter.contents = UIImage(bundleImageName: "Premium/Stars/Particle")?.cgImage
emitter.birthRate = 14.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(color: UIColor, size: CGSize) {
if self.emitterLayer.emitterCells == nil {
self.setup(color: color)
}
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)
}
}
private var ribbonOutlineImage: UIImage? = {
if let image = UIImage(bundleImageName: "Premium/GiftRibbon") {
return generateScaledImage(image: image, size: CGSize(width: image.size.width + 8.0, height: image.size.height + 8.0), opaque: false)?.withRenderingMode(.alwaysTemplate)
} else {
return UIImage()
}
}()