import Foundation import UIKit import Display import AsyncDisplayKit import ComponentFlow import SwiftSignalKit import ViewControllerComponent import ComponentDisplayAdapters import TelegramPresentationData import AccountContext import TelegramCore import Postbox import MultilineTextComponent import BalancedTextComponent import Markdown import ListSectionComponent import BundleIconComponent import TextFormat import UndoUI import ListItemComponentAdaptor import StatisticsUI import ItemListUI import StarsWithdrawalScreen final class StarsStatisticsScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext let peerId: EnginePeer.Id let revenueContext: StarsRevenueStatsContext let openTransaction: (StarsContext.State.Transaction) -> Void let withdraw: () -> Void let showTimeoutTooltip: (Int32) -> Void let buyAds: () -> Void init( context: AccountContext, peerId: EnginePeer.Id, revenueContext: StarsRevenueStatsContext, openTransaction: @escaping (StarsContext.State.Transaction) -> Void, withdraw: @escaping () -> Void, showTimeoutTooltip: @escaping (Int32) -> Void, buyAds: @escaping () -> Void ) { self.context = context self.peerId = peerId self.revenueContext = revenueContext self.openTransaction = openTransaction self.withdraw = withdraw self.showTimeoutTooltip = showTimeoutTooltip self.buyAds = buyAds } static func ==(lhs: StarsStatisticsScreenComponent, rhs: StarsStatisticsScreenComponent) -> Bool { if lhs.context !== rhs.context { return false } if lhs.peerId != rhs.peerId { return false } if lhs.revenueContext !== rhs.revenueContext { return false } return true } private final class ScrollViewImpl: UIScrollView { override func touchesShouldCancel(in view: UIView) -> Bool { return true } override var contentOffset: CGPoint { set(value) { var value = value if value.y > self.contentSize.height - self.bounds.height { value.y = max(0.0, self.contentSize.height - self.bounds.height) self.bounces = false } else { self.bounces = true } super.contentOffset = value } get { return super.contentOffset } } func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool { if let _ = otherGestureRecognizer as? UIPanGestureRecognizer { return true } return false } func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return false } override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { if gestureRecognizer is UIPanGestureRecognizer, let gestureRecognizers = gestureRecognizer.view?.gestureRecognizers { for otherGestureRecognizer in gestureRecognizers { if otherGestureRecognizer !== gestureRecognizer, let panGestureRecognizer = otherGestureRecognizer as? UIPanGestureRecognizer, panGestureRecognizer.minimumNumberOfTouches == 2 { return gestureRecognizer.numberOfTouches < 2 } } if let view = gestureRecognizer.view?.hitTest(gestureRecognizer.location(in: gestureRecognizer.view), with: nil) as? UIControl { return !view.isTracking } return true } else { return true } } } class View: UIView, UIScrollViewDelegate { private let scrollView: ScrollViewImpl private var currentSelectedPanelId: AnyHashable? private let navigationBackgroundView: BlurredBackgroundView private let navigationSeparatorLayer: SimpleLayer private let navigationSeparatorLayerContainer: SimpleLayer private let headerView = ComponentView() private let headerOffsetContainer: UIView private let scrollContainerView: UIView private let titleView = ComponentView() private let chartView = ComponentView() private let proceedsView = ComponentView() private let balanceView = ComponentView() private let transactionsHeader = ComponentView() private let transactionsBackground = UIView() private let panelContainer = ComponentView() private var allTransactionsContext: StarsTransactionsContext? private var incomingTransactionsContext: StarsTransactionsContext? private var outgoingTransactionsContext: StarsTransactionsContext? private var component: StarsStatisticsScreenComponent? private weak var state: EmptyComponentState? private var environment: Environment? private var navigationMetrics: (navigationHeight: CGFloat, statusBarHeight: CGFloat)? private var controller: (() -> ViewController?)? private var enableVelocityTracking: Bool = false private var previousVelocityM1: CGFloat = 0.0 private var previousVelocity: CGFloat = 0.0 private var listIsExpanded = false private var ignoreScrolling: Bool = false private var stateDisposable: Disposable? private var starsState: StarsRevenueStats? private var previousBalance: Int64? private var cachedChevronImage: (UIImage, PresentationTheme)? override init(frame: CGRect) { self.headerOffsetContainer = UIView() self.headerOffsetContainer.isUserInteractionEnabled = false self.navigationBackgroundView = BlurredBackgroundView(color: nil, enableBlur: true) self.navigationBackgroundView.alpha = 0.0 self.navigationSeparatorLayer = SimpleLayer() self.navigationSeparatorLayer.opacity = 0.0 self.navigationSeparatorLayerContainer = SimpleLayer() self.navigationSeparatorLayerContainer.opacity = 0.0 self.scrollContainerView = UIView() self.scrollView = ScrollViewImpl() super.init(frame: frame) self.scrollView.delaysContentTouches = true self.scrollView.canCancelContentTouches = true self.scrollView.clipsToBounds = false if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { self.scrollView.contentInsetAdjustmentBehavior = .never } if #available(iOS 13.0, *) { self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false } self.scrollView.showsVerticalScrollIndicator = false self.scrollView.showsHorizontalScrollIndicator = false self.scrollView.alwaysBounceHorizontal = false self.scrollView.scrollsToTop = false self.scrollView.delegate = self self.scrollView.clipsToBounds = true self.addSubview(self.scrollView) self.scrollView.addSubview(self.scrollContainerView) self.scrollContainerView.addSubview(self.transactionsBackground) self.addSubview(self.navigationBackgroundView) self.navigationSeparatorLayerContainer.addSublayer(self.navigationSeparatorLayer) self.layer.addSublayer(self.navigationSeparatorLayerContainer) self.addSubview(self.headerOffsetContainer) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { self.stateDisposable?.dispose() } func scrollToTop() { self.scrollView.setContentOffset(CGPoint(x: 0.0, y: -self.scrollView.contentInset.top), animated: true) } func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { self.enableVelocityTracking = true } func scrollViewDidScroll(_ scrollView: UIScrollView) { if !self.ignoreScrolling { self.updateScrolling(transition: .immediate) if let view = self.chartView.view as? ListItemComponentAdaptor.View, let node = view.itemNode as? StatsGraphItemNode { node.resetInteraction() } if self.enableVelocityTracking { self.previousVelocityM1 = self.previousVelocity if let value = (scrollView.value(forKey: (["_", "verticalVelocity"] as [String]).joined()) as? NSNumber)?.doubleValue { self.previousVelocity = CGFloat(value) } } } } func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { guard let navigationMetrics = self.navigationMetrics else { return } if let panelContainerView = self.panelContainer.view as? StarsTransactionsPanelContainerComponent.View { let paneAreaExpansionFinalPoint: CGFloat = panelContainerView.frame.minY - navigationMetrics.navigationHeight if abs(scrollView.contentOffset.y - paneAreaExpansionFinalPoint) < .ulpOfOne { panelContainerView.transferVelocity(self.previousVelocityM1) } } } func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { guard let _ = self.navigationMetrics else { return } let paneAreaExpansionDistance: CGFloat = 32.0 let paneAreaExpansionFinalPoint: CGFloat = scrollView.contentSize.height - scrollView.bounds.height if targetContentOffset.pointee.y > paneAreaExpansionFinalPoint - paneAreaExpansionDistance && targetContentOffset.pointee.y < paneAreaExpansionFinalPoint { targetContentOffset.pointee.y = paneAreaExpansionFinalPoint self.enableVelocityTracking = false self.previousVelocity = 0.0 self.previousVelocityM1 = 0.0 } } private var lastScrollBounds: CGRect? private var lastBottomOffset: CGFloat? private func updateScrolling(transition: ComponentTransition) { let scrollBounds = self.scrollView.bounds let isLockedAtPanels = scrollBounds.maxY == self.scrollView.contentSize.height if let _ = self.navigationMetrics { let topContentOffset = self.scrollView.contentOffset.y let navigationBackgroundAlpha = min(20.0, max(0.0, topContentOffset)) / 20.0 let animatedTransition = ComponentTransition(animation: .curve(duration: 0.18, curve: .easeInOut)) animatedTransition.setAlpha(view: self.navigationBackgroundView, alpha: navigationBackgroundAlpha) animatedTransition.setAlpha(layer: self.navigationSeparatorLayerContainer, alpha: navigationBackgroundAlpha) let expansionDistance: CGFloat = 32.0 var expansionDistanceFactor: CGFloat = abs(scrollBounds.maxY - self.scrollView.contentSize.height) / expansionDistance expansionDistanceFactor = max(0.0, min(1.0, expansionDistanceFactor)) transition.setAlpha(layer: self.navigationSeparatorLayer, alpha: expansionDistanceFactor) if let panelContainerView = self.panelContainer.view as? StarsTransactionsPanelContainerComponent.View { panelContainerView.updateNavigationMergeFactor(value: 1.0 - expansionDistanceFactor, transition: transition) } let listIsExpanded = expansionDistanceFactor == 0.0 if listIsExpanded != self.listIsExpanded { self.listIsExpanded = listIsExpanded if !self.isUpdating { self.state?.updated(transition: .init(animation: .curve(duration: 0.25, curve: .slide))) } } } let _ = self.panelContainer.updateEnvironment( transition: transition, environment: { StarsTransactionsPanelContainerEnvironment(isScrollable: isLockedAtPanels) } ) // guard let environment = self.environment?[ViewControllerComponentContainer.Environment.self].value else { // return // } // // let scrollBounds = self.scrollView.bounds // // let topContentOffset = self.scrollView.contentOffset.y // let navigationBackgroundAlpha = min(20.0, max(0.0, topContentOffset)) / 20.0 // // let animatedTransition = ComponentTransition(animation: .curve(duration: 0.18, curve: .easeInOut)) // animatedTransition.setAlpha(view: self.navigationBackgroundView, alpha: navigationBackgroundAlpha) // animatedTransition.setAlpha(layer: self.navigationSeparatorLayerContainer, alpha: navigationBackgroundAlpha) // // let expansionDistance: CGFloat = 32.0 // var expansionDistanceFactor: CGFloat = abs(scrollBounds.maxY - self.scrollView.contentSize.height) / expansionDistance // expansionDistanceFactor = max(0.0, min(1.0, expansionDistanceFactor)) // // transition.setAlpha(layer: self.navigationSeparatorLayer, alpha: expansionDistanceFactor) // // let bottomOffset = max(0.0, self.scrollView.contentSize.height - self.scrollView.contentOffset.y - self.scrollView.frame.height) // self.lastBottomOffset = bottomOffset // // let transactionsScrollBounds: CGRect // if let transactionsView = self.transactionsView.view { // transactionsScrollBounds = CGRect(origin: CGPoint(x: 0.0, y: scrollBounds.origin.y - transactionsView.frame.minY), size: scrollBounds.size) // } else { // transactionsScrollBounds = .zero // } // self.lastScrollBounds = transactionsScrollBounds // // let _ = self.transactionsView.updateEnvironment( // transition: transition, // environment: { // StarsTransactionsPanelEnvironment( // theme: environment.theme, // strings: environment.strings, // dateTimeFormat: environment.dateTimeFormat, // containerInsets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: environment.safeInsets.bottom, right: environment.safeInsets.right), // isScrollable: false, // isCurrent: true, // externalScrollBounds: transactionsScrollBounds, // externalBottomOffset: bottomOffset // ) // } // ) } private var isUpdating = false func update(component: StarsStatisticsScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false } self.component = component self.environment = environment self.state = state let environment = environment[ViewControllerComponentContainer.Environment.self].value let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } let strings = environment.strings if self.stateDisposable == nil { self.stateDisposable = (component.revenueContext.state |> deliverOnMainQueue).start(next: { [weak self] state in guard let self else { return } self.starsState = state.stats if !self.isUpdating { self.state?.updated() } }) } self.controller = environment.controller self.navigationMetrics = (environment.navigationHeight, environment.statusBarHeight) self.navigationSeparatorLayer.backgroundColor = environment.theme.rootController.navigationBar.separatorColor.cgColor let navigationFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: environment.navigationHeight)) self.navigationBackgroundView.updateColor(color: environment.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) self.navigationBackgroundView.update(size: navigationFrame.size, transition: transition.containedViewLayoutTransition) transition.setFrame(view: self.navigationBackgroundView, frame: navigationFrame) let navigationSeparatorFrame = CGRect(origin: CGPoint(x: 0.0, y: navigationFrame.maxY), size: CGSize(width: availableSize.width, height: UIScreenPixel)) transition.setFrame(layer: self.navigationSeparatorLayerContainer, frame: navigationSeparatorFrame) transition.setFrame(layer: self.navigationSeparatorLayer, frame: CGRect(origin: CGPoint(), size: navigationSeparatorFrame.size)) self.backgroundColor = environment.theme.list.blocksBackgroundColor var contentHeight: CGFloat = 0.0 let sideInsets: CGFloat = environment.safeInsets.left + environment.safeInsets.right + 16 * 2.0 contentHeight += environment.navigationHeight contentHeight += 31.0 let titleSize = self.titleView.update( transition: .immediate, component: AnyComponent( MultilineTextComponent( text: .plain(NSAttributedString(string: strings.Stars_BotRevenue_Title, font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor)), horizontalAlignment: .center, truncationType: .end, maximumNumberOfLines: 1 ) ), environment: {}, containerSize: availableSize ) if let titleView = self.titleView.view { if titleView.superview == nil { self.addSubview(titleView) } let titlePosition = CGPoint(x: availableSize.width / 2.0, y: environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0) transition.setPosition(view: titleView, position: titlePosition) transition.setBounds(view: titleView, bounds: CGRect(origin: .zero, size: titleSize)) } if let revenueGraph = self.starsState?.revenueGraph { let chartSize = self.chartView.update( transition: .immediate, component: AnyComponent(ListSectionComponent( theme: environment.theme, header: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: strings.Stars_BotRevenue_Revenue_Title.uppercased(), font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor )), maximumNumberOfLines: 0 )), footer: nil, items: [ AnyComponentWithIdentity(id: 0, component: AnyComponent(ListItemComponentAdaptor( itemGenerator: StatsGraphItem(presentationData: ItemListPresentationData(presentationData), graph: revenueGraph, type: .stars, noInitialZoom: true, conversionRate: starsState?.usdRate ?? 0.0, sectionId: 0, style: .blocks), params: ListViewItemLayoutParams(width: availableSize.width - sideInsets, leftInset: 0.0, rightInset: 0.0, availableHeight: 10000.0, isStandalone: true) ))), ], displaySeparators: false )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInsets, height: availableSize.height) ) let chartFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - chartSize.width) / 2.0), y: contentHeight), size: chartSize) if let chartView = self.chartView.view { if chartView.superview == nil { self.scrollView.addSubview(chartView) } transition.setFrame(view: chartView, frame: chartFrame) } contentHeight += chartSize.height contentHeight += 44.0 } let proceedsSize = self.proceedsView.update( transition: .immediate, component: AnyComponent(ListSectionComponent( theme: environment.theme, header: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: strings.Stars_BotRevenue_Proceeds_Title.uppercased(), font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor )), maximumNumberOfLines: 0 )), footer: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: strings.Stars_BotRevenue_Proceeds_Info, font: Font.regular(13.0), textColor: environment.theme.list.freeTextColor )), maximumNumberOfLines: 0 )), items: [ AnyComponentWithIdentity(id: 0, component: AnyComponent(StarsOverviewItemComponent( theme: environment.theme, dateTimeFormat: environment.dateTimeFormat, title: strings.Stars_BotRevenue_Proceeds_Available, value: starsState?.balances.availableBalance ?? 0, rate: starsState?.usdRate ?? 0.0 ))), AnyComponentWithIdentity(id: 1, component: AnyComponent(StarsOverviewItemComponent( theme: environment.theme, dateTimeFormat: environment.dateTimeFormat, title: strings.Stars_BotRevenue_Proceeds_Current, value: starsState?.balances.currentBalance ?? 0, rate: starsState?.usdRate ?? 0.0 ))), AnyComponentWithIdentity(id: 2, component: AnyComponent(StarsOverviewItemComponent( theme: environment.theme, dateTimeFormat: environment.dateTimeFormat, title: strings.Stars_BotRevenue_Proceeds_Total, value: starsState?.balances.overallRevenue ?? 0, rate: starsState?.usdRate ?? 0.0 ))) ], displaySeparators: false )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInsets, height: availableSize.height) ) let proceedsFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - proceedsSize.width) / 2.0), y: contentHeight), size: proceedsSize) if let proceedsView = self.proceedsView.view { if proceedsView.superview == nil { self.scrollView.addSubview(proceedsView) } transition.setFrame(view: proceedsView, frame: proceedsFrame) } contentHeight += proceedsSize.height contentHeight += 31.0 let termsFont = Font.regular(13.0) let termsTextColor = environment.theme.list.freeTextColor let termsMarkdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: termsFont, textColor: termsTextColor), bold: MarkdownAttributeSet(font: termsFont, textColor: termsTextColor), link: MarkdownAttributeSet(font: termsFont, textColor: environment.theme.list.itemAccentColor), linkAttribute: { contents in return (TelegramTextAttributes.URL, contents) }) let balanceInfoString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(strings.Stars_BotRevenue_Withdraw_Info, attributes: termsMarkdownAttributes, textAlignment: .natural )) if self.cachedChevronImage == nil || self.cachedChevronImage?.1 !== environment.theme { self.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Contact List/SubtitleArrow"), color: environment.theme.list.itemAccentColor)!, environment.theme) } if let range = balanceInfoString.string.range(of: ">"), let chevronImage = self.cachedChevronImage?.0 { balanceInfoString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: balanceInfoString.string)) } let balanceSize = self.balanceView.update( transition: .immediate, component: AnyComponent(ListSectionComponent( theme: environment.theme, header: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: strings.Stars_BotRevenue_Withdraw_Balance.uppercased(), font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor )), maximumNumberOfLines: 0 )), footer: AnyComponent(MultilineTextComponent( text: .plain(balanceInfoString), maximumNumberOfLines: 0, highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.1), highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), highlightAction: { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) } else { return nil } }, tapAction: { [weak self] attributes, _ in if let controller = self?.controller?() as? StarsStatisticsScreen, let navigationController = controller.navigationController as? NavigationController { component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: strings.Stars_BotRevenue_Withdraw_Info_URL, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {}) } } )), items: [AnyComponentWithIdentity(id: 0, component: AnyComponent( StarsBalanceComponent( theme: environment.theme, strings: strings, dateTimeFormat: environment.dateTimeFormat, count: self.starsState?.balances.availableBalance ?? 0, rate: self.starsState?.usdRate ?? 0, actionTitle: strings.Stars_BotRevenue_Withdraw_Withdraw, actionAvailable: true, actionIsEnabled: self.starsState?.balances.withdrawEnabled ?? true, actionCooldownUntilTimestamp: self.starsState?.balances.nextWithdrawalTimestamp, action: { [weak self] in guard let self, let component = self.component else { return } var remainingCooldownSeconds: Int32 = 0 if let cooldownUntilTimestamp = self.starsState?.balances.nextWithdrawalTimestamp { remainingCooldownSeconds = cooldownUntilTimestamp - Int32(Date().timeIntervalSince1970) remainingCooldownSeconds = max(0, remainingCooldownSeconds) if remainingCooldownSeconds > 0 { component.showTimeoutTooltip(cooldownUntilTimestamp) } else { component.withdraw() } } else { component.withdraw() } }, buyAds: { [weak self] in guard let self, let component = self.component else { return } component.buyAds() } ) ))] )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInsets, height: availableSize.height) ) let balanceFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - balanceSize.width) / 2.0), y: contentHeight), size: balanceSize) if let balanceView = self.balanceView.view { if balanceView.superview == nil { self.scrollView.addSubview(balanceView) } transition.setFrame(view: balanceView, frame: balanceFrame) } contentHeight += balanceSize.height contentHeight += 44.0 var panelItems: [StarsTransactionsPanelContainerComponent.Item] = [] if "".isEmpty { let allTransactionsContext: StarsTransactionsContext if let current = self.allTransactionsContext { allTransactionsContext = current } else { allTransactionsContext = component.context.engine.payments.peerStarsTransactionsContext(subject: .peer(component.peerId), mode: .all) self.allTransactionsContext = allTransactionsContext } let incomingTransactionsContext: StarsTransactionsContext if let current = self.incomingTransactionsContext { incomingTransactionsContext = current } else { incomingTransactionsContext = component.context.engine.payments.peerStarsTransactionsContext(subject: .peer(component.peerId), mode: .incoming) self.incomingTransactionsContext = incomingTransactionsContext } let outgoingTransactionsContext: StarsTransactionsContext if let current = self.outgoingTransactionsContext { outgoingTransactionsContext = current } else { outgoingTransactionsContext = component.context.engine.payments.peerStarsTransactionsContext(subject: .peer(component.peerId), mode: .outgoing) self.outgoingTransactionsContext = outgoingTransactionsContext } panelItems.append(StarsTransactionsPanelContainerComponent.Item( id: "all", title: environment.strings.Stars_Intro_AllTransactions, panel: AnyComponent(StarsTransactionsListPanelComponent( context: component.context, transactionsContext: allTransactionsContext, isAccount: true, action: { transaction in component.openTransaction(transaction) } )) )) panelItems.append(StarsTransactionsPanelContainerComponent.Item( id: "incoming", title: environment.strings.Stars_Intro_Incoming, panel: AnyComponent(StarsTransactionsListPanelComponent( context: component.context, transactionsContext: incomingTransactionsContext, isAccount: true, action: { transaction in component.openTransaction(transaction) } )) )) panelItems.append(StarsTransactionsPanelContainerComponent.Item( id: "outgoing", title: environment.strings.Stars_Intro_Outgoing, panel: AnyComponent(StarsTransactionsListPanelComponent( context: component.context, transactionsContext: outgoingTransactionsContext, isAccount: true, action: { transaction in component.openTransaction(transaction) } )) )) } var wasLockedAtPanels = false if let panelContainerView = self.panelContainer.view, let navigationMetrics = self.navigationMetrics { if self.scrollView.bounds.minY > 0.0 && abs(self.scrollView.bounds.minY - (panelContainerView.frame.minY - navigationMetrics.navigationHeight)) <= UIScreenPixel { wasLockedAtPanels = true } } let panelTransition = transition if !panelItems.isEmpty { let panelContainerInset: CGFloat = self.listIsExpanded ? 0.0 : 16.0 let panelContainerCornerRadius: CGFloat = self.listIsExpanded ? 0.0 : 11.0 let panelContainerSize = self.panelContainer.update( transition: panelTransition, component: AnyComponent(StarsTransactionsPanelContainerComponent( theme: environment.theme, strings: environment.strings, dateTimeFormat: environment.dateTimeFormat, insets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left + panelContainerInset, bottom: environment.safeInsets.bottom, right: environment.safeInsets.right + panelContainerInset), items: panelItems, currentPanelUpdated: { [weak self] id, transition in guard let self else { return } self.currentSelectedPanelId = id self.state?.updated(transition: transition) } )), environment: { StarsTransactionsPanelContainerEnvironment(isScrollable: wasLockedAtPanels) }, containerSize: CGSize(width: availableSize.width, height: availableSize.height - environment.navigationHeight) ) if let panelContainerView = self.panelContainer.view { if panelContainerView.superview == nil { self.scrollContainerView.addSubview(panelContainerView) } transition.setFrame(view: panelContainerView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - panelContainerSize.width) / 2.0), y: contentHeight), size: panelContainerSize)) transition.setCornerRadius(layer: panelContainerView.layer, cornerRadius: panelContainerCornerRadius) } contentHeight += panelContainerSize.height } else { self.panelContainer.view?.removeFromSuperview() } self.ignoreScrolling = true transition.setPosition(view: self.scrollView, position: CGRect(origin: CGPoint(), size: availableSize).center) let contentSize = CGSize(width: availableSize.width, height: contentHeight) if self.scrollView.contentSize != contentSize { self.scrollView.contentSize = contentSize } transition.setFrame(view: self.scrollContainerView, frame: CGRect(origin: CGPoint(), size: contentSize)) var scrollViewBounds = self.scrollView.bounds scrollViewBounds.size = availableSize transition.setBounds(view: self.scrollView, bounds: scrollViewBounds) self.ignoreScrolling = false self.updateScrolling(transition: transition) return availableSize } } func makeView() -> View { return View(frame: CGRect()) } func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } public final class StarsStatisticsScreen: ViewControllerComponentContainer { private let context: AccountContext private let peerId: EnginePeer.Id private let revenueContext: StarsRevenueStatsContext private weak var tooltipScreen: UndoOverlayController? private var timer: Foundation.Timer? public init(context: AccountContext, peerId: EnginePeer.Id, revenueContext: StarsRevenueStatsContext) { self.context = context self.peerId = peerId self.revenueContext = revenueContext var withdrawImpl: (() -> Void)? var buyAdsImpl: (() -> Void)? var showTimeoutTooltipImpl: ((Int32) -> Void)? var openTransactionImpl: ((StarsContext.State.Transaction) -> Void)? super.init(context: context, component: StarsStatisticsScreenComponent( context: context, peerId: peerId, revenueContext: revenueContext, openTransaction: { transaction in openTransactionImpl?(transaction) }, withdraw: { withdrawImpl?() }, showTimeoutTooltip: { timestamp in showTimeoutTooltipImpl?(timestamp) }, buyAds: { buyAdsImpl?() } ), navigationBarAppearance: .transparent) self.navigationPresentation = .modalInLargeLayout openTransactionImpl = { [weak self] transaction in guard let self else { return } let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) |> deliverOnMainQueue).start(next: { [weak self] peer in guard let self, let peer else { return } let controller = context.sharedContext.makeStarsTransactionScreen(context: context, transaction: transaction, peer: peer) self.push(controller) }) } withdrawImpl = { [weak self] in guard let self else { return } let _ = (context.engine.peers.checkStarsRevenueWithdrawalAvailability() |> deliverOnMainQueue).start(error: { [weak self] error in guard let self else { return } switch error { case .serverProvided: return case .requestPassword: let _ = (revenueContext.state |> take(1) |> deliverOnMainQueue).startStandalone(next: { [weak self] state in guard let self, let stats = state.stats else { return } let controller = self.context.sharedContext.makeStarsWithdrawalScreen(context: context, stats: stats, completion: { [weak self] amount in guard let self else { return } let controller = confirmStarsRevenueWithdrawalController(context: context, peerId: peerId, amount: amount, present: { [weak self] c, a in self?.present(c, in: .window(.root)) }, completion: { url in let presentationData = context.sharedContext.currentPresentationData.with { $0 } context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {}) Queue.mainQueue().after(2.0) { revenueContext.reload() //TODO: //self?.transactionsContext.reload() } }) self.present(controller, in: .window(.root)) }) self.push(controller) }) default: let controller = starsRevenueWithdrawalController(context: context, peerId: peerId, amount: 0, initialError: error, present: { [weak self] c, a in self?.present(c, in: .window(.root)) }, completion: { _ in }) self.present(controller, in: .window(.root)) } }) } showTimeoutTooltipImpl = { [weak self] cooldownUntilTimestamp in guard let self, self.tooltipScreen == nil else { return } let remainingCooldownSeconds = cooldownUntilTimestamp - Int32(Date().timeIntervalSince1970) let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } let content: UndoOverlayContent = .universal( animation: "anim_clock", scale: 0.058, colors: [:], title: nil, text: presentationData.strings.Stars_Withdraw_Withdraw_ErrorTimeout(stringForRemainingTime(remainingCooldownSeconds)).string, customUndoText: nil, timeout: nil ) let controller = UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { _ in return true }) self.tooltipScreen = controller self.present(controller, in: .window(.root)) if remainingCooldownSeconds < 3600 { if self.timer == nil { self.timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { [weak self] _ in guard let self else { return } if let tooltipScreen = self.tooltipScreen { let remainingCooldownSeconds = cooldownUntilTimestamp - Int32(Date().timeIntervalSince1970) let content: UndoOverlayContent = .universal( animation: "anim_clock", scale: 0.058, colors: [:], title: nil, text: presentationData.strings.Stars_Withdraw_Withdraw_ErrorTimeout(stringForRemainingTime(remainingCooldownSeconds)).string, customUndoText: nil, timeout: nil ) tooltipScreen.content = content } else { if let timer = self.timer { self.timer = nil timer.invalidate() } } }) } } } buyAdsImpl = { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let _ = (context.engine.peers.requestStarsRevenueAdsAccountlUrl(peerId: peerId) |> deliverOnMainQueue).startStandalone(next: { url in guard let url else { return } context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {}) }) } self.scrollToTop = { [weak self] in guard let self, let componentView = self.node.hostView.componentView as? StarsStatisticsScreenComponent.View else { return } componentView.scrollToTop() } } required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override public func viewDidLoad() { super.viewDidLoad() } }