import Foundation import UIKit import Display import ComponentFlow import SwiftSignalKit import TelegramCore import Markdown import TextFormat import TelegramPresentationData import ViewControllerComponent import SheetComponent import BundleIconComponent import BalancedTextComponent import MultilineTextComponent import SolidRoundedButtonComponent import LottieComponent import AccountContext import TelegramStringFormatting import PremiumPeerShortcutComponent enum MonetizationTransaction: Equatable { case incoming(amount: Int64, fromTimestamp: Int32, toTimestamp: Int32) case outgoing(amount: Int64, timestamp: Int32, address: String, explorerUrl: String) var amount: Int64 { switch self { case let .incoming(amount, _, _), let .outgoing(amount, _, _, _): return amount } } } private final class SheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext let peer: EnginePeer let transaction: MonetizationTransaction let openExplorer: (String) -> Void let dismiss: () -> Void init( context: AccountContext, peer: EnginePeer, transaction: MonetizationTransaction, openExplorer: @escaping (String) -> Void, dismiss: @escaping () -> Void ) { self.context = context self.peer = peer self.transaction = transaction self.openExplorer = openExplorer self.dismiss = dismiss } static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool { if lhs.context !== rhs.context { return false } if lhs.peer != rhs.peer { return false } if lhs.transaction != rhs.transaction { return false } return true } final class State: ComponentState { var cachedCloseImage: (UIImage, PresentationTheme)? let playOnce = ActionSlot() private var didPlayAnimation = false func playAnimationIfNeeded() { guard !self.didPlayAnimation else { return } self.didPlayAnimation = true self.playOnce.invoke(Void()) } } func makeState() -> State { return State() } static var body: Body { let closeButton = Child(Button.self) let amount = Child(MultilineTextComponent.self) let title = Child(MultilineTextComponent.self) let date = Child(MultilineTextComponent.self) let address = Child(MultilineTextComponent.self) let peerShortcut = Child(PremiumPeerShortcutComponent.self) let actionButton = Child(SolidRoundedButtonComponent.self) return { context in let environment = context.environment[EnvironmentType.self] let component = context.component let state = context.state let theme = environment.theme let strings = environment.strings let dateTimeFormat = component.context.sharedContext.currentPresentationData.with { $0 }.dateTimeFormat let sideInset: CGFloat = 16.0 + environment.safeInsets.left let textSideInset: CGFloat = 32.0 + environment.safeInsets.left let titleFont = Font.semibold(17.0) let textFont = Font.regular(17.0) let fixedFont = Font.monospace(17.0) let textColor = theme.actionSheet.primaryTextColor let secondaryTextColor = theme.actionSheet.secondaryTextColor var contentSize = CGSize(width: context.availableSize.width, height: 45.0) let closeImage: UIImage if let (image, theme) = state.cachedCloseImage, theme === environment.theme { closeImage = image } else { closeImage = generateCloseButtonImage(backgroundColor: UIColor(rgb: 0x808084, alpha: 0.1), foregroundColor: theme.actionSheet.inputClearButtonColor)! state.cachedCloseImage = (closeImage, theme) } let closeButton = closeButton.update( component: Button( content: AnyComponent(Image(image: closeImage)), action: { [weak component] in component?.dismiss() } ), availableSize: CGSize(width: 30.0, height: 30.0), transition: .immediate ) context.add(closeButton .position(CGPoint(x: context.availableSize.width - environment.safeInsets.left - closeButton.size.width, y: 28.0)) ) let amountString: NSMutableAttributedString let dateString: String let titleString: String let subtitleString: String let buttonTitle: String let explorerUrl: String? let integralFont = Font.with(size: 48.0, design: .round, weight: .semibold) let fractionalFont = Font.with(size: 24.0, design: .round, weight: .semibold) //TODO:localize switch component.transaction { case let .incoming(amount, fromTimestamp, toTimestamp): amountString = amountAttributedString(formatBalanceText(amount, decimalSeparator: dateTimeFormat.decimalSeparator, showPlus: true), integralFont: integralFont, fractionalFont: fractionalFont, color: theme.list.itemDisclosureActions.constructive.fillColor).mutableCopy() as! NSMutableAttributedString amountString.append(NSAttributedString(string: " TON", font: fractionalFont, textColor: theme.list.itemDisclosureActions.constructive.fillColor)) dateString = "\(stringForFullDate(timestamp: fromTimestamp, strings: strings, dateTimeFormat: dateTimeFormat)) – \(stringForFullDate(timestamp: toTimestamp, strings: strings, dateTimeFormat: dateTimeFormat))" titleString = "Proceeds from Ads displayed in" subtitleString = "" buttonTitle = strings.Common_OK explorerUrl = nil case let .outgoing(amount, timestamp, address, explorerUrlValue): amountString = amountAttributedString(formatBalanceText(amount, decimalSeparator: dateTimeFormat.decimalSeparator), integralFont: integralFont, fractionalFont: fractionalFont, color: theme.list.itemDestructiveColor).mutableCopy() as! NSMutableAttributedString amountString.append(NSAttributedString(string: " TON", font: fractionalFont, textColor: theme.list.itemDestructiveColor)) dateString = stringForFullDate(timestamp: timestamp, strings: strings, dateTimeFormat: dateTimeFormat) titleString = "Balance Withdrawal to" subtitleString = formatAddress(address) buttonTitle = "View in Blockchain Explorer" explorerUrl = explorerUrlValue } let amount = amount.update( component: MultilineTextComponent( text: .plain(amountString), horizontalAlignment: .center, maximumNumberOfLines: 1, lineSpacing: 0.1 ), availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), transition: .immediate ) context.add(amount .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + amount.size.height / 2.0)) ) contentSize.height += amount.size.height contentSize.height += -5.0 let date = date.update( component: MultilineTextComponent( text: .plain(NSAttributedString(string: dateString, font: textFont, textColor: secondaryTextColor)), horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.1 ), availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), transition: .immediate ) context.add(date .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + date.size.height / 2.0)) ) contentSize.height += date.size.height contentSize.height += 32.0 let title = title.update( component: MultilineTextComponent( text: .plain(NSAttributedString(string: titleString, font: titleFont, textColor: textColor)), horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.1 ), availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), transition: .immediate ) context.add(title .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height / 2.0)) ) contentSize.height += title.size.height contentSize.height += 3.0 if !subtitleString.isEmpty { let address = address.update( component: MultilineTextComponent( text: .plain(NSAttributedString(string: subtitleString, font: fixedFont, textColor: textColor)), horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.1 ), availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), transition: .immediate ) context.add(address .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + address.size.height / 2.0)) ) contentSize.height += address.size.height contentSize.height += 50.0 } else { contentSize.height += 5.0 let peerShortcut = peerShortcut.update( component: PremiumPeerShortcutComponent( context: component.context, theme: theme, peer: component.peer ), availableSize: CGSize(width: context.availableSize.width - 32.0, height: context.availableSize.height), transition: .immediate ) context.add(peerShortcut .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + peerShortcut.size.height / 2.0)) ) contentSize.height += peerShortcut.size.height contentSize.height += 50.0 } let actionButton = actionButton.update( component: SolidRoundedButtonComponent( title: buttonTitle, theme: SolidRoundedButtonComponent.Theme( backgroundColor: theme.list.itemCheckColors.fillColor, backgroundColors: [], foregroundColor: theme.list.itemCheckColors.foregroundColor ), font: .bold, fontSize: 17.0, height: 50.0, cornerRadius: 10.0, gloss: false, iconName: nil, animationName: nil, iconPosition: .left, action: { component.dismiss() if let explorerUrl { component.openExplorer(explorerUrl) } } ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), transition: context.transition ) context.add(actionButton .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + actionButton.size.height / 2.0)) ) contentSize.height += actionButton.size.height contentSize.height += 22.0 contentSize.height += environment.safeInsets.bottom state.playAnimationIfNeeded() return contentSize } } } private final class SheetContainerComponent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext let peer: EnginePeer let transaction: MonetizationTransaction let openExplorer: (String) -> Void init( context: AccountContext, peer: EnginePeer, transaction: MonetizationTransaction, openExplorer: @escaping (String) -> Void ) { self.context = context self.peer = peer self.transaction = transaction self.openExplorer = openExplorer } static func ==(lhs: SheetContainerComponent, rhs: SheetContainerComponent) -> Bool { if lhs.context !== rhs.context { return false } if lhs.peer != rhs.peer { return false } if lhs.transaction != rhs.transaction { return false } return true } static var body: Body { let sheet = Child(SheetComponent.self) let animateOut = StoredActionSlot(Action.self) let sheetExternalState = SheetComponent.ExternalState() return { context in let environment = context.environment[EnvironmentType.self] let controller = environment.controller let sheet = sheet.update( component: SheetComponent( content: AnyComponent(SheetContent( context: context.component.context, peer: context.component.peer, transaction: context.component.transaction, openExplorer: context.component.openExplorer, dismiss: { animateOut.invoke(Action { _ in if let controller = controller() { controller.dismiss(completion: nil) } }) } )), backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), followContentSizeChanges: true, externalState: sheetExternalState, animateOut: animateOut ), environment: { environment SheetComponentEnvironment( isDisplaying: environment.value.isVisible, isCentered: environment.metrics.widthClass == .regular, hasInputHeight: !environment.inputHeight.isZero, regularMetricsSize: CGSize(width: 430.0, height: 900.0), dismiss: { animated in if animated { animateOut.invoke(Action { _ in if let controller = controller() { controller.dismiss(completion: nil) } }) } else { if let controller = controller() { controller.dismiss(completion: nil) } } } ) }, availableSize: context.availableSize, transition: context.transition ) context.add(sheet .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) ) if let controller = controller(), !controller.automaticallyControlPresentationContextLayout { let layout = ContainerViewLayout( size: context.availableSize, metrics: environment.metrics, deviceMetrics: environment.deviceMetrics, intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: max(environment.safeInsets.bottom, sheetExternalState.contentHeight), right: 0.0), safeInsets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: 0.0, right: environment.safeInsets.right), additionalInsets: .zero, statusBarHeight: environment.statusBarHeight, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false ) controller.presentationContext.containerLayoutUpdated(layout, transition: context.transition.containedViewLayoutTransition) } return context.availableSize } } } final class TransactionInfoScreen: ViewControllerComponentContainer { private let context: AccountContext init( context: AccountContext, peer: EnginePeer, transaction: MonetizationTransaction, openExplorer: @escaping (String) -> Void ) { self.context = context super.init( context: context, component: SheetContainerComponent( context: context, peer: peer, transaction: transaction, openExplorer: openExplorer ), navigationBarAppearance: .none, statusBarStyle: .ignore, theme: .default ) self.navigationPresentation = .flatModal } required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() self.view.disablesInteractiveModalDismiss = true } func dismissAnimated() { if let view = self.node.hostView.findTaggedView(tag: SheetComponent.View.Tag()) as? SheetComponent.View { view.dismissAnimated() } } } func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? { return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(backgroundColor.cgColor) context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) context.setLineWidth(2.0) context.setLineCap(.round) context.setStrokeColor(foregroundColor.cgColor) context.move(to: CGPoint(x: 10.0, y: 10.0)) context.addLine(to: CGPoint(x: 20.0, y: 20.0)) context.strokePath() context.move(to: CGPoint(x: 20.0, y: 10.0)) context.addLine(to: CGPoint(x: 10.0, y: 20.0)) context.strokePath() }) }