import Foundation import UIKit import Display import AsyncDisplayKit import SwiftSignalKit import Postbox import TelegramCore import TelegramPresentationData import TelegramUIPreferences import TelegramStringFormatting import PresentationDataUtils import AccountContext import ComponentFlow import ViewControllerComponent import MultilineTextComponent import MultilineTextWithEntitiesComponent import BalancedTextComponent import ListSectionComponent import ListActionItemComponent import ListItemComponentAdaptor import BundleIconComponent import LottieComponent import TextFieldComponent import ButtonComponent import BotPaymentsUI import ChatEntityKeyboardInputNode import EmojiSuggestionsComponent import ChatPresentationInterfaceState import AudioToolbox import TextFormat import InAppPurchaseManager import BlurredBackgroundComponent import ProgressNavigationButtonNode import Markdown import GiftViewScreen import UndoUI import ConfettiEffect import EdgeEffect import AnimatedTextComponent import GlassBarButtonComponent import MessageInputPanelComponent private final class GiftSetupScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext let peerId: EnginePeer.Id let subject: GiftSetupScreen.Subject let completion: (() -> Void)? init( context: AccountContext, peerId: EnginePeer.Id, subject: GiftSetupScreen.Subject, completion: (() -> Void)? = nil ) { self.context = context self.peerId = peerId self.subject = subject self.completion = completion } static func ==(lhs: GiftSetupScreenComponent, rhs: GiftSetupScreenComponent) -> Bool { return true } private struct ItemLayout: Equatable { var containerSize: CGSize var containerInset: CGFloat var containerCornerRadius: CGFloat var bottomInset: CGFloat var topInset: CGFloat init(containerSize: CGSize, containerInset: CGFloat, containerCornerRadius: CGFloat, bottomInset: CGFloat, topInset: CGFloat) { self.containerSize = containerSize self.containerInset = containerInset self.containerCornerRadius = containerCornerRadius self.bottomInset = bottomInset self.topInset = topInset } } private final class ScrollView: UIScrollView { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { return super.hitTest(point, with: event) } } final class View: UIView, UIScrollViewDelegate { private let dimView: UIView private let containerView: UIView private let backgroundLayer: SimpleLayer private let navigationBarContainer: SparseContainerView private let scrollView: ScrollView private let scrollContentClippingView: SparseContainerView private let scrollContentView: UIView private let bottomEdgeEffectView: EdgeEffectView private let backgroundHandleView: UIImageView private let closeButton = ComponentView() private let remainingCount = ComponentView() private let auctionFooter = ComponentView() private let resaleSection = ComponentView() private let introContent = ComponentView() private let introSection = ComponentView() private let starsSection = ComponentView() private let upgradeSection = ComponentView() private let hideSection = ComponentView() private let inputPanel = ComponentView() private let inputPanelExternalState = MessageInputPanelComponent.ExternalState() private let actionButton = ComponentView() private var ignoreScrolling: Bool = false private var component: GiftSetupScreenComponent? private weak var state: EmptyComponentState? private var isUpdating: Bool = false private var environment: ViewControllerComponentContainer.Environment? private var itemLayout: ItemLayout? private var currentInputMode: MessageInputPanelComponent.InputMode = .text private var inputMediaNodeData: ChatEntityKeyboardInputNode.InputData? private var inputMediaNodeDataDisposable: Disposable? private var inputMediaNodeStateContext = ChatEntityKeyboardInputNode.StateContext() private var inputMediaInteraction: ChatEntityKeyboardInputNode.Interaction? private var inputMediaNode: ChatEntityKeyboardInputNode? private var inputMediaNodeBackground = SimpleLayer() private var inputMediaNodeTargetTag: AnyObject? private let inputMediaNodeDataPromise = Promise() private var previousInputHeight: CGFloat? private var currentEmojiSuggestionView: ComponentHostView? private var hideName = false private var includeUpgrade = false private var payWithStars = false private var inProgress = false private var peerMap: [EnginePeer.Id: EnginePeer] = [:] private var sendPaidMessageStars: StarsAmount? private var giftAuction: GiftAuctionContext? private var giftAuctionState: GiftAuctionContext.State? private var giftAuctionDisposable: Disposable? private var giftAuctionTimer: SwiftSignalKit.Timer? private var cachedStarImage: (UIImage, PresentationTheme)? private var updateDisposable: Disposable? private var optionsDisposable: Disposable? private(set) var options: [StarsTopUpOption] = [] { didSet { self.optionsPromise.set(self.options) } } private let optionsPromise = ValuePromise<[StarsTopUpOption]?>(nil) private let previewPromise = Promise(nil) private var cachedChevronImage: (UIImage, PresentationTheme)? override init(frame: CGRect) { self.dimView = UIView() self.containerView = UIView() self.containerView.clipsToBounds = true self.containerView.layer.cornerRadius = 38.0 self.containerView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] self.backgroundLayer = SimpleLayer() self.backgroundLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] self.backgroundLayer.cornerRadius = 38.0 self.backgroundHandleView = UIImageView() self.navigationBarContainer = SparseContainerView() self.scrollView = ScrollView() self.scrollContentClippingView = SparseContainerView() self.scrollContentClippingView.clipsToBounds = true self.scrollContentView = UIView() self.bottomEdgeEffectView = EdgeEffectView() super.init(frame: frame) self.addSubview(self.dimView) self.addSubview(self.containerView) self.containerView.layer.addSublayer(self.backgroundLayer) 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.alwaysBounceVertical = true self.scrollView.scrollsToTop = false self.scrollView.delegate = self self.scrollView.clipsToBounds = true self.containerView.addSubview(self.scrollContentClippingView) self.scrollContentClippingView.addSubview(self.scrollView) self.scrollView.addSubview(self.scrollContentView) self.containerView.addSubview(self.navigationBarContainer) self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { self.inputMediaNodeDataDisposable?.dispose() self.updateDisposable?.dispose() self.optionsDisposable?.dispose() self.giftAuctionDisposable?.dispose() self.giftAuctionTimer?.invalidate() } func scrollViewDidScroll(_ scrollView: UIScrollView) { if !self.ignoreScrolling { self.updateScrolling(transition: .immediate) } } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if !self.bounds.contains(point) { return nil } if !self.backgroundLayer.frame.contains(point) { return self.dimView } if let result = self.navigationBarContainer.hitTest(self.convert(point, to: self.navigationBarContainer), with: event) { return result } let result = super.hitTest(point, with: event) return result } @objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { guard let environment = self.environment, let controller = environment.controller() else { return } controller.dismiss() } } private func updateScrolling(transition: ComponentTransition) { guard let itemLayout = self.itemLayout else { return } var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset topOffset = max(0.0, topOffset) transition.setTransform(layer: self.backgroundLayer, transform: CATransform3DMakeTranslation(0.0, topOffset + itemLayout.containerInset, 0.0)) transition.setPosition(view: self.navigationBarContainer, position: CGPoint(x: 0.0, y: topOffset + itemLayout.containerInset)) var topOffsetFraction = self.scrollView.bounds.minY / 100.0 topOffsetFraction = max(0.0, min(1.0, topOffsetFraction)) let minScale: CGFloat = (itemLayout.containerSize.width - 6.0 * 2.0) / itemLayout.containerSize.width let minScaledTranslation: CGFloat = (itemLayout.containerSize.height - itemLayout.containerSize.height * minScale) * 0.5 - 6.0 let minScaledCornerRadius: CGFloat = itemLayout.containerCornerRadius let scale = minScale * (1.0 - topOffsetFraction) + 1.0 * topOffsetFraction let scaledTranslation = minScaledTranslation * (1.0 - topOffsetFraction) let scaledCornerRadius = minScaledCornerRadius * (1.0 - topOffsetFraction) + itemLayout.containerCornerRadius * topOffsetFraction var containerTransform = CATransform3DIdentity containerTransform = CATransform3DTranslate(containerTransform, 0.0, scaledTranslation, 0.0) containerTransform = CATransform3DScale(containerTransform, scale, scale, scale) transition.setTransform(view: self.containerView, transform: containerTransform) transition.setCornerRadius(layer: self.containerView.layer, cornerRadius: scaledCornerRadius) } func animateIn() { self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY self.scrollContentClippingView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) self.backgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) self.navigationBarContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) if let actionButtonView = self.actionButton.view { actionButtonView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) } self.bottomEdgeEffectView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) } func animateOut(completion: @escaping () -> Void) { let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY self.dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) self.scrollContentClippingView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in completion() }) self.backgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) self.navigationBarContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) if let actionButtonView = self.actionButton.view { actionButtonView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) } self.bottomEdgeEffectView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) } @objc private func proceed() { guard let component = self.component, let environment = self.environment else { return } if case let .starGift(gift, _) = component.subject, gift.flags.contains(.isAuction), let navigationController = environment.controller()?.navigationController as? NavigationController { let giftAuction = self.giftAuction let openAuction = { [weak giftAuction, weak navigationController] in guard let giftAuction, let navigationController else { return } let controller = component.context.sharedContext.makeGiftAuctionScreen(context: component.context, gift: .generic(gift), auctionContext: giftAuction) navigationController.pushViewController(controller) } //if self.openedAuction { openAuction() // } else { // let controller = component.context.sharedContext.makeGiftAuctionInfoScreen(context: component.context, gift: .generic(gift), completion: { // openAuction() // }) // environment.controller()?.push(controller) // } return } switch component.subject { case let .premium(product): if self.payWithStars, let starsPrice = product.starsPrice, let peer = self.peerMap[component.peerId] { if let balance = component.context.starsContext?.currentState?.balance, balance.value < starsPrice { self.proceedWithStarGift() } else { let controller = textAlertController( context: component.context, title: environment.strings.Gift_Send_Premium_Confirmation_Title, text: environment.strings.Gift_Send_Premium_Confirmation_Text( peer.compactDisplayTitle, environment.strings.Gift_Send_Premium_Confirmation_Text_Stars(Int32(clamping: starsPrice)) ).string, actions: [ TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: environment.strings.Gift_Send_Premium_Confirmation_Confirm, action: { [weak self] in if let self { self.proceedWithStarGift() } }) ], parseMarkdown: true ) environment.controller()?.present(controller, in: .window(.root)) } } else { self.proceedWithPremiumGift() } case .starGift: self.proceedWithStarGift() } } private func proceedWithPremiumGift() { guard let component = self.component, case let .premium(product) = component.subject, let storeProduct = product.storeProduct, let inAppPurchaseManager = component.context.inAppPurchaseManager else { return } self.inProgress = true self.state?.updated() let (currency, amount) = storeProduct.priceCurrencyAndAmount addAppLogEvent(postbox: component.context.account.postbox, type: "premium_gift.promo_screen_accept") var textInputText = NSAttributedString() if let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View, case let .text(text) = inputPanelView.getSendMessageInput() { textInputText = text } let entities = generateChatInputTextEntities(textInputText) let purpose: AppStoreTransactionPurpose = .giftCode(peerIds: [component.peerId], boostPeer: nil, currency: currency, amount: amount, text: textInputText.string, entities: entities) let quantity: Int32 = 1 let completion = component.completion let _ = (component.context.engine.payments.canPurchasePremium(purpose: purpose) |> deliverOnMainQueue).start(next: { [weak self] available in guard let self else { return } let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } if available { let _ = (inAppPurchaseManager.buyProduct(storeProduct, quantity: quantity, purpose: purpose) |> deliverOnMainQueue).start(next: { [weak self] status in if let completion { completion() if let self, let controller = self.environment?.controller() { controller.dismiss() } } else { guard let self, case .purchased = status, let controller = self.environment?.controller(), let navigationController = controller.navigationController as? NavigationController else { return } var controllers = navigationController.viewControllers controllers = controllers.filter { !($0 is GiftSetupScreen) && !($0 is GiftOptionsScreenProtocol) && !($0 is PeerInfoScreen) && !($0 is ContactSelectionController) } var foundController = false for controller in controllers.reversed() { if let chatController = controller as? ChatController, case .peer(id: component.peerId) = chatController.chatLocation { chatController.hintPlayNextOutgoingGift() foundController = true break } } if !foundController { let chatController = component.context.sharedContext.makeChatController(context: component.context, chatLocation: .peer(id: component.peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil) chatController.hintPlayNextOutgoingGift() controllers.append(chatController) } navigationController.setViewControllers(controllers, animated: true) } }, error: { [weak self] error in guard let self, let controller = self.environment?.controller() else { return } self.state?.updated(transition: .immediate) var errorText: String? switch error { case .generic: errorText = presentationData.strings.Premium_Purchase_ErrorUnknown case .network: errorText = presentationData.strings.Premium_Purchase_ErrorNetwork case .notAllowed: errorText = presentationData.strings.Premium_Purchase_ErrorNotAllowed case .cantMakePayments: errorText = presentationData.strings.Premium_Purchase_ErrorCantMakePayments case .assignFailed: errorText = presentationData.strings.Premium_Purchase_ErrorUnknown case .tryLater: errorText = presentationData.strings.Premium_Purchase_ErrorUnknown case .cancelled: break } if let errorText { addAppLogEvent(postbox: component.context.account.postbox, type: "premium_gift.promo_screen_fail") let alertController = textAlertController(context: component.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) controller.present(alertController, in: .window(.root)) } }) } else { self.inProgress = false self.state?.updated(transition: .immediate) } }) } private func proceedWithStarGift() { guard let component = self.component, let starsContext = component.context.starsContext, let starsState = starsContext.currentState else { return } let context = component.context let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } let peerId = component.peerId var textInputText = NSAttributedString() if let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View, case let .text(text) = inputPanelView.getSendMessageInput() { textInputText = text } let entities = generateChatInputTextEntities(textInputText) var finalPrice: Int64 var perUserLimit: Int32? var giftFile: TelegramMediaFile? let source: BotPaymentInvoiceSource switch component.subject { case let .premium(product): if let option = product.starsGiftOption { finalPrice = option.amount source = .premiumGift(peerId: peerId, option: option, text: textInputText.string, entities: entities) } else { fatalError() } case let .starGift(starGift, _): finalPrice = starGift.price if self.includeUpgrade, let upgradeStars = starGift.upgradeStars { finalPrice += upgradeStars } perUserLimit = starGift.perUserLimit?.total giftFile = starGift.file source = .starGift(hideName: self.hideName, includeUpgrade: self.includeUpgrade, peerId: peerId, giftId: starGift.id, text: textInputText.string, entities: entities) } let proceed = { [weak self] in guard let self else { return } self.inProgress = true self.state?.updated() let completion = component.completion let signal = BotCheckoutController.InputData.fetch(context: component.context, source: source) |> `catch` { error -> Signal in switch error { case .disallowedStarGifts: return .fail(.disallowedStarGift) case .starGiftsUserLimit: return .fail(.starGiftUserLimit) default: return .fail(.generic) } } |> mapToSignal { inputData -> Signal in return component.context.engine.payments.sendStarsPaymentForm(formId: inputData.form.id, source: source) } |> deliverOnMainQueue let _ = signal.start(next: { [weak self] result in guard let self, let controller = self.environment?.controller(), let navigationController = controller.navigationController as? NavigationController else { return } if peerId.namespace == Namespaces.Peer.CloudChannel, case let .starGift(starGift, _) = component.subject { var controllers = navigationController.viewControllers controllers = controllers.filter { !($0 is GiftSetupScreen) && !($0 is GiftOptionsScreenProtocol) } navigationController.setViewControllers(controllers, animated: true) let tooltipController = UndoOverlayController( presentationData: presentationData, content: .sticker( context: context, file: starGift.file, loop: true, title: nil, text: presentationData.strings.Gift_Send_Success(self.peerMap[peerId]?.compactDisplayTitle ?? "", presentationData.strings.Gift_Send_Success_Stars(Int32(clamping: starGift.price))).string, undoText: nil, customAction: nil ), action: { _ in return true } ) (navigationController.viewControllers.last as? ViewController)?.present(tooltipController, in: .current) navigationController.view.addSubview(ConfettiView(frame: navigationController.view.bounds)) } else if peerId.namespace == Namespaces.Peer.CloudUser { var controllers = navigationController.viewControllers controllers = controllers.filter { !($0 is GiftSetupScreen) && !($0 is GiftOptionsScreenProtocol) && !($0 is PeerInfoScreen) && !($0 is ContactSelectionController) } var foundController = false for controller in controllers.reversed() { if let chatController = controller as? ChatController, case .peer(id: component.peerId) = chatController.chatLocation { chatController.hintPlayNextOutgoingGift() foundController = true break } } if !foundController { let chatController = component.context.sharedContext.makeChatController(context: component.context, chatLocation: .peer(id: component.peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil) chatController.hintPlayNextOutgoingGift() controllers.append(chatController) } navigationController.setViewControllers(controllers, animated: true) if case let .starGift(starGift, _) = component.subject, let perUserLimit = starGift.perUserLimit { Queue.mainQueue().after(0.5) { let remains = max(0, perUserLimit.remains - 1) let text: String if remains == 0 { text = presentationData.strings.Gift_Send_Limited_Success_Text_None } else { text = presentationData.strings.Gift_Send_Limited_Success_Text(remains) } let tooltipController = UndoOverlayController( presentationData: presentationData, content: .sticker( context: context, file: starGift.file, loop: true, title: presentationData.strings.Gift_Send_Limited_Success_Title, text: text, undoText: nil, customAction: nil ), position: .top, action: { _ in return true } ) (navigationController.viewControllers.last as? ViewController)?.present(tooltipController, in: .current) } } } if let completion { completion() if let controller = self.environment?.controller() { controller.dismiss() } } starsContext.load(force: true) }, error: { [weak self] error in guard let self, let controller = self.environment?.controller() else { return } self.inProgress = false self.state?.updated() var errorText: String? switch error { case .starGiftUserLimit: if let perUserLimit, let giftFile { let text = presentationData.strings.Gift_Options_Gift_BuyLimitReached(perUserLimit) let undoController = UndoOverlayController( presentationData: presentationData, content: .sticker(context: component.context, file: giftFile, loop: true, title: nil, text: text, undoText: nil, customAction: nil), elevatedLayout: true, action: { _ in return false } ) controller.present(undoController, in: .current) return } return case .starGiftOutOfStock: errorText = presentationData.strings.Gift_Send_ErrorOutOfStock case .disallowedStarGift: errorText = presentationData.strings.Gift_Send_ErrorDisallowed(self.peerMap[peerId]?.compactDisplayTitle ?? "").string default: errorText = presentationData.strings.Gift_Send_ErrorUnknown } if let errorText = errorText { let alertController = textAlertController(context: component.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})], parseMarkdown: true) controller.present(alertController, in: .window(.root)) } }) } if starsState.balance < StarsAmount(value: finalPrice, nanos: 0) { let _ = (self.optionsPromise.get() |> filter { $0 != nil } |> take(1) |> deliverOnMainQueue).startStandalone(next: { [weak self] options in guard let self, let component = self.component, let controller = self.environment?.controller() else { return } let purchaseController = component.context.sharedContext.makeStarsPurchaseScreen( context: component.context, starsContext: starsContext, options: options ?? [], purpose: .starGift(peerId: component.peerId, requiredStars: finalPrice), targetPeerId: nil, customTheme: nil, completion: { [weak self, weak starsContext] stars in guard let self, let starsContext else { return } self.inProgress = true self.state?.updated() starsContext.add(balance: StarsAmount(value: stars, nanos: 0)) let _ = (starsContext.onUpdate |> deliverOnMainQueue).start(next: { proceed() }) } ) controller.push(purchaseController) }) } else { proceed() } } @objc private func previewTap() { self.deactivateInput() } private func updateInputMediaNode( component: GiftSetupScreenComponent, availableSize: CGSize, bottomInset: CGFloat, inputHeight: CGFloat, effectiveInputHeight: CGFloat, metrics: LayoutMetrics, deviceMetrics: DeviceMetrics, transition: ComponentTransition ) -> CGFloat { let bottomInset: CGFloat = bottomInset + 8.0 let bottomContainerInset: CGFloat = 0.0 let needsInputActivation: Bool = !"".isEmpty var height: CGFloat = 0.0 if case .emoji = self.currentInputMode, let inputData = self.inputMediaNodeData { let inputMediaNode: ChatEntityKeyboardInputNode var inputMediaNodeTransition = transition var animateIn = false if let current = self.inputMediaNode { inputMediaNode = current } else { animateIn = true inputMediaNodeTransition = inputMediaNodeTransition.withAnimation(.none) inputMediaNode = ChatEntityKeyboardInputNode( context: component.context, currentInputData: inputData, updatedInputData: self.inputMediaNodeDataPromise.get(), defaultToEmojiTab: true, opaqueTopPanelBackground: false, useOpaqueTheme: true, interaction: self.inputMediaInteraction, chatPeerId: nil, stateContext: self.inputMediaNodeStateContext, forceHasPremium: true ) inputMediaNode.clipsToBounds = true inputMediaNode.externalTopPanelContainerImpl = nil inputMediaNode.useExternalSearchContainer = true if inputMediaNode.view.superview == nil { self.inputMediaNodeBackground.removeAllAnimations() self.layer.addSublayer(self.inputMediaNodeBackground) self.addSubview(inputMediaNode.view) } self.inputMediaNode = inputMediaNode } let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } let presentationInterfaceState = ChatPresentationInterfaceState( chatWallpaper: .builtin(WallpaperSettings()), theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, limitsConfiguration: component.context.currentLimitsConfiguration.with { $0 }, fontSize: presentationData.chatFontSize, bubbleCorners: presentationData.chatBubbleCorners, accountPeerId: component.context.account.peerId, mode: .standard(.default), chatLocation: .peer(id: component.context.account.peerId), subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil, threadData: nil, isGeneralThreadClosed: nil, replyMessage: nil, accountPeerColor: nil, businessIntro: nil ) self.inputMediaNodeBackground.backgroundColor = presentationData.theme.rootController.navigationBar.opaqueBackgroundColor.cgColor let heightAndOverflow = inputMediaNode.updateLayout(width: availableSize.width, leftInset: 0.0, rightInset: 0.0, bottomInset: bottomInset, standardInputHeight: deviceMetrics.standardInputHeight(inLandscape: false), inputHeight: inputHeight < 100.0 ? inputHeight - bottomContainerInset : inputHeight, maximumHeight: availableSize.height, inputPanelHeight: 0.0, transition: .immediate, interfaceState: presentationInterfaceState, layoutMetrics: metrics, deviceMetrics: deviceMetrics, isVisible: true, isExpanded: false) let inputNodeHeight = heightAndOverflow.0 let inputNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - inputNodeHeight), size: CGSize(width: availableSize.width, height: inputNodeHeight)) let inputNodeBackgroundFrame = CGRect(origin: CGPoint(x: inputNodeFrame.minX, y: inputNodeFrame.minY - 6.0), size: CGSize(width: inputNodeFrame.width, height: inputNodeFrame.height + 6.0)) if needsInputActivation { let inputNodeFrame = inputNodeFrame.offsetBy(dx: 0.0, dy: inputNodeHeight) ComponentTransition.immediate.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame) ComponentTransition.immediate.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeBackgroundFrame) } if animateIn { var targetFrame = inputNodeFrame targetFrame.origin.y = availableSize.height inputMediaNodeTransition.setFrame(layer: inputMediaNode.layer, frame: targetFrame) let inputNodeBackgroundTargetFrame = CGRect(origin: CGPoint(x: targetFrame.minX, y: targetFrame.minY - 6.0), size: CGSize(width: targetFrame.width, height: targetFrame.height + 6.0)) inputMediaNodeTransition.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeBackgroundTargetFrame) transition.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame) transition.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeBackgroundFrame) } else { inputMediaNodeTransition.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame) inputMediaNodeTransition.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeBackgroundFrame) } height = heightAndOverflow.0 } else { self.inputMediaNodeTargetTag = nil if let inputMediaNode = self.inputMediaNode { self.inputMediaNode = nil var targetFrame = inputMediaNode.frame targetFrame.origin.y = availableSize.height transition.setFrame(view: inputMediaNode.view, frame: targetFrame, completion: { [weak inputMediaNode] _ in if let inputMediaNode { Queue.mainQueue().after(0.3) { inputMediaNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, removeOnCompletion: false, completion: { [weak inputMediaNode] _ in inputMediaNode?.view.removeFromSuperview() }) } } }) transition.setFrame(layer: self.inputMediaNodeBackground, frame: targetFrame, completion: { [weak self] _ in Queue.mainQueue().after(0.3) { guard let self else { return } if self.currentInputMode == .text { self.inputMediaNodeBackground.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, removeOnCompletion: false, completion: { [weak self] finished in guard let self else { return } if finished { self.inputMediaNodeBackground.removeFromSuperlayer() } self.inputMediaNodeBackground.removeAllAnimations() }) } } }) } } return height } private func activateInput() { self.currentInputMode = .text if !hasFirstResponder(self) { if let view = self.inputPanel.view as? MessageInputPanelComponent.View { view.activateInput() } } else { self.state?.updated(transition: .immediate) } } private var nextTransitionUserData: Any? @objc private func deactivateInput() { guard let _ = self.inputPanel.view as? MessageInputPanelComponent.View else { return } self.currentInputMode = .text if hasFirstResponder(self) { if let view = self.inputPanel.view as? MessageInputPanelComponent.View { self.nextTransitionUserData = TextFieldComponent.AnimationHint(view: nil, kind: .textFocusChanged(isFocused: false)) if view.isActive { view.deactivateInput(force: true) } else { self.endEditing(true) } } } else { self.state?.updated(transition: .spring(duration: 0.4).withUserData(TextFieldComponent.AnimationHint(view: nil, kind: .textFocusChanged(isFocused: false)))) } } func update(component: GiftSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false } let environment = environment[ViewControllerComponentContainer.Environment.self].value let themeUpdated = self.environment?.theme !== environment.theme let resetScrolling = self.scrollView.bounds.width != availableSize.width let fillingSize: CGFloat if case .regular = environment.metrics.widthClass { fillingSize = min(availableSize.width, 414.0) - environment.safeInsets.left * 2.0 } else { fillingSize = min(availableSize.width, 428.0) - environment.safeInsets.left * 2.0 } let sideInset: CGFloat = floor((availableSize.width - fillingSize) * 0.5) + 24.0 let peerName = self.peerMap[component.peerId]?.compactDisplayTitle ?? "" let isSelfGift = component.peerId == component.context.account.peerId let isChannelGift = component.peerId.namespace == Namespaces.Peer.CloudChannel if self.component == nil { if isSelfGift { self.hideName = true } if case let .starGift(gift, _) = component.subject, gift.flags.contains(.isAuction), let giftAuctionsManager = component.context.giftAuctionsManager { let giftAuction = giftAuctionsManager.auctionContextForGift(giftId: gift.id) self.giftAuction = giftAuction self.giftAuctionDisposable = (giftAuction.state |> deliverOnMainQueue).start(next: { [weak self] state in guard let self else { return } self.giftAuctionState = state self.state?.updated() }) self.giftAuctionTimer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in self?.state?.updated() }, queue: Queue.mainQueue()) self.giftAuctionTimer?.start() } var releasedBy: EnginePeer.Id? if case let .starGift(gift, true) = component.subject, gift.upgradeStars != nil { self.includeUpgrade = true } if case let .starGift(gift, _) = component.subject { releasedBy = gift.releasedBy } var peerIds: [EnginePeer.Id] = [ component.context.account.peerId, component.peerId ] if let releasedBy { peerIds.append(releasedBy) } let _ = combineLatest(queue: Queue.mainQueue(), component.context.engine.data.get(EngineDataMap( peerIds.map { peerId -> TelegramEngine.EngineData.Item.Peer.Peer in return TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) } )), component.context.engine.data.get( TelegramEngine.EngineData.Item.Peer.SendPaidMessageStars(id: component.peerId) ) ).start(next: { [weak self] peers, sendPaidMessageStars in guard let self else { return } var peersMap: [EnginePeer.Id: EnginePeer] = [:] for (peerId, maybePeer) in peers { if let peer = maybePeer { peersMap[peerId] = peer } } self.peerMap = peersMap self.sendPaidMessageStars = sendPaidMessageStars self.state?.updated() }) self.inputMediaNodeDataPromise.set( ChatEntityKeyboardInputNode.inputData( context: component.context, chatPeerId: nil, areCustomEmojiEnabled: true, hasTrending: false, hasSearch: true, hasStickers: false, hasGifs: false, hideBackground: true, forceHasPremium: true, sendGif: nil ) ) self.inputMediaNodeDataDisposable = (self.inputMediaNodeDataPromise.get() |> deliverOnMainQueue).start(next: { [weak self] value in guard let self else { return } self.inputMediaNodeData = value }) self.inputMediaInteraction = ChatEntityKeyboardInputNode.Interaction( sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in let _ = self }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _ , _, _, _, _ in return false }, updateChoosingSticker: { _ in }, switchToTextInput: { [weak self] in guard let self else { return } self.currentInputMode = .text self.state?.updated(transition: .spring(duration: 0.4)) }, dismissTextInput: { }, insertText: { [weak self] text in guard let self else { return } self.inputPanelExternalState.insertText(text) }, backwardsDeleteText: { [weak self] in guard let self else { return } self.inputPanelExternalState.deleteBackward() }, openStickerEditor: { }, presentController: { [weak self] c, a in guard let self else { return } self.environment?.controller()?.present(c, in: .window(.root), with: a) }, presentGlobalOverlayController: { [weak self] c, a in guard let self else { return } self.environment?.controller()?.presentInGlobalOverlay(c, with: a) }, getNavigationController: { [weak self] () -> NavigationController? in guard let self else { return nil } guard let controller = self.environment?.controller() as? GiftSetupScreen else { return nil } if let navigationController = controller.navigationController as? NavigationController { return navigationController } return nil }, requestLayout: { [weak self] transition in guard let self else { return } if !self.isUpdating { self.state?.updated(transition: ComponentTransition(transition)) } } ) self.optionsDisposable = (component.context.engine.payments.starsTopUpOptions() |> deliverOnMainQueue).start(next: { [weak self] options in guard let self else { return } self.options = options }) if case let .starGift(gift, _) = component.subject { if let _ = gift.upgradeStars { self.previewPromise.set( component.context.engine.payments.starGiftUpgradePreview(giftId: gift.id) ) } self.updateDisposable = component.context.engine.payments.keepStarGiftsUpdated().start() } } self.component = component self.state = state self.environment = environment let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } if themeUpdated { self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5) self.backgroundLayer.backgroundColor = environment.theme.list.blocksBackgroundColor.cgColor } transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize)) let sectionSpacing: CGFloat = 24.0 var contentHeight: CGFloat = 0.0 if self.backgroundHandleView.image == nil { self.backgroundHandleView.image = generateStretchableFilledCircleImage(diameter: 5.0, color: .white)?.withRenderingMode(.alwaysTemplate) } self.backgroundHandleView.tintColor = environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.2) let backgroundHandleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - 36.0) * 0.5), y: 5.0), size: CGSize(width: 36.0, height: 5.0)) if self.backgroundHandleView.superview == nil { self.navigationBarContainer.addSubview(self.backgroundHandleView) } transition.setFrame(view: self.backgroundHandleView, frame: backgroundHandleFrame) let closeButtonSize = self.closeButton.update( transition: .immediate, component: AnyComponent(GlassBarButtonComponent( size: CGSize(width: 40.0, height: 40.0), backgroundColor: environment.theme.rootController.navigationBar.glassBarButtonBackgroundColor, isDark: environment.theme.overallDarkAppearance, state: .generic, component: AnyComponentWithIdentity(id: "close", component: AnyComponent( BundleIconComponent( name: "Navigation/Close", tintColor: environment.theme.rootController.navigationBar.glassBarButtonForegroundColor ) )), action: { [weak self] _ in guard let self else { return } self.environment?.controller()?.dismiss() } )), environment: {}, containerSize: CGSize(width: 40.0, height: 40.0) ) let closeButtonFrame = CGRect(origin: CGPoint(x: 16.0, y: 16.0), size: closeButtonSize) if let closeButtonView = self.closeButton.view { if closeButtonView.superview == nil { self.navigationBarContainer.addSubview(closeButtonView) } transition.setFrame(view: closeButtonView, frame: closeButtonFrame) } let containerInset: CGFloat = environment.statusBarHeight + 10.0 var initialContentHeight = contentHeight let clippingY: CGFloat if case let .starGift(starGift, forceUnique) = component.subject, let availability = starGift.availability, availability.resale > 0 { if let forceUnique, !forceUnique { } else { let resaleSectionSize = self.resaleSection.update( transition: transition, component: AnyComponent(ListSectionComponent( theme: environment.theme, style: .glass, header: nil, footer: nil, items: [ AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( theme: environment.theme, style: .glass, title: AnyComponent(VStack([ AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent( MultilineTextComponent( text: .plain(NSAttributedString(string: environment.strings.Gift_Send_AvailableForResale, font: Font.regular(presentationData.listsFontSize.baseDisplaySize), textColor: environment.theme.list.itemPrimaryTextColor)) ) )), ], alignment: .left, spacing: 2.0)), accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: presentationStringsFormattedNumber(Int32(availability.resale), environment.dateTimeFormat.groupingSeparator), font: Font.regular(presentationData.listsFontSize.baseDisplaySize), textColor: environment.theme.list.itemSecondaryTextColor )), maximumNumberOfLines: 0 ))), insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 16.0))), action: { [weak self] _ in guard let self, let component = self.component, let controller = environment.controller() else { return } let storeController = component.context.sharedContext.makeGiftStoreController( context: component.context, peerId: component.peerId, gift: starGift ) controller.push(storeController) } ))) ] )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) ) let resaleSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: resaleSectionSize) if let resaleSectionView = self.resaleSection.view { if resaleSectionView.superview == nil { self.scrollContentView.addSubview(resaleSectionView) } transition.setFrame(view: resaleSectionView, frame: resaleSectionFrame) } contentHeight += resaleSectionSize.height contentHeight += sectionSpacing } } let giftConfiguration = GiftConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 }) let footerAttributes = MarkdownAttributes( body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.freeTextColor), bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: environment.theme.list.freeTextColor), link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemAccentColor), linkAttribute: { contents in return (TelegramTextAttributes.URL, contents) } ) // let introFooter: AnyComponent? // switch component.subject { // case .premium: // introFooter = AnyComponent(MultilineTextComponent( // text: .plain(NSAttributedString( // string: environment.strings.Gift_Send_Customize_Info(peerName).string, // font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), // textColor: environment.theme.list.freeTextColor // )), // maximumNumberOfLines: 0 // )) // case .starGift: // introFooter = nil // } // // let introSectionSize = self.introSection.update( // transition: transition, // component: AnyComponent(ListSectionComponent( // theme: environment.theme, // style: .glass, // header: nil, // footer: introFooter, // items: introSectionItems // )), // environment: {}, // containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) // ) // let introSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: introSectionSize) // if let introSectionView = self.introSection.view { // if introSectionView.superview == nil { // self.scrollContentView.addSubview(introSectionView) // self.introSection.parentState = state // } // transition.setFrame(view: introSectionView, frame: introSectionFrame) // } // contentHeight += introSectionSize.height // contentHeight += sectionSpacing var inputHeight: CGFloat = 0.0 inputHeight += self.updateInputMediaNode( component: component, availableSize: availableSize, bottomInset: environment.safeInsets.bottom, inputHeight: 0.0, effectiveInputHeight: environment.deviceMetrics.standardInputHeight(inLandscape: false), metrics: environment.metrics, deviceMetrics: environment.deviceMetrics, transition: transition ) if self.inputMediaNode == nil { if environment.inputHeight.isZero && self.inputPanelExternalState.isEditing, let previousInputHeight = self.previousInputHeight { inputHeight = previousInputHeight } else { inputHeight = environment.inputHeight } } self.previousInputHeight = inputHeight let listItemParams = ListViewItemLayoutParams(width: availableSize.width, leftInset: 0.0, rightInset: 0.0, availableHeight: 10000.0, isStandalone: true) var introContentSize = CGSize() if let accountPeer = self.peerMap[component.context.account.peerId] { var inputPanelSize = CGSize() let inputPanelInset: CGFloat = 16.0 if self.sendPaidMessageStars == nil { let nextInputMode: MessageInputPanelComponent.InputMode switch self.currentInputMode { case .text: nextInputMode = .emoji case .emoji: nextInputMode = .text default: nextInputMode = .emoji } self.inputPanel.parentState = state inputPanelSize = self.inputPanel.update( transition: transition, component: AnyComponent(MessageInputPanelComponent( externalState: self.inputPanelExternalState, context: component.context, theme: environment.theme, strings: environment.strings, style: .gift, placeholder: .plain(environment.strings.Gift_Send_Customize_MessagePlaceholder), sendPaidMessageStars: nil, maxLength: Int(giftConfiguration.maxCaptionLength), queryTypes: [], alwaysDarkWhenHasText: false, useGrayBackground: false, resetInputContents: nil, nextInputMode: { _ in return nextInputMode }, areVoiceMessagesAvailable: false, presentController: { c in }, presentInGlobalOverlay: { c in }, sendMessageAction: { [weak self] _ in guard let self else { return } self.deactivateInput() }, sendMessageOptionsAction: nil, sendStickerAction: { _ in }, setMediaRecordingActive: nil, lockMediaRecording: { }, stopAndPreviewMediaRecording: { }, discardMediaRecordingPreview: nil, attachmentAction: nil, myReaction: nil, likeAction: nil, likeOptionsAction: nil, inputModeAction: { [weak self] in if let self { switch self.currentInputMode { case .text: self.currentInputMode = .emoji case .emoji: self.currentInputMode = .text default: self.currentInputMode = .emoji } if self.currentInputMode == .text { self.activateInput() } else { self.state?.updated(transition: .immediate) } } }, timeoutAction: nil, forwardAction: nil, paidMessageAction: nil, moreAction: nil, presentCaptionPositionTooltip: nil, presentVoiceMessagesUnavailableTooltip: nil, presentTextLengthLimitTooltip: { }, presentTextFormattingTooltip: { }, paste: { _ in }, audioRecorder: nil, videoRecordingStatus: nil, isRecordingLocked: false, hasRecordedVideo: false, recordedAudioPreview: nil, hasRecordedVideoPreview: false, wasRecordingDismissed: false, timeoutValue: nil, timeoutSelected: false, displayGradient: false, bottomInset: 0.0, isFormattingLocked: false, hideKeyboard: self.currentInputMode == .emoji, customInputView: nil, forceIsEditing: self.currentInputMode == .emoji, disabledPlaceholder: nil, header: nil, isChannel: false, storyItem: nil, chatLocation: nil )), environment: {}, containerSize: CGSize(width: availableSize.width - inputPanelInset * 2.0, height: 160.0) ) } var upgradeStars: Int64? let subject: ChatGiftPreviewItem.Subject var releasedBy: EnginePeer.Id? switch component.subject { case let .premium(product): if self.payWithStars, let starsPrice = product.starsPrice { subject = .premium(months: product.months, amount: starsPrice, currency: "XTR") } else { let (currency, amount) = product.storeProduct?.priceCurrencyAndAmount ?? ("USD", 1) subject = .premium(months: product.months, amount: amount, currency: currency) } case let .starGift(gift, _): subject = .starGift(gift: gift) upgradeStars = gift.upgradeStars releasedBy = gift.releasedBy } var peers: [EnginePeer] = [accountPeer] if let peer = self.peerMap[component.peerId] { peers.append(peer) } if let releasedBy, let peer = self.peerMap[releasedBy] { peers.append(peer) } var textInputText = NSAttributedString() if let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View, case let .text(text) = inputPanelView.getSendMessageInput(applyAutocorrection: false) { textInputText = text } introContentSize = self.introContent.update( transition: transition, component: AnyComponent( ListItemComponentAdaptor( itemGenerator: ChatGiftPreviewItem( context: component.context, theme: environment.theme, componentTheme: environment.theme, strings: environment.strings, sectionId: 0, fontSize: presentationData.chatFontSize, chatBubbleCorners: presentationData.chatBubbleCorners, wallpaper: presentationData.chatWallpaper, dateTimeFormat: environment.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, peers: peers, subject: subject, chatPeerId: component.peerId, text: textInputText.string, entities: generateChatInputTextEntities(textInputText), upgradeStars: self.includeUpgrade ? upgradeStars : nil, chargeStars: nil, bottomInset: max(0.0, inputPanelSize.height - 26.0) ), params: listItemParams ) ), environment: {}, containerSize: CGSize(width: availableSize.width, height: 10000.0) ) if let introContentView = self.introContent.view { if introContentView.superview == nil { introContentView.clipsToBounds = true introContentView.layer.cornerRadius = 38.0 introContentView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] self.scrollContentView.addSubview(introContentView) introContentView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.previewTap))) } transition.setFrame(view: introContentView, frame: CGRect(origin: CGPoint(), size: introContentSize)) } let inputPanelFrame = CGRect(origin: CGPoint(x: inputPanelInset, y: contentHeight + introContentSize.height - inputPanelInset - inputPanelSize.height + 6.0), size: inputPanelSize) if let inputPanelView = self.inputPanel.view { if inputPanelView.superview == nil { self.scrollContentView.addSubview(inputPanelView) } transition.setFrame(view: inputPanelView, frame: inputPanelFrame) } } contentHeight += introContentSize.height contentHeight += sectionSpacing switch component.subject { case let .premium(product): let balance = component.context.starsContext?.currentState?.balance.value ?? 0 if let starsPrice = product.starsPrice, balance >= starsPrice { let balanceString = presentationStringsFormattedNumber(Int32(balance), environment.dateTimeFormat.groupingSeparator) let starsFooterRawString = environment.strings.Gift_Send_PayWithStars_Info("# \(balanceString)").string let starsFooterText = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(starsFooterRawString, attributes: footerAttributes)) if self.cachedChevronImage == nil || self.cachedChevronImage?.1 !== environment.theme { self.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: environment.theme.list.itemAccentColor)!, environment.theme) } if let range = starsFooterText.string.range(of: "#") { starsFooterText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: starsFooterText.string)) } if let range = starsFooterText.string.range(of: ">"), let chevronImage = self.cachedChevronImage?.0 { starsFooterText.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: starsFooterText.string)) } let priceString = presentationStringsFormattedNumber(Int32(starsPrice), environment.dateTimeFormat.groupingSeparator) let starsAttributedText = NSMutableAttributedString(string: environment.strings.Gift_Send_PayWithStars("#\(priceString)").string, font: Font.regular(presentationData.listsFontSize.baseDisplaySize), textColor: environment.theme.list.itemPrimaryTextColor) let range = (starsAttributedText.string as NSString).range(of: "#") if range.location != NSNotFound { starsAttributedText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range) starsAttributedText.addAttribute(.baselineOffset, value: 1.0, range: range) } let starsSectionSize = self.starsSection.update( transition: transition, component: AnyComponent(ListSectionComponent( theme: environment.theme, style: .glass, header: nil, footer: AnyComponent(MultilineTextWithEntitiesComponent( context: component.context, animationCache: component.context.animationCache, animationRenderer: component.context.animationRenderer, placeholderColor: .clear, text: .plain(starsFooterText), 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] _, _ in guard let self, let component = self.component, let controller = self.environment?.controller(), let starsContext = component.context.starsContext else { return } let _ = (self.optionsPromise.get() |> filter { $0 != nil } |> take(1) |> deliverOnMainQueue).startStandalone(next: { options in let purchaseController = component.context.sharedContext.makeStarsPurchaseScreen(context: component.context, starsContext: starsContext, options: options ?? [], purpose: .generic, targetPeerId: nil, customTheme: nil, completion: { stars in starsContext.add(balance: StarsAmount(value: stars, nanos: 0)) }) controller.push(purchaseController) }) } )), items: [ AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( theme: environment.theme, style: .glass, title: AnyComponent(VStack([ AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent( MultilineTextWithEntitiesComponent( context: component.context, animationCache: component.context.animationCache, animationRenderer: component.context.animationRenderer, placeholderColor: environment.theme.list.mediaPlaceholderColor, text: .plain(starsAttributedText) ) )), ], alignment: .left, spacing: 2.0)), accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.payWithStars, action: { [weak self] _ in guard let self else { return } self.payWithStars = !self.payWithStars self.state?.updated(transition: .spring(duration: 0.4)) })), action: nil ))) ] )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) ) let starsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: starsSectionSize) if let starsSectionView = self.starsSection.view { if starsSectionView.superview == nil { self.scrollContentView.addSubview(starsSectionView) } transition.setFrame(view: starsSectionView, frame: starsSectionFrame) } contentHeight += starsSectionSize.height contentHeight += sectionSpacing } case let .starGift(gift, forceUnique): if let upgradeStars = gift.upgradeStars, component.peerId != component.context.account.peerId { let upgradeFooterRawString: String if isChannelGift { upgradeFooterRawString = environment.strings.Gift_SendChannel_Upgrade_Info(peerName).string } else { if forceUnique == true { upgradeFooterRawString = environment.strings.Gift_Send_Upgrade_ForcedInfo(peerName).string } else { upgradeFooterRawString = environment.strings.Gift_Send_Upgrade_Info(peerName).string } } let parsedString = parseMarkdownIntoAttributedString(upgradeFooterRawString, attributes: footerAttributes) let upgradeFooterText = NSMutableAttributedString(attributedString: parsedString) if self.cachedChevronImage == nil || self.cachedChevronImage?.1 !== environment.theme { self.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: environment.theme.list.itemAccentColor)!, environment.theme) } if let range = upgradeFooterText.string.range(of: ">"), let chevronImage = self.cachedChevronImage?.0 { upgradeFooterText.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: upgradeFooterText.string)) } let upgradeAttributedText = NSMutableAttributedString(string: environment.strings.Gift_Send_Upgrade("#\(upgradeStars)").string, font: Font.regular(presentationData.listsFontSize.baseDisplaySize), textColor: environment.theme.list.itemPrimaryTextColor) let range = (upgradeAttributedText.string as NSString).range(of: "#") if range.location != NSNotFound { upgradeAttributedText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range) upgradeAttributedText.addAttribute(.baselineOffset, value: 1.0, range: range) } let upgradeSectionSize = self.upgradeSection.update( transition: transition, component: AnyComponent(ListSectionComponent( theme: environment.theme, style: .glass, header: nil, footer: AnyComponent(MultilineTextComponent( text: .plain(upgradeFooterText), 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] _, _ in guard let self else { return } let _ = (self.previewPromise.get() |> take(1) |> deliverOnMainQueue).start(next: { [weak self] upgradePreview in guard let self, let component = self.component, let controller = self.environment?.controller(), let upgradePreview else { return } let previewController = GiftViewScreen( context: component.context, subject: .upgradePreview(upgradePreview.attributes, peerName) ) controller.push(previewController) }) } )), items: [ AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( theme: environment.theme, style: .glass, title: AnyComponent(VStack([ AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent( MultilineTextWithEntitiesComponent( context: component.context, animationCache: component.context.animationCache, animationRenderer: component.context.animationRenderer, placeholderColor: environment.theme.list.mediaPlaceholderColor, text: .plain(upgradeAttributedText) ) )), ], alignment: .left, spacing: 2.0)), accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.includeUpgrade, isEnabled: forceUnique != true, action: { [weak self] _ in guard let self, forceUnique != true else { return } self.includeUpgrade = !self.includeUpgrade self.state?.updated(transition: .spring(duration: 0.4)) })), action: nil ))) ] )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) ) let upgradeSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: upgradeSectionSize) if let upgradeSectionView = self.upgradeSection.view { if upgradeSectionView.superview == nil { self.scrollContentView.addSubview(upgradeSectionView) } transition.setFrame(view: upgradeSectionView, frame: upgradeSectionFrame) } contentHeight += upgradeSectionSize.height contentHeight += sectionSpacing } let hideSectionFooterString: String if isSelfGift { hideSectionFooterString = environment.strings.Gift_SendSelf_HideMyName_Info } else if isChannelGift { hideSectionFooterString = environment.strings.Gift_SendChannel_HideMyName_Info } else { hideSectionFooterString = environment.strings.Gift_Send_HideMyName_Info(peerName, peerName).string } let hideSectionSize = self.hideSection.update( transition: transition, component: AnyComponent(ListSectionComponent( theme: environment.theme, style: .glass, header: nil, footer: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: hideSectionFooterString, font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor )), maximumNumberOfLines: 0 )), items: [ AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( theme: environment.theme, style: .glass, title: AnyComponent(VStack([ AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: isSelfGift ? environment.strings.Gift_SendSelf_HideMyName : environment.strings.Gift_Send_HideMyName, font: Font.regular(presentationData.listsFontSize.baseDisplaySize), textColor: environment.theme.list.itemPrimaryTextColor )), maximumNumberOfLines: 1 ))), ], alignment: .left, spacing: 2.0)), accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.hideName, action: { [weak self] _ in guard let self else { return } self.hideName = !self.hideName self.state?.updated(transition: .spring(duration: 0.4)) })), action: nil ))) ] )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) ) let hideSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: hideSectionSize) if let hideSectionView = self.hideSection.view { if hideSectionView.superview == nil { self.scrollContentView.addSubview(hideSectionView) } transition.setFrame(view: hideSectionView, frame: hideSectionFrame) } contentHeight += hideSectionSize.height } contentHeight += sectionSpacing if case let .starGift(starGift, _) = component.subject, let availability = starGift.availability { contentHeight -= 77.0 contentHeight += 16.0 let remains: Int32 = availability.remains let total: Int32 = availability.total let position = CGFloat(remains) / CGFloat(total) let sold = total - remains let remainingCountSize = self.remainingCount.update( transition: transition, component: AnyComponent(RemainingCountComponent( inactiveColor: environment.theme.list.itemBlocksBackgroundColor, activeColors: [UIColor(rgb: 0x72d6ff), UIColor(rgb: 0x32a0f9)], inactiveTitle: environment.strings.Gift_Send_Remains(remains), inactiveValue: "", inactiveTitleColor: environment.theme.list.itemSecondaryTextColor, activeTitle: "", activeValue: environment.strings.Gift_Send_Sold(sold), activeTitleColor: .white, badgeText: "", badgePosition: position, badgeGraphPosition: position, invertProgress: true, leftString: "", groupingSeparator: environment.dateTimeFormat.groupingSeparator )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) ) let remainingCountFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: remainingCountSize) if let remainingCountView = self.remainingCount.view { if remainingCountView.superview == nil { self.scrollContentView.addSubview(remainingCountView) } transition.setFrame(view: remainingCountView, frame: remainingCountFrame) } contentHeight += remainingCountSize.height contentHeight += 7.0 if starGift.flags.contains(.isAuction) { let parsedString = parseMarkdownIntoAttributedString("50 gifts are dropped at varying intervals to the top 50 bidders by bid amount. [Learn more >]()", attributes: footerAttributes) let auctionFooterText = NSMutableAttributedString(attributedString: parsedString) if self.cachedChevronImage == nil || self.cachedChevronImage?.1 !== environment.theme { self.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: environment.theme.list.itemAccentColor)!, environment.theme) } if let range = auctionFooterText.string.range(of: ">"), let chevronImage = self.cachedChevronImage?.0 { auctionFooterText.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: auctionFooterText.string)) } let auctionFooterSize = self.auctionFooter.update( transition: transition, component: AnyComponent(MultilineTextComponent( text: .plain(auctionFooterText), 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] _, _ in guard let self, let component = self.component, case let .starGift(gift, _) = component.subject, let controller = self.environment?.controller() else { return } let infoController = component.context.sharedContext.makeGiftAuctionInfoScreen(context: component.context, gift: .generic(gift), completion: nil) controller.push(infoController) } )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 16.0 * 2.0, height: 10000.0) ) let auctionFooterFrame = CGRect(origin: CGPoint(x: sideInset + 16.0, y: contentHeight), size: auctionFooterSize) if let auctionFooterView = self.auctionFooter.view { if auctionFooterView.superview == nil { self.scrollContentView.addSubview(auctionFooterView) } transition.setFrame(view: auctionFooterView, frame: auctionFooterFrame) } contentHeight += auctionFooterSize.height } contentHeight += sectionSpacing } initialContentHeight = contentHeight if self.cachedStarImage == nil || self.cachedStarImage?.1 !== environment.theme { self.cachedStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: .white)!, environment.theme) } var buttonIsEnabled = true let buttonString: String switch component.subject { case let .premium(product): if self.payWithStars, let starsPrice = product.starsPrice { let amountString = presentationStringsFormattedNumber(Int32(starsPrice), presentationData.dateTimeFormat.groupingSeparator) buttonString = "\(environment.strings.Gift_Send_Send) # \(amountString)" } else { let amountString = product.price buttonString = "\(environment.strings.Gift_Send_Send) \(amountString)" } case let .starGift(starGift, _): var finalPrice: Int64 = starGift.price if self.includeUpgrade, let upgradePrice = starGift.upgradeStars { finalPrice += upgradePrice } let amountString = presentationStringsFormattedNumber(Int32(finalPrice), presentationData.dateTimeFormat.groupingSeparator) let buttonTitle = isSelfGift ? environment.strings.Gift_Send_Buy : environment.strings.Gift_Send_Send buttonString = "\(buttonTitle) # \(amountString)" if let availability = starGift.availability, availability.remains == 0 { buttonIsEnabled = false } } var buttonTitleItems: [AnyComponentWithIdentity] = [] let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) if let range = buttonAttributedString.string.range(of: "#"), let starImage = self.cachedStarImage?.0 { buttonAttributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: buttonAttributedString.string)) buttonAttributedString.addAttribute(.foregroundColor, value: environment.theme.list.itemCheckColors.foregroundColor, range: NSRange(range, in: buttonAttributedString.string)) buttonAttributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: buttonAttributedString.string)) buttonAttributedString.addAttribute(.kern, value: 2.0, range: NSRange(range, in: buttonAttributedString.string)) } var buttonIsLoading = false if let _ = self.giftAuction { //TODO:localize let buttonAttributedString = NSMutableAttributedString(string: "Place a Bid", font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) buttonTitleItems.append(AnyComponentWithIdentity(id: "bid", component: AnyComponent( MultilineTextComponent(text: .plain(buttonAttributedString)) ))) if let giftAuctionState = self.giftAuctionState { switch giftAuctionState.auctionState { case let .ongoing(_, _, _, _, nextDropDate, _, _, _): let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) let dropTimeout = nextDropDate - currentTime let minutes = Int(dropTimeout / 60) let seconds = Int(dropTimeout % 60) let rawString = environment.strings.Gift_Setup_NextDropIn var buttonAnimatedTitleItems: [AnimatedTextComponent.Item] = [] var startIndex = rawString.startIndex while true { if let range = rawString.range(of: "{", range: startIndex ..< rawString.endIndex) { if range.lowerBound != startIndex { buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: AnyHashable(buttonAnimatedTitleItems.count), content: .text(String(rawString[startIndex ..< range.lowerBound])))) } startIndex = range.upperBound if let endRange = rawString.range(of: "}", range: startIndex ..< rawString.endIndex) { let controlString = rawString[range.upperBound ..< endRange.lowerBound] if controlString == "m" { buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: AnyHashable(buttonAnimatedTitleItems.count), content: .number(minutes, minDigits: 2))) } else if controlString == "s" { buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: AnyHashable(buttonAnimatedTitleItems.count), content: .number(seconds, minDigits: 2))) } startIndex = endRange.upperBound } } else { break } } if startIndex != rawString.endIndex { buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: AnyHashable(buttonAnimatedTitleItems.count), content: .text(String(rawString[startIndex ..< rawString.endIndex])))) } buttonTitleItems.append(AnyComponentWithIdentity(id: "timer", component: AnyComponent(AnimatedTextComponent( font: Font.with(size: 12.0, weight: .medium, traits: .monospacedNumbers), color: environment.theme.list.itemCheckColors.foregroundColor.withAlphaComponent(0.7), items: buttonAnimatedTitleItems, noDelay: true )))) case .finished: buttonIsEnabled = false } } else { buttonIsLoading = true } } else { buttonTitleItems.append(AnyComponentWithIdentity(id: buttonString, component: AnyComponent( MultilineTextComponent(text: .plain(buttonAttributedString)) ))) } let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 32.0) let buttonHeight: CGFloat = 52.0 let actionButtonSize = self.actionButton.update( transition: .spring(duration: 0.2), component: AnyComponent(ButtonComponent( background: ButtonComponent.Background( style: .glass, color: environment.theme.list.itemCheckColors.fillColor, foreground: environment.theme.list.itemCheckColors.foregroundColor, pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9), isShimmering: true ), content: AnyComponentWithIdentity( id: AnyHashable("title"), component: AnyComponent(VStack(buttonTitleItems, spacing: 1.0)) ), isEnabled: buttonIsEnabled, displaysProgress: buttonIsLoading || self.inProgress, action: { [weak self] in self?.proceed() } )), environment: {}, containerSize: CGSize(width: availableSize.width - buttonInsets.left - buttonInsets.right, height: buttonHeight) ) var bottomPanelHeight = 13.0 + buttonInsets.bottom + actionButtonSize.height let bottomEdgeEffectHeight: CGFloat = bottomPanelHeight let bottomEdgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomEdgeEffectHeight), size: CGSize(width: availableSize.width, height: bottomEdgeEffectHeight)) transition.setFrame(view: self.bottomEdgeEffectView, frame: bottomEdgeEffectFrame) self.bottomEdgeEffectView.update(content: environment.theme.actionSheet.opaqueItemBackgroundColor, blur: true, alpha: 1.0, rect: bottomEdgeEffectFrame, edge: .bottom, edgeSize: bottomEdgeEffectFrame.height, transition: transition) if self.bottomEdgeEffectView.superview == nil { self.containerView.addSubview(self.bottomEdgeEffectView) } let actionButtonFrame = CGRect(origin: CGPoint(x: buttonInsets.left, y: availableSize.height - buttonInsets.bottom - actionButtonSize.height), size: actionButtonSize) if let buttonView = self.actionButton.view { if buttonView.superview == nil { self.containerView.addSubview(buttonView) } buttonView.frame = actionButtonFrame } bottomPanelHeight -= 1.0 contentHeight += bottomPanelHeight initialContentHeight += bottomPanelHeight clippingY = actionButtonFrame.maxY + 24.0 var topInset: CGFloat = max(0.0, availableSize.height - containerInset - initialContentHeight) if self.inputPanelExternalState.isEditing { topInset = 0.0 } let scrollContentHeight = max(topInset + contentHeight + containerInset, availableSize.height - containerInset) self.scrollContentClippingView.layer.cornerRadius = 38.0 self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, containerCornerRadius: environment.deviceMetrics.screenCornerRadius, bottomInset: environment.safeInsets.bottom, topInset: topInset) transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + containerInset), size: CGSize(width: availableSize.width, height: contentHeight))) transition.setPosition(layer: self.backgroundLayer, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)) transition.setBounds(layer: self.backgroundLayer, bounds: CGRect(origin: CGPoint(), size: CGSize(width: fillingSize, height: availableSize.height))) let scrollClippingFrame = CGRect(origin: CGPoint(x: 0.0, y: containerInset), size: CGSize(width: availableSize.width, height: clippingY - containerInset)) transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center) transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size)) self.ignoreScrolling = true transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height))) let contentSize = CGSize(width: availableSize.width, height: scrollContentHeight) if contentSize != self.scrollView.contentSize { self.scrollView.contentSize = contentSize } if resetScrolling { self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: availableSize) } self.ignoreScrolling = false self.updateScrolling(transition: transition) transition.setPosition(view: self.containerView, position: CGRect(origin: CGPoint(), size: availableSize).center) transition.setBounds(view: self.containerView, bounds: CGRect(origin: CGPoint(), size: availableSize)) if let controller = environment.controller(), !controller.automaticallyControlPresentationContextLayout { let bottomInset: CGFloat = contentHeight - 12.0 let layout = ContainerViewLayout( size: availableSize, metrics: environment.metrics, deviceMetrics: environment.deviceMetrics, intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomInset, right: 0.0), safeInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0), additionalInsets: .zero, statusBarHeight: environment.statusBarHeight, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false ) controller.presentationContext.containerLayoutUpdated(layout, transition: transition.containedViewLayoutTransition) } 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 class GiftSetupScreen: ViewControllerComponentContainer { public enum Subject: Equatable { case premium(PremiumGiftProduct) case starGift(StarGift.Gift, Bool?) } private let context: AccountContext private var didPlayAppearAnimation: Bool = false private var isDismissed: Bool = false public init( context: AccountContext, peerId: EnginePeer.Id, subject: Subject, completion: (() -> Void)? = nil ) { self.context = context super.init(context: context, component: GiftSetupScreenComponent( context: context, peerId: peerId, subject: subject, completion: completion ), navigationBarAppearance: .none, theme: .default) self.statusBar.statusBarStyle = .Ignore self.navigationPresentation = .flatModal self.blocksBackgroundWhenInOverlay = true self.automaticallyControlPresentationContextLayout = false } required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { } override public func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) self.view.disablesInteractiveModalDismiss = true if !self.didPlayAppearAnimation { self.didPlayAppearAnimation = true if let componentView = self.node.hostView.componentView as? GiftSetupScreenComponent.View { componentView.animateIn() } } } override public func dismiss(completion: (() -> Void)? = nil) { if !self.isDismissed { self.isDismissed = true if let componentView = self.node.hostView.componentView as? GiftSetupScreenComponent.View { componentView.animateOut(completion: { [weak self] in completion?() self?.dismiss(animated: false) }) } else { self.dismiss(animated: false) } } } } private struct GiftConfiguration { static var defaultValue: GiftConfiguration { return GiftConfiguration(maxCaptionLength: 255) } let maxCaptionLength: Int32 fileprivate init(maxCaptionLength: Int32) { self.maxCaptionLength = maxCaptionLength } static func with(appConfiguration: AppConfiguration) -> GiftConfiguration { if let data = appConfiguration.data { var maxCaptionLength: Int32? if let value = data["stargifts_message_length_max"] as? Double { maxCaptionLength = Int32(value) } return GiftConfiguration(maxCaptionLength: maxCaptionLength ?? GiftConfiguration.defaultValue.maxCaptionLength) } else { return .defaultValue } } } public final class PremiumGiftProduct: Equatable { public let giftOption: CachedPremiumGiftOption public let starsGiftOption: CachedPremiumGiftOption? public let storeProduct: InAppPurchaseManager.Product? public let discount: Int? public var id: String { return self.storeProduct?.id ?? (self.giftOption.storeProductId ?? "") } public var months: Int32 { return self.giftOption.months } public var price: String { return self.storeProduct?.price ?? formatCurrencyAmount(self.giftOption.amount, currency: self.giftOption.currency) } public var starsPrice: Int64? { return self.starsGiftOption?.amount } public init( giftOption: CachedPremiumGiftOption, starsGiftOption: CachedPremiumGiftOption?, storeProduct: InAppPurchaseManager.Product?, discount: Int? ) { self.giftOption = giftOption self.starsGiftOption = starsGiftOption self.storeProduct = storeProduct self.discount = discount } public static func ==(lhs: PremiumGiftProduct, rhs: PremiumGiftProduct) -> Bool { if lhs.giftOption != rhs.giftOption { return false } if lhs.starsGiftOption != rhs.starsGiftOption { return false } if lhs.storeProduct != rhs.storeProduct { return false } if lhs.discount != rhs.discount { return false } return true } } func hasFirstResponder(_ view: UIView) -> Bool { if view.isFirstResponder { return true } for subview in view.subviews { if hasFirstResponder(subview) { return true } } return false }