2025-04-17 16:28:59 +04:00

1202 lines
55 KiB
Swift

import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import PresentationDataUtils
import AccountContext
import ComponentFlow
import ViewControllerComponent
import MultilineTextComponent
import BalancedTextComponent
import BundleIconComponent
import Markdown
import TelegramStringFormatting
import PlainButtonComponent
import BlurredBackgroundComponent
import PremiumStarComponent
import TextFormat
import GiftItemComponent
import InAppPurchaseManager
import GiftViewScreen
import UndoUI
import ContextUI
import LottieComponent
final class GiftStoreScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let starsContext: StarsContext
let peerId: EnginePeer.Id
let gift: StarGift.Gift
init(
context: AccountContext,
starsContext: StarsContext,
peerId: EnginePeer.Id,
gift: StarGift.Gift
) {
self.context = context
self.starsContext = starsContext
self.peerId = peerId
self.gift = gift
}
static func ==(lhs: GiftStoreScreenComponent, rhs: GiftStoreScreenComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.peerId != rhs.peerId {
return false
}
if lhs.gift != rhs.gift {
return false
}
return true
}
private final class ScrollView: UIScrollView {
override func touchesShouldCancel(in view: UIView) -> Bool {
return true
}
}
final class View: UIView, UIScrollViewDelegate {
private let topOverscrollLayer = SimpleLayer()
private let scrollView: ScrollView
private let loadingNode: LoadingShimmerNode
private let emptyResultsAnimation = ComponentView<Empty>()
private let emptyResultsTitle = ComponentView<Empty>()
private let emptyResultsAction = ComponentView<Empty>()
private let topPanel = ComponentView<Empty>()
private let topSeparator = ComponentView<Empty>()
private let cancelButton = ComponentView<Empty>()
private let sortButton = ComponentView<Empty>()
private let balanceTitle = ComponentView<Empty>()
private let balanceValue = ComponentView<Empty>()
private let balanceIcon = ComponentView<Empty>()
private let title = ComponentView<Empty>()
private let subtitle = ComponentView<Empty>()
private var starsItems: [AnyHashable: ComponentView<Empty>] = [:]
private let filterSelector = ComponentView<Empty>()
private var isUpdating: Bool = false
private var starsStateDisposable: Disposable?
private var starsState: StarsContext.State?
private var component: GiftStoreScreenComponent?
private(set) weak var state: State?
private var environment: EnvironmentType?
override init(frame: CGRect) {
self.scrollView = ScrollView()
self.scrollView.showsVerticalScrollIndicator = true
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.scrollsToTop = false
self.scrollView.delaysContentTouches = false
self.scrollView.canCancelContentTouches = true
self.scrollView.contentInsetAdjustmentBehavior = .never
if #available(iOS 13.0, *) {
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
}
self.scrollView.alwaysBounceVertical = true
self.loadingNode = LoadingShimmerNode()
super.init(frame: frame)
self.scrollView.delegate = self
self.addSubview(self.scrollView)
self.addSubview(self.loadingNode.view)
self.scrollView.layer.addSublayer(self.topOverscrollLayer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.starsStateDisposable?.dispose()
}
func scrollToTop() {
self.scrollView.setContentOffset(CGPoint(), animated: true)
}
var nextScrollTransition: ComponentTransition?
func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.updateScrolling(interactive: true, transition: self.nextScrollTransition ?? .immediate)
}
private var currentGifts: ([StarGift], Set<String>, Set<String>, Set<String>)?
private var effectiveGifts: [StarGift]? {
if let gifts = self.state?.starGiftsState?.gifts {
return gifts
} else {
return nil
}
}
private func updateScrolling(interactive: Bool = false, transition: ComponentTransition) {
guard let environment = self.environment, let component = self.component, self.state?.starGiftsState?.dataState != .loading else {
return
}
let availableWidth = self.scrollView.bounds.width
let contentOffset = self.scrollView.contentOffset.y
let topPanelAlpha = min(20.0, max(0.0, contentOffset)) / 20.0
if let topPanelView = self.topPanel.view, let topSeparator = self.topSeparator.view {
transition.setAlpha(view: topPanelView, alpha: topPanelAlpha)
transition.setAlpha(view: topSeparator, alpha: topPanelAlpha)
}
let visibleBounds = self.scrollView.bounds.insetBy(dx: 0.0, dy: -10.0)
if let starGifts = self.effectiveGifts {
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
let optionSpacing: CGFloat = 10.0
let optionWidth = (availableWidth - sideInset * 2.0 - optionSpacing * 2.0) / 3.0
let starsOptionSize = CGSize(width: optionWidth, height: 154.0)
var validIds: [AnyHashable] = []
var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: environment.navigationHeight + 39.0 + 9.0), size: starsOptionSize)
let controller = environment.controller
for gift in starGifts {
guard case let .unique(uniqueGift) = gift else {
continue
}
var isVisible = false
if visibleBounds.intersects(itemFrame) {
isVisible = true
}
if isVisible {
let itemId = AnyHashable(gift.id)
validIds.append(itemId)
var itemTransition = transition
let visibleItem: ComponentView<Empty>
if let current = self.starsItems[itemId] {
visibleItem = current
} else {
visibleItem = ComponentView()
if !transition.animation.isImmediate {
itemTransition = .immediate
}
self.starsItems[itemId] = visibleItem
}
var ribbon: GiftItemComponent.Ribbon?
var ribbonColor: GiftItemComponent.Ribbon.Color = .blue
for attribute in uniqueGift.attributes {
if case let .backdrop(_, _, innerColor, outerColor, _, _, _) = attribute {
ribbonColor = .custom(outerColor, innerColor)
break
}
}
ribbon = GiftItemComponent.Ribbon(
text: "#\(uniqueGift.number)",
font: .monospaced,
color: ribbonColor
)
let subject: GiftItemComponent.Subject = .uniqueGift(gift: uniqueGift, price: "⭐️\(uniqueGift.resellStars ?? 0)")
let _ = visibleItem.update(
transition: itemTransition,
component: AnyComponent(
PlainButtonComponent(
content: AnyComponent(
GiftItemComponent(
context: component.context,
theme: environment.theme,
strings: environment.strings,
peer: nil,
subject: subject,
ribbon: ribbon
)
),
effectAlignment: .center,
action: { [weak self] in
if let self, let component = self.component, let state = self.state {
if let controller = controller() as? GiftStoreScreen {
let mainController: ViewController
if let parentController = controller.parentController() {
mainController = parentController
} else {
mainController = controller
}
let giftController = GiftViewScreen(
context: component.context,
subject: .uniqueGift(uniqueGift, state.peerId)
)
mainController.push(giftController)
}
}
},
animateAlpha: false
)
),
environment: {},
containerSize: starsOptionSize
)
if let itemView = visibleItem.view {
if itemView.superview == nil {
self.scrollView.addSubview(itemView)
}
itemTransition.setFrame(view: itemView, frame: itemFrame)
}
}
itemFrame.origin.x += itemFrame.width + optionSpacing
if itemFrame.maxX > availableWidth {
itemFrame.origin.x = sideInset
itemFrame.origin.y += starsOptionSize.height + optionSpacing
}
}
var removeIds: [AnyHashable] = []
for (id, item) in self.starsItems {
if !validIds.contains(id) {
removeIds.append(id)
if let itemView = item.view {
if !transition.animation.isImmediate {
itemView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.25, removeOnCompletion: false)
itemView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
itemView.removeFromSuperview()
})
} else {
itemView.removeFromSuperview()
}
}
}
}
for id in removeIds {
self.starsItems.removeValue(forKey: id)
}
}
let bottomContentOffset = max(0.0, self.scrollView.contentSize.height - self.scrollView.contentOffset.y - self.scrollView.frame.height)
if interactive, bottomContentOffset < 320.0 {
self.state?.starGiftsContext.loadMore()
}
}
func openSortContextMenu(sourceView: UIView) {
guard let component = self.component, let controller = self.environment?.controller() else {
return
}
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: "Sort by Price", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/SortValue"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.default)
self?.state?.starGiftsContext.updateSorting(.value)
})))
items.append(.action(ContextMenuActionItem(text: "Sort by Date", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/SortDate"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.default)
self?.state?.starGiftsContext.updateSorting(.date)
})))
items.append(.action(ContextMenuActionItem(text: "Sort by Number", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/SortNumber"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.default)
self?.state?.starGiftsContext.updateSorting(.number)
})))
let contextController = ContextController(presentationData: presentationData, source: .reference(GiftStoreReferenceContentSource(controller: controller, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil)
controller.presentInGlobalOverlay(contextController)
}
func openModelContextMenu(sourceView: UIView) {
guard let component = self.component, let controller = self.environment?.controller() else {
return
}
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let searchQueryPromise = ValuePromise<String>("")
let attributes = self.state?.starGiftsState?.attributes ?? []
let modelAttributes = attributes.filter { attribute in
if case .model = attribute {
return true
} else {
return false
}
}
let currentFilterAttributes = self.state?.starGiftsState?.filterAttributes ?? []
let selectedModelAttributes = currentFilterAttributes.filter { attribute in
if case .model = attribute {
return true
} else {
return false
}
}
//TODO:localize
var items: [ContextMenuItem] = []
items.append(.custom(SearchContextItem(
context: component.context,
placeholder: "Search",
value: "",
valueChanged: { value in
searchQueryPromise.set(value)
}
), false))
items.append(.separator)
items.append(.custom(GiftAttributeListContextItem(
context: component.context,
attributes: modelAttributes,
selectedAttributes: selectedModelAttributes,
attributeCount: self.state?.starGiftsState?.attributeCount ?? [:],
searchQuery: searchQueryPromise.get(),
attributeSelected: { [weak self] attribute, exclusive in
guard let self else {
return
}
var updatedFilterAttributes: [ResaleGiftsContext.Attribute]
if exclusive {
updatedFilterAttributes = currentFilterAttributes.filter { attribute in
if case .model = attribute {
return false
}
return true
}
updatedFilterAttributes.append(attribute)
} else {
updatedFilterAttributes = currentFilterAttributes
if selectedModelAttributes.contains(attribute) {
updatedFilterAttributes.removeAll(where: { $0 == attribute })
} else {
updatedFilterAttributes.append(attribute)
}
}
self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes)
},
selectAll: { [weak self] in
guard let self else {
return
}
let updatedFilterAttributes = currentFilterAttributes.filter { attribute in
if case .model = attribute {
return false
}
return true
}
self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes)
}
), false))
let contextController = ContextController(
context: component.context,
presentationData: presentationData,
source: .reference(GiftStoreReferenceContentSource(controller: controller, sourceView: sourceView)),
items: .single(ContextController.Items(content: .list(items))),
gesture: nil
)
controller.presentInGlobalOverlay(contextController)
}
func openBackdropContextMenu(sourceView: UIView) {
guard let component = self.component, let controller = self.environment?.controller() else {
return
}
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let searchQueryPromise = ValuePromise<String>("")
let attributes = self.state?.starGiftsState?.attributes ?? []
let backdropAttributes = attributes.filter { attribute in
if case .backdrop = attribute {
return true
} else {
return false
}
}
let currentFilterAttributes = self.state?.starGiftsState?.filterAttributes ?? []
let selectedBackdropAttributes = currentFilterAttributes.filter { attribute in
if case .backdrop = attribute {
return true
} else {
return false
}
}
//TODO:localize
var items: [ContextMenuItem] = []
items.append(.custom(SearchContextItem(
context: component.context,
placeholder: "Search",
value: "",
valueChanged: { value in
searchQueryPromise.set(value)
}
), false))
items.append(.separator)
items.append(.custom(GiftAttributeListContextItem(
context: component.context,
attributes: backdropAttributes,
selectedAttributes: selectedBackdropAttributes,
attributeCount: self.state?.starGiftsState?.attributeCount ?? [:],
searchQuery: searchQueryPromise.get(),
attributeSelected: { [weak self] attribute, exclusive in
guard let self else {
return
}
var updatedFilterAttributes: [ResaleGiftsContext.Attribute]
if exclusive {
updatedFilterAttributes = currentFilterAttributes.filter { attribute in
if case .backdrop = attribute {
return false
}
return true
}
updatedFilterAttributes.append(attribute)
} else {
updatedFilterAttributes = currentFilterAttributes
if selectedBackdropAttributes.contains(attribute) {
updatedFilterAttributes.removeAll(where: { $0 == attribute })
} else {
updatedFilterAttributes.append(attribute)
}
}
self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes)
},
selectAll: { [weak self] in
guard let self else {
return
}
let updatedFilterAttributes = currentFilterAttributes.filter { attribute in
if case .backdrop = attribute {
return false
}
return true
}
self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes)
}
), false))
let contextController = ContextController(
context: component.context,
presentationData: presentationData,
source: .reference(GiftStoreReferenceContentSource(controller: controller, sourceView: sourceView)),
items: .single(ContextController.Items(content: .list(items))),
gesture: nil
)
controller.presentInGlobalOverlay(contextController)
}
func openSymbolContextMenu(sourceView: UIView) {
guard let component = self.component, let controller = self.environment?.controller() else {
return
}
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let searchQueryPromise = ValuePromise<String>("")
let attributes = self.state?.starGiftsState?.attributes ?? []
let patternAttributes = attributes.filter { attribute in
if case .pattern = attribute {
return true
} else {
return false
}
}
let currentFilterAttributes = self.state?.starGiftsState?.filterAttributes ?? []
let selectedPatternAttributes = currentFilterAttributes.filter { attribute in
if case .pattern = attribute {
return true
} else {
return false
}
}
//TODO:localize
var items: [ContextMenuItem] = []
items.append(.custom(SearchContextItem(
context: component.context,
placeholder: "Search",
value: "",
valueChanged: { value in
searchQueryPromise.set(value)
}
), false))
items.append(.separator)
items.append(.custom(GiftAttributeListContextItem(
context: component.context,
attributes: patternAttributes,
selectedAttributes: selectedPatternAttributes,
attributeCount: self.state?.starGiftsState?.attributeCount ?? [:],
searchQuery: searchQueryPromise.get(),
attributeSelected: { [weak self] attribute, exclusive in
guard let self else {
return
}
var updatedFilterAttributes: [ResaleGiftsContext.Attribute]
if exclusive {
updatedFilterAttributes = currentFilterAttributes.filter { attribute in
if case .pattern = attribute {
return false
}
return true
}
updatedFilterAttributes.append(attribute)
} else {
updatedFilterAttributes = currentFilterAttributes
if selectedPatternAttributes.contains(attribute) {
updatedFilterAttributes.removeAll(where: { $0 == attribute })
} else {
updatedFilterAttributes.append(attribute)
}
}
self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes)
},
selectAll: { [weak self] in
guard let self else {
return
}
let updatedFilterAttributes = currentFilterAttributes.filter { attribute in
if case .pattern = attribute {
return false
}
return true
}
self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes)
}
), false))
let contextController = ContextController(
context: component.context,
presentationData: presentationData,
source: .reference(GiftStoreReferenceContentSource(controller: controller, sourceView: sourceView)),
items: .single(ContextController.Items(content: .list(items))),
gesture: nil
)
controller.presentInGlobalOverlay(contextController)
}
func update(component: GiftStoreScreenComponent, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
let environment = environment[EnvironmentType.self].value
let themeUpdated = self.environment?.theme !== environment.theme
self.environment = environment
self.state = state
if self.component == nil {
self.starsStateDisposable = (component.starsContext.state
|> deliverOnMainQueue).start(next: { [weak self] state in
guard let self else {
return
}
self.starsState = state
if !self.isUpdating {
self.state?.updated()
}
})
}
self.component = component
let theme = environment.theme
let strings = environment.strings
if themeUpdated {
self.backgroundColor = environment.theme.list.blocksBackgroundColor
}
let bottomContentInset: CGFloat = 24.0
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
let headerSideInset: CGFloat = 24.0 + environment.safeInsets.left
var contentHeight: CGFloat = 0.0
contentHeight += environment.navigationHeight
let topPanelHeight = environment.navigationHeight + 39.0
let topPanelSize = self.topPanel.update(
transition: transition,
component: AnyComponent(BlurredBackgroundComponent(
color: theme.rootController.navigationBar.blurredBackgroundColor
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: topPanelHeight)
)
let topSeparatorSize = self.topSeparator.update(
transition: transition,
component: AnyComponent(Rectangle(
color: theme.rootController.navigationBar.separatorColor
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: UIScreenPixel)
)
let topPanelFrame = CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: topPanelSize.height))
let topSeparatorFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelSize.height), size: CGSize(width: topSeparatorSize.width, height: topSeparatorSize.height))
if let topPanelView = self.topPanel.view, let topSeparatorView = self.topSeparator.view {
if topPanelView.superview == nil {
topPanelView.alpha = 0.0
topSeparatorView.alpha = 0.0
self.addSubview(topPanelView)
self.addSubview(topSeparatorView)
}
transition.setFrame(view: topPanelView, frame: topPanelFrame)
transition.setFrame(view: topSeparatorView, frame: topSeparatorFrame)
}
// let cancelButtonSize = self.cancelButton.update(
// transition: transition,
// component: AnyComponent(
// PlainButtonComponent(
// content: AnyComponent(
// MultilineTextComponent(
// text: .plain(NSAttributedString(string: strings.Common_Cancel, font: Font.regular(17.0), textColor: theme.rootController.navigationBar.accentTextColor)),
// horizontalAlignment: .center
// )
// ),
// effectAlignment: .center,
// action: {
// controller()?.dismiss()
// },
// animateScale: false
// )
// ),
// environment: {},
// containerSize: CGSize(width: availableSize.width, height: 100.0)
// )
// let cancelButtonFrame = CGRect(origin: CGPoint(x: environment.safeInsets.left + 16.0, y: environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0 - cancelButtonSize.height / 2.0), size: cancelButtonSize)
// if let cancelButtonView = self.cancelButton.view {
// if cancelButtonView.superview == nil {
// self.addSubview(cancelButtonView)
// }
// transition.setFrame(view: cancelButtonView, frame: cancelButtonFrame)
// }
let balanceTitleSize = self.balanceTitle.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: strings.Stars_Purchase_Balance,
font: Font.regular(14.0),
textColor: environment.theme.actionSheet.primaryTextColor
)),
maximumNumberOfLines: 1
)),
environment: {},
containerSize: availableSize
)
let formattedBalance = formatStarsAmountText(self.starsState?.balance ?? StarsAmount.zero, dateTimeFormat: environment.dateTimeFormat)
let smallLabelFont = Font.regular(11.0)
let labelFont = Font.semibold(14.0)
let balanceText = tonAmountAttributedString(formattedBalance, integralFont: labelFont, fractionalFont: smallLabelFont, color: environment.theme.actionSheet.primaryTextColor, decimalSeparator: environment.dateTimeFormat.decimalSeparator)
let balanceValueSize = self.balanceValue.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(balanceText),
maximumNumberOfLines: 1
)),
environment: {},
containerSize: availableSize
)
let balanceIconSize = self.balanceIcon.update(
transition: .immediate,
component: AnyComponent(BundleIconComponent(name: "Premium/Stars/StarSmall", tintColor: nil)),
environment: {},
containerSize: availableSize
)
if let balanceTitleView = self.balanceTitle.view, let balanceValueView = self.balanceValue.view, let balanceIconView = self.balanceIcon.view {
if balanceTitleView.superview == nil {
self.addSubview(balanceTitleView)
self.addSubview(balanceValueView)
self.addSubview(balanceIconView)
}
let navigationHeight = environment.navigationHeight - environment.statusBarHeight
let topBalanceOriginY = environment.statusBarHeight + (navigationHeight - balanceTitleSize.height - balanceValueSize.height) / 2.0
balanceTitleView.center = CGPoint(x: availableSize.width - 16.0 - environment.safeInsets.right - balanceTitleSize.width / 2.0, y: topBalanceOriginY + balanceTitleSize.height / 2.0)
balanceTitleView.bounds = CGRect(origin: .zero, size: balanceTitleSize)
balanceValueView.center = CGPoint(x: availableSize.width - 16.0 - environment.safeInsets.right - balanceValueSize.width / 2.0, y: topBalanceOriginY + balanceTitleSize.height + balanceValueSize.height / 2.0)
balanceValueView.bounds = CGRect(origin: .zero, size: balanceValueSize)
balanceIconView.center = CGPoint(x: availableSize.width - 16.0 - environment.safeInsets.right - balanceValueSize.width - balanceIconSize.width / 2.0 - 2.0, y: topBalanceOriginY + balanceTitleSize.height + balanceValueSize.height / 2.0 - UIScreenPixel)
balanceIconView.bounds = CGRect(origin: .zero, size: balanceIconSize)
}
let titleSize = self.title.update(
transition: transition,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.gift.title ?? "Gift", font: Font.semibold(17.0), textColor: theme.rootController.navigationBar.primaryTextColor)),
horizontalAlignment: .center
)),
environment: {},
containerSize: CGSize(width: availableSize.width - headerSideInset * 2.0, height: 100.0)
)
if let titleView = self.title.view {
if titleView.superview == nil {
self.addSubview(titleView)
}
transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) / 2.0), y: 10.0), size: titleSize))
}
let effectiveCount: Int32
if let count = self.effectiveGifts?.count {
effectiveCount = Int32(count)
} else if let resale = component.gift.availability?.resale {
effectiveCount = Int32(resale)
} else {
effectiveCount = 0
}
let subtitleSize = self.subtitle.update(
transition: transition,
component: AnyComponent(BalancedTextComponent(
text: .plain(NSAttributedString(string: "\(effectiveCount) for resale", font: Font.regular(13.0), textColor: theme.rootController.navigationBar.secondaryTextColor)),
horizontalAlignment: .center,
maximumNumberOfLines: 1
)),
environment: {},
containerSize: CGSize(width: availableSize.width - headerSideInset * 2.0, height: 100.0)
)
let subtitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - subtitleSize.width) / 2.0), y: 31.0), size: subtitleSize)
if let subtitleView = self.subtitle.view {
if subtitleView.superview == nil {
self.addSubview(subtitleView)
}
transition.setFrame(view: subtitleView, frame: subtitleFrame)
}
let optionSpacing: CGFloat = 10.0
let optionWidth = (availableSize.width - sideInset * 2.0 - optionSpacing * 2.0) / 3.0
var sortingTitle = "Date"
var sortingIcon: String = "Peer Info/SortDate"
if let sorting = self.state?.starGiftsState?.sorting {
switch sorting {
case .date:
sortingTitle = "Date"
sortingIcon = "Peer Info/SortDate"
case .value:
sortingTitle = "Price"
sortingIcon = "Peer Info/SortValue"
case .number:
sortingTitle = "Number"
sortingIcon = "Peer Info/SortNumber"
}
}
var filterItems: [FilterSelectorComponent.Item] = []
filterItems.append(FilterSelectorComponent.Item(
id: AnyHashable(0),
iconName: sortingIcon,
title: sortingTitle,
action: { [weak self] view in
if let self {
self.openSortContextMenu(sourceView: view)
}
}
))
var modelTitle = "Model"
var backdropTitle = "Backdrop"
var symbolTitle = "Symbol"
if let filterAttributes = self.state?.starGiftsState?.filterAttributes {
var modelCount = 0
var backdropCount = 0
var symbolCount = 0
for attribute in filterAttributes {
switch attribute {
case .model:
modelCount += 1
case .pattern:
symbolCount += 1
case .backdrop:
backdropCount += 1
}
}
if modelCount > 0 {
if modelCount > 1 {
modelTitle = "\(modelCount) Models"
} else {
modelTitle = "1 Model"
}
}
if backdropCount > 0 {
if backdropCount > 1 {
backdropTitle = "\(backdropCount) Backdrops"
} else {
backdropTitle = "1 Backdrop"
}
}
if symbolCount > 0 {
if symbolCount > 1 {
symbolTitle = "\(symbolCount) Symbols"
} else {
symbolTitle = "1 Symbol"
}
}
}
filterItems.append(FilterSelectorComponent.Item(
id: AnyHashable(1),
title: modelTitle,
action: { [weak self] view in
if let self {
self.openModelContextMenu(sourceView: view)
}
}
))
filterItems.append(FilterSelectorComponent.Item(
id: AnyHashable(2),
title: backdropTitle,
action: { [weak self] view in
if let self {
self.openBackdropContextMenu(sourceView: view)
}
}
))
filterItems.append(FilterSelectorComponent.Item(
id: AnyHashable(3),
title: symbolTitle,
action: { [weak self] view in
if let self {
self.openSymbolContextMenu(sourceView: view)
}
}
))
let filterSize = self.filterSelector.update(
transition: transition,
component: AnyComponent(FilterSelectorComponent(
context: component.context,
colors: FilterSelectorComponent.Colors(
foreground: theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.65),
background: theme.list.itemSecondaryTextColor.withMultipliedAlpha(0.15)
),
items: filterItems
)),
environment: {},
containerSize: CGSize(width: availableSize.width - 10.0 * 2.0, height: 50.0)
)
if let filterSelectorView = self.filterSelector.view {
if filterSelectorView.superview == nil {
self.addSubview(filterSelectorView)
}
transition.setFrame(view: filterSelectorView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - filterSize.width) / 2.0), y: 56.0), size: filterSize))
}
if let starGifts = self.state?.starGiftsState?.gifts {
let starsOptionSize = CGSize(width: optionWidth, height: 154.0)
let optionSpacing: CGFloat = 10.0
contentHeight += ceil(CGFloat(starGifts.count) / 3.0) * (starsOptionSize.height + optionSpacing)
contentHeight += -optionSpacing + 66.0
}
contentHeight += bottomContentInset
contentHeight += environment.safeInsets.bottom
let previousBounds = self.scrollView.bounds
let contentSize = CGSize(width: availableSize.width, height: contentHeight)
if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) {
self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize)
}
if self.scrollView.contentSize != contentSize {
if contentSize.height < self.scrollView.contentSize.height, !transition.animation.isImmediate {
self.nextScrollTransition = transition
}
self.scrollView.contentSize = contentSize
self.nextScrollTransition = nil
}
let scrollInsets = UIEdgeInsets(top: topPanelHeight, left: 0.0, bottom: 0.0, right: 0.0)
if self.scrollView.scrollIndicatorInsets != scrollInsets {
self.scrollView.scrollIndicatorInsets = scrollInsets
}
if !previousBounds.isEmpty, !transition.animation.isImmediate {
let bounds = self.scrollView.bounds
if bounds.maxY != previousBounds.maxY {
let offsetY = previousBounds.maxY - bounds.maxY
transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true)
}
}
self.topOverscrollLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -3000.0), size: CGSize(width: availableSize.width, height: 3000.0))
self.updateScrolling(transition: transition)
var isLoading = false
if self.state?.starGiftsState?.gifts == nil || self.state?.starGiftsState?.dataState == .loading {
isLoading = true
}
let loadingTransition: ComponentTransition = .easeInOut(duration: 0.25)
if isLoading {
self.loadingNode.update(size: availableSize, theme: environment.theme, transition: .immediate)
loadingTransition.setAlpha(view: self.loadingNode.view, alpha: 1.0)
} else {
loadingTransition.setAlpha(view: self.loadingNode.view, alpha: 0.0)
}
transition.setFrame(view: self.loadingNode.view, frame: CGRect(origin: CGPoint(x: 0.0, y: environment.navigationHeight + 39.0 + 7.0), size: availableSize))
let fadeTransition = ComponentTransition.easeInOut(duration: 0.25)
if let effectiveGifts = self.effectiveGifts, effectiveGifts.isEmpty && self.state?.starGiftsState?.dataState != .loading {
let sideInset: CGFloat = 44.0
let emptyAnimationHeight = 148.0
let topInset: CGFloat = environment.navigationHeight + 39.0
let bottomInset: CGFloat = environment.safeInsets.bottom
let visibleHeight = availableSize.height
let emptyAnimationSpacing: CGFloat = 20.0
let emptyTextSpacing: CGFloat = 18.0
let emptyResultsTitleSize = self.emptyResultsTitle.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: "No Matching Gifts", font: Font.semibold(17.0), textColor: theme.list.itemPrimaryTextColor)),
horizontalAlignment: .center
)
),
environment: {},
containerSize: availableSize
)
let emptyResultsActionSize = self.emptyResultsAction.update(
transition: .immediate,
component: AnyComponent(
PlainButtonComponent(
content: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: "Clear Filters", font: Font.regular(17.0), textColor: theme.list.itemAccentColor)),
horizontalAlignment: .center,
maximumNumberOfLines: 0
)
),
effectAlignment: .center,
action: { [weak self] in
guard let self else {
return
}
self.state?.starGiftsContext.updateFilterAttributes([])
},
animateScale: false
)
),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: visibleHeight)
)
let emptyResultsAnimationSize = self.emptyResultsAnimation.update(
transition: .immediate,
component: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(name: "ChatListNoResults")
)),
environment: {},
containerSize: CGSize(width: emptyAnimationHeight, height: emptyAnimationHeight)
)
let emptyTotalHeight = emptyAnimationHeight + emptyAnimationSpacing + emptyResultsTitleSize.height + emptyResultsActionSize.height + emptyTextSpacing
let emptyAnimationY = topInset + floorToScreenPixels((visibleHeight - topInset - bottomInset - emptyTotalHeight) / 2.0)
let emptyResultsAnimationFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - emptyResultsAnimationSize.width) / 2.0), y: emptyAnimationY), size: emptyResultsAnimationSize)
let emptyResultsTitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - emptyResultsTitleSize.width) / 2.0), y: emptyResultsAnimationFrame.maxY + emptyAnimationSpacing), size: emptyResultsTitleSize)
let emptyResultsActionFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - emptyResultsActionSize.width) / 2.0), y: emptyResultsTitleFrame.maxY + emptyTextSpacing), size: emptyResultsActionSize)
if let view = self.emptyResultsAnimation.view as? LottieComponent.View {
if view.superview == nil {
view.alpha = 0.0
fadeTransition.setAlpha(view: view, alpha: 1.0)
self.insertSubview(view, belowSubview: self.loadingNode.view)
view.playOnce()
}
view.bounds = CGRect(origin: .zero, size: emptyResultsAnimationFrame.size)
ComponentTransition.immediate.setPosition(view: view, position: emptyResultsAnimationFrame.center)
}
if let view = self.emptyResultsTitle.view {
if view.superview == nil {
view.alpha = 0.0
fadeTransition.setAlpha(view: view, alpha: 1.0)
self.insertSubview(view, belowSubview: self.loadingNode.view)
}
view.bounds = CGRect(origin: .zero, size: emptyResultsTitleFrame.size)
ComponentTransition.immediate.setPosition(view: view, position: emptyResultsTitleFrame.center)
}
if let view = self.emptyResultsAction.view {
if view.superview == nil {
view.alpha = 0.0
fadeTransition.setAlpha(view: view, alpha: 1.0)
self.insertSubview(view, belowSubview: self.loadingNode.view)
}
view.bounds = CGRect(origin: .zero, size: emptyResultsActionFrame.size)
ComponentTransition.immediate.setPosition(view: view, position: emptyResultsActionFrame.center)
view.alpha = self.state?.starGiftsState?.attributes.isEmpty == true ? 0.0 : 1.0
}
} else {
if let view = self.emptyResultsAnimation.view {
fadeTransition.setAlpha(view: view, alpha: 0.0, completion: { _ in
view.removeFromSuperview()
})
}
if let view = self.emptyResultsTitle.view {
fadeTransition.setAlpha(view: view, alpha: 0.0, completion: { _ in
view.removeFromSuperview()
})
}
if let view = self.emptyResultsAction.view {
fadeTransition.setAlpha(view: view, alpha: 0.0, completion: { _ in
view.removeFromSuperview()
})
}
}
return availableSize
}
}
func makeView() -> View {
return View()
}
final class State: ComponentState {
private let context: AccountContext
var peerId: EnginePeer.Id
private var disposable: Disposable?
fileprivate let starGiftsContext: ResaleGiftsContext
fileprivate var starGiftsState: ResaleGiftsContext.State?
init(
context: AccountContext,
peerId: EnginePeer.Id,
giftId: Int64
) {
self.context = context
self.peerId = peerId
self.starGiftsContext = ResaleGiftsContext(account: context.account, giftId: giftId)
super.init()
self.disposable = (self.starGiftsContext.state
|> deliverOnMainQueue).start(next: { [weak self] state in
guard let self else {
return
}
self.starGiftsState = state
self.updated()
})
}
deinit {
self.disposable?.dispose()
}
}
func makeState() -> State {
return State(context: self.context, peerId: self.peerId, giftId: self.gift.id)
}
func update(view: View, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public class GiftStoreScreen: ViewControllerComponentContainer {
private let context: AccountContext
public var parentController: () -> ViewController? = {
return nil
}
public init(
context: AccountContext,
starsContext: StarsContext,
peerId: EnginePeer.Id,
gift: StarGift.Gift
) {
self.context = context
super.init(context: context, component: GiftStoreScreenComponent(
context: context,
starsContext: starsContext,
peerId: peerId,
gift: gift
), navigationBarAppearance: .transparent, theme: .default, updatedPresentationData: nil)
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.context.sharedContext.currentPresentationData.with { $0 }.strings.Common_Back, style: .plain, target: nil, action: nil)
self.scrollToTop = { [weak self] in
guard let self, let componentView = self.node.hostView.componentView as? GiftStoreScreenComponent.View else {
return
}
componentView.scrollToTop()
}
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
}
}
private extension StarGift {
var id: String {
switch self {
case let .generic(gift):
return "\(gift.id)"
case let .unique(gift):
return gift.slug
}
}
}
private final class GiftStoreReferenceContentSource: ContextReferenceContentSource {
private let controller: ViewController
private let sourceView: UIView
let forceDisplayBelowKeyboard = true
init(controller: ViewController, sourceView: UIView) {
self.controller = controller
self.sourceView = sourceView
}
func transitionInfo() -> ContextControllerReferenceViewInfo? {
return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds)
}
}