mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-11-29 03:21:29 +00:00
2200 lines
119 KiB
Swift
2200 lines
119 KiB
Swift
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<Empty>()
|
|
|
|
private let remainingCount = ComponentView<Empty>()
|
|
private let auctionFooter = ComponentView<Empty>()
|
|
private let resaleSection = ComponentView<Empty>()
|
|
private let introContent = ComponentView<Empty>()
|
|
private let introSection = ComponentView<Empty>()
|
|
private let starsSection = ComponentView<Empty>()
|
|
private let upgradeSection = ComponentView<Empty>()
|
|
private let hideSection = ComponentView<Empty>()
|
|
|
|
private let inputPanel = ComponentView<Empty>()
|
|
private let inputPanelExternalState = MessageInputPanelComponent.ExternalState()
|
|
|
|
private let actionButton = ComponentView<Empty>()
|
|
|
|
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<ChatEntityKeyboardInputNode.InputData>()
|
|
private var previousInputHeight: CGFloat?
|
|
|
|
private var currentEmojiSuggestionView: ComponentHostView<Empty>?
|
|
|
|
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<StarGiftUpgradePreview?>(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<BotCheckoutController.InputData, SendBotPaymentFormError> in
|
|
switch error {
|
|
case .disallowedStarGifts:
|
|
return .fail(.disallowedStarGift)
|
|
case .starGiftsUserLimit:
|
|
return .fail(.starGiftUserLimit)
|
|
default:
|
|
return .fail(.generic)
|
|
}
|
|
}
|
|
|> mapToSignal { inputData -> Signal<SendBotPaymentResult, SendBotPaymentFormError> 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<ViewControllerComponentContainer.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<Empty>?
|
|
// 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<Empty>] = []
|
|
|
|
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<ViewControllerComponentContainer.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
|
|
}
|