import Foundation import UIKit import AsyncDisplayKit import Display import ComponentFlow import SwiftSignalKit import Postbox import TelegramCore import TelegramPresentationData import TelegramUIPreferences import AccountContext import AppBundle import AvatarNode import Markdown import GiftItemComponent import ChatMessagePaymentAlertController import ActivityIndicator import TabSelectorComponent import BundleIconComponent import MultilineTextComponent import TelegramStringFormatting import TooltipUI private final class GiftPurchaseAlertContentNode: AlertContentNode { private let context: AccountContext private let strings: PresentationStrings private var presentationTheme: PresentationTheme private let gift: StarGift.UniqueGift private let peer: EnginePeer fileprivate var currency: CurrencyAmount.Currency fileprivate let header = ComponentView() private let title = ComponentView() private let text = ComponentView() private let giftView = ComponentView() private let arrow = ComponentView() private let avatarNode: AvatarNode private let actionNodesSeparator: ASDisplayNode private let actionNodes: [TextAlertContentActionNode] private let actionVerticalSeparators: [ASDisplayNode] private var activityIndicator: ActivityIndicator? private var validLayout: CGSize? var inProgress = false { didSet { if let size = self.validLayout { let _ = self.updateLayout(size: size, transition: .immediate) } } } var updatedCurrency: (CurrencyAmount.Currency) -> Void = { _ in } override var dismissOnOutsideTap: Bool { return self.isUserInteractionEnabled } init( context: AccountContext, theme: AlertControllerTheme, presentationTheme: PresentationTheme, strings: PresentationStrings, gift: StarGift.UniqueGift, peer: EnginePeer, actions: [TextAlertAction] ) { self.context = context self.strings = strings self.presentationTheme = presentationTheme self.gift = gift self.peer = peer self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0)) self.actionNodesSeparator = ASDisplayNode() self.actionNodesSeparator.isLayerBacked = true self.actionNodes = actions.map { action -> TextAlertContentActionNode in return TextAlertContentActionNode(theme: theme, action: action) } var actionVerticalSeparators: [ASDisplayNode] = [] if actions.count > 1 { for _ in 0 ..< actions.count - 1 { let separatorNode = ASDisplayNode() separatorNode.isLayerBacked = true actionVerticalSeparators.append(separatorNode) } } self.actionVerticalSeparators = actionVerticalSeparators self.currency = self.gift.resellForTonOnly ? .ton : .stars super.init() self.addSubnode(self.avatarNode) self.addSubnode(self.actionNodesSeparator) for actionNode in self.actionNodes { self.addSubnode(actionNode) } for separatorNode in self.actionVerticalSeparators { self.addSubnode(separatorNode) } self.updateTheme(theme) self.avatarNode.setPeer(context: context, theme: presentationTheme, peer: peer) } override func updateTheme(_ theme: AlertControllerTheme) { self.actionNodesSeparator.backgroundColor = theme.separatorColor for actionNode in self.actionNodes { actionNode.updateTheme(theme) } for separatorNode in self.actionVerticalSeparators { separatorNode.backgroundColor = theme.separatorColor } if let size = self.validLayout { _ = self.updateLayout(size: size, transition: .immediate) } } func requestUpdate(transition: ContainedViewLayoutTransition) { if let size = self.validLayout { _ = self.updateLayout(size: size, transition: transition) } } override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { var size = size size.width = min(size.width, 270.0) self.validLayout = size let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } var origin: CGPoint = CGPoint(x: 0.0, y: 20.0) var resellPrice: CurrencyAmount? if let actionNode = self.actionNodes.first { switch self.currency { case .stars: if let resellAmount = self.gift.resellAmounts?.first(where: { $0.currency == .stars }) { resellPrice = resellAmount actionNode.action = TextAlertAction(type: .defaultAction, title: self.strings.Gift_Buy_Confirm_BuyFor(Int32(resellAmount.amount.value)), action: actionNode.action.action) } case .ton: if let resellAmount = self.gift.resellAmounts?.first(where: { $0.currency == .ton }) { resellPrice = resellAmount let valueString = formatTonAmountText(resellAmount.amount.value, dateTimeFormat: presentationData.dateTimeFormat) actionNode.action = TextAlertAction(type: .defaultAction, title: self.strings.Gift_Buy_Confirm_BuyForTon(valueString).string, action: actionNode.action.action) } } } if self.gift.resellForTonOnly { let headerSize = self.header.update( transition: .immediate, component: AnyComponent( MultilineTextComponent( text: .plain(NSAttributedString(string: self.strings.Gift_Buy_AcceptsTonOnly, font: Font.regular(13.0), textColor: self.presentationTheme.actionSheet.secondaryTextColor)), horizontalAlignment: .center, maximumNumberOfLines: 2 ) ), environment: {}, containerSize: CGSize(width: size.width - 32.0, height: size.height) ) let headerFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - headerSize.width) / 2.0), y: origin.y), size: headerSize) if let view = self.header.view { if view.superview == nil { self.view.addSubview(view) } view.frame = headerFrame } origin.y += headerSize.height + 17.0 } else { origin.y -= 4.0 let headerSize = self.header.update( transition: ComponentTransition(transition), component: AnyComponent(TabSelectorComponent( colors: TabSelectorComponent.Colors( foreground: self.presentationTheme.list.itemSecondaryTextColor, selection: self.presentationTheme.list.itemSecondaryTextColor.withMultipliedAlpha(0.15), simple: true ), theme: self.presentationTheme, customLayout: TabSelectorComponent.CustomLayout( font: Font.medium(14.0), spacing: 10.0 ), items: [ TabSelectorComponent.Item( id: AnyHashable(0), content: .text(self.strings.Gift_Buy_PayInStars) ), TabSelectorComponent.Item( id: AnyHashable(1), content: .text(self.strings.Gift_Buy_PayInTon) ) ], selectedId: self.currency == .ton ? AnyHashable(1) : AnyHashable(0), setSelectedId: { [weak self] id in guard let self else { return } let currency: CurrencyAmount.Currency if id == AnyHashable(0) { currency = .stars } else { currency = .ton } if self.currency != currency { self.currency = currency self.updatedCurrency(currency) self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring)) } } )), environment: {}, containerSize: CGSize(width: size.width - 16.0 * 2.0, height: 100.0) ) let headerFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - headerSize.width) / 2.0), y: origin.y), size: headerSize) if let view = self.header.view { if view.superview == nil { self.view.addSubview(view) } view.frame = headerFrame } origin.y += headerSize.height + 17.0 } let avatarSize = CGSize(width: 60.0, height: 60.0) self.avatarNode.updateSize(size: avatarSize) let giftFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) - 44.0, y: origin.y), size: avatarSize) let _ = self.giftView.update( transition: .immediate, component: AnyComponent( GiftItemComponent( context: self.context, theme: self.presentationTheme, strings: self.strings, peer: nil, subject: .uniqueGift(gift: self.gift, price: nil), mode: .thumbnail ) ), environment: {}, containerSize: avatarSize ) if let view = self.giftView.view { if view.superview == nil { self.view.addSubview(view) } view.frame = giftFrame } let arrowSize = self.arrow.update( transition: .immediate, component: AnyComponent(BundleIconComponent(name: "Peer Info/AlertArrow", tintColor: self.presentationTheme.actionSheet.secondaryTextColor)), environment: {}, containerSize: size ) let arrowFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - arrowSize.width) / 2.0), y: origin.y + floorToScreenPixels((avatarSize.height - arrowSize.height) / 2.0)), size: arrowSize) if let view = self.arrow.view { if view.superview == nil { self.view.addSubview(view) } view.frame = arrowFrame } let avatarFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) + 44.0, y: origin.y), size: avatarSize) transition.updateFrame(node: self.avatarNode, frame: avatarFrame) origin.y += avatarSize.height + 17.0 let titleSize = self.title.update( transition: .immediate, component: AnyComponent( MultilineTextComponent( text: .plain(NSAttributedString(string: self.strings.Gift_Buy_Confirm_Title, font: Font.semibold(17.0), textColor: self.presentationTheme.actionSheet.primaryTextColor)), horizontalAlignment: .center ) ), environment: { }, containerSize: CGSize(width: size.width - 32.0, height: size.height) ) let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize) if let view = self.title.view { if view.superview == nil { self.view.addSubview(view) } view.frame = titleFrame } origin.y += titleSize.height + 5.0 let giftTitle = "\(self.gift.title) #\(presentationStringsFormattedNumber(self.gift.number, presentationData.dateTimeFormat.groupingSeparator))" let priceString: String if let resellPrice { switch resellPrice.currency { case .stars: priceString = self.strings.Gift_Buy_Confirm_Text_Stars(Int32(resellPrice.amount.value)) case .ton: priceString = "**\(formatTonAmountText(resellPrice.amount.value, dateTimeFormat: presentationData.dateTimeFormat)) TON**" } } else { priceString = "" } let text: String if self.peer.id == self.context.account.peerId { text = self.strings.Gift_Buy_Confirm_Text(giftTitle, priceString).string } else { text = self.strings.Gift_Buy_Confirm_GiftText(giftTitle, priceString, self.peer.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder)).string } let textSize = self.text.update( transition: .immediate, component: AnyComponent( MultilineTextComponent( text: .markdown(text: text, attributes: MarkdownAttributes( body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: self.presentationTheme.actionSheet.primaryTextColor), bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: self.presentationTheme.actionSheet.primaryTextColor), link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: self.presentationTheme.actionSheet.primaryTextColor), linkAttribute: { url in return ("URL", url) } )), horizontalAlignment: .center, maximumNumberOfLines: 0 ) ), environment: { }, containerSize: CGSize(width: size.width - 32.0, height: size.height) ) let textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize) if let view = self.text.view { if view.superview == nil { self.view.addSubview(view) } view.frame = textFrame } origin.y += textSize.height + 10.0 let actionButtonHeight: CGFloat = 44.0 var minActionsWidth: CGFloat = 0.0 let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count)) let actionTitleInsets: CGFloat = 8.0 for actionNode in self.actionNodes { let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight)) minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets) } let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0) let contentWidth = max(size.width, minActionsWidth) let actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) let resultSize = CGSize(width: contentWidth, height: origin.y + actionsHeight - 26.0 + insets.top + insets.bottom) transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) var actionOffset: CGFloat = 0.0 //let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count)) var separatorIndex = -1 var nodeIndex = 0 for actionNode in self.actionNodes { if separatorIndex >= 0 { let separatorNode = self.actionVerticalSeparators[separatorIndex] /*switch effectiveActionLayout { case .horizontal: transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel))) case .vertical:*/ do { transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) } } separatorIndex += 1 let currentActionWidth: CGFloat /*switch effectiveActionLayout { case .horizontal: if nodeIndex == self.actionNodes.count - 1 { currentActionWidth = resultSize.width - actionOffset } else { currentActionWidth = actionWidth } case .vertical:*/ do { currentActionWidth = resultSize.width } let actionNodeFrame: CGRect /*switch effectiveActionLayout { case .horizontal: actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) actionOffset += currentActionWidth case .vertical:*/ do { actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) actionOffset += actionButtonHeight } transition.updateFrame(node: actionNode, frame: actionNodeFrame) nodeIndex += 1 } if self.inProgress { let activityIndicator: ActivityIndicator if let current = self.activityIndicator { activityIndicator = current } else { activityIndicator = ActivityIndicator(type: .custom(self.presentationTheme.list.freeInputField.controlColor, 18.0, 1.5, false)) self.addSubnode(activityIndicator) } if let actionNode = self.actionNodes.first { actionNode.isHidden = true let indicatorSize = CGSize(width: 22.0, height: 22.0) transition.updateFrame(node: activityIndicator, frame: CGRect(origin: CGPoint(x: actionNode.frame.minX + floor((actionNode.frame.width - indicatorSize.width) / 2.0), y: actionNode.frame.minY + floor((actionNode.frame.height - indicatorSize.height) / 2.0)), size: indicatorSize)) } } return resultSize } } public func giftPurchaseAlertController( context: AccountContext, gift: StarGift.UniqueGift, peer: EnginePeer, navigationController: NavigationController?, commit: @escaping (CurrencyAmount.Currency) -> Void, dismissed: @escaping () -> Void ) -> AlertController { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let strings = presentationData.strings var contentNode: GiftPurchaseAlertContentNode? var dismissImpl: ((Bool) -> Void)? var commitImpl: (() -> Void)? let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: "", action: { commitImpl?() dismissImpl?(true) }), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { dismissImpl?(true) })] contentNode = GiftPurchaseAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), presentationTheme: presentationData.theme, strings: strings, gift: gift, peer: peer, actions: actions) let controller = ChatMessagePaymentAlertController( context: context, presentationData: presentationData, contentNode: contentNode!, navigationController: navigationController, chatPeerId: context.account.peerId, showBalance: true, currency: gift.resellForTonOnly ? .ton : .stars, animateBalanceOverlay: false ) controller.dismissed = { _ in dismissed() } dismissImpl = { [weak controller] animated in if animated { controller?.dismissAnimated() } else { controller?.dismiss() } } commitImpl = { [weak contentNode] in contentNode?.inProgress = true commit(contentNode?.currency ?? .stars) } contentNode?.updatedCurrency = { [weak controller] currency in controller?.currency = currency } if !gift.resellForTonOnly { Queue.mainQueue().after(0.3) { if let headerView = contentNode?.header.view { let absoluteFrame = headerView.convert(headerView.bounds, to: nil) let location = CGRect(origin: CGPoint(x: absoluteFrame.minX + floor(absoluteFrame.width * 0.75), y: absoluteFrame.minY - 8.0), size: CGSize()) let tooltipController = TooltipScreen(account: context.account, sharedContext: context.sharedContext, text: .plain(text: presentationData.strings.Gift_Buy_PayInTon_Tooltip), style: .wide, location: .point(location, .bottom), displayDuration: .default, inset: 16.0, shouldDismissOnTouch: { _, _ in return .dismiss(consume: false) }) controller.present(tooltipController, in: .window(.root)) } } } return controller }