mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
1789 lines
96 KiB
Swift
1789 lines
96 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 ListMultilineTextFieldItemComponent
|
|
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
|
|
|
|
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 {
|
|
if lhs.context !== rhs.context {
|
|
return false
|
|
}
|
|
if lhs.peerId != rhs.peerId {
|
|
return false
|
|
}
|
|
if lhs.subject != rhs.subject {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
private final class ScrollView: UIScrollView {
|
|
override func touchesShouldCancel(in view: UIView) -> Bool {
|
|
return true
|
|
}
|
|
}
|
|
|
|
final class View: UIView, UIScrollViewDelegate {
|
|
private let topOverscrollLayer = SimpleLayer()
|
|
private let scrollView: ScrollView
|
|
|
|
private let navigationTitle = ComponentView<Empty>()
|
|
private let remainingCount = 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 buttonBackground = ComponentView<Empty>()
|
|
private let buttonSeparator = SimpleLayer()
|
|
private let button = ComponentView<Empty>()
|
|
|
|
private var ignoreScrolling: Bool = false
|
|
private var isUpdating: Bool = false
|
|
|
|
private var component: GiftSetupScreenComponent?
|
|
private(set) weak var state: EmptyComponentState?
|
|
private var environment: EnvironmentType?
|
|
|
|
private let introPlaceholderTag = NSObject()
|
|
private let textInputState = ListMultilineTextFieldItemComponent.ExternalState()
|
|
private let textInputTag = NSObject()
|
|
private var resetText: String?
|
|
|
|
private var currentInputMode: ListMultilineTextFieldItemComponent.InputMode = .keyboard
|
|
|
|
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 currentEmojiSuggestionView: ComponentHostView<Empty>?
|
|
|
|
private var hideName = false
|
|
private var includeUpgrade = false
|
|
private var payWithStars = false
|
|
|
|
private var inProgress = false
|
|
|
|
|
|
private var previousHadInputHeight: Bool = false
|
|
private var previousInputHeight: CGFloat?
|
|
private var recenterOnTag: NSObject?
|
|
|
|
private var peerMap: [EnginePeer.Id: EnginePeer] = [:]
|
|
private var sendPaidMessageStars: StarsAmount?
|
|
|
|
private var starImage: (UIImage, PresentationTheme)?
|
|
|
|
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<[StarGift.UniqueGift.Attribute]?>(nil)
|
|
|
|
private var cachedChevronImage: (UIImage, PresentationTheme)?
|
|
|
|
override init(frame: CGRect) {
|
|
self.scrollView = ScrollView()
|
|
self.scrollView.showsVerticalScrollIndicator = false
|
|
self.scrollView.showsHorizontalScrollIndicator = false
|
|
self.scrollView.scrollsToTop = false
|
|
self.scrollView.delaysContentTouches = false
|
|
self.scrollView.canCancelContentTouches = true
|
|
self.scrollView.contentInsetAdjustmentBehavior = .never
|
|
if #available(iOS 13.0, *) {
|
|
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
|
|
}
|
|
self.scrollView.alwaysBounceVertical = true
|
|
|
|
super.init(frame: frame)
|
|
|
|
self.scrollView.delegate = self
|
|
self.addSubview(self.scrollView)
|
|
|
|
self.scrollView.layer.addSublayer(self.topOverscrollLayer)
|
|
|
|
self.disablesInteractiveKeyboardGestureRecognizer = true
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
deinit {
|
|
}
|
|
|
|
func scrollToTop() {
|
|
self.scrollView.setContentOffset(CGPoint(), animated: true)
|
|
}
|
|
|
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
if !self.ignoreScrolling {
|
|
self.updateScrolling(transition: .immediate)
|
|
}
|
|
}
|
|
|
|
private var scrolledUp = true
|
|
private func updateScrolling(transition: ComponentTransition) {
|
|
let navigationRevealOffsetY: CGFloat = 0.0
|
|
|
|
let navigationAlphaDistance: CGFloat = 16.0
|
|
let navigationAlpha: CGFloat = max(0.0, min(1.0, (self.scrollView.contentOffset.y - navigationRevealOffsetY) / navigationAlphaDistance))
|
|
if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar {
|
|
transition.setAlpha(layer: navigationBar.backgroundNode.layer, alpha: navigationAlpha)
|
|
transition.setAlpha(layer: navigationBar.stripeNode.layer, alpha: navigationAlpha)
|
|
}
|
|
|
|
var scrolledUp = false
|
|
if navigationAlpha < 0.5 {
|
|
scrolledUp = true
|
|
} else if navigationAlpha > 0.5 {
|
|
scrolledUp = false
|
|
}
|
|
|
|
if self.scrolledUp != scrolledUp {
|
|
self.scrolledUp = scrolledUp
|
|
if !self.isUpdating {
|
|
self.state?.updated()
|
|
}
|
|
}
|
|
|
|
if let navigationTitleView = self.navigationTitle.view {
|
|
transition.setAlpha(view: navigationTitleView, alpha: 1.0)
|
|
}
|
|
|
|
let bottomContentOffset = max(0.0, self.scrollView.contentSize.height - self.scrollView.contentOffset.y - self.scrollView.frame.height)
|
|
let bottomPanelAlpha = min(16.0, bottomContentOffset) / 16.0
|
|
self.buttonBackground.view?.alpha = bottomPanelAlpha
|
|
self.buttonSeparator.opacity = Float(bottomPanelAlpha)
|
|
}
|
|
|
|
@objc private func proceed() {
|
|
guard let component = self.component, let environment = self.environment else {
|
|
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(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")
|
|
|
|
let entities = generateChatInputTextEntities(self.textInputState.text)
|
|
let purpose: AppStoreTransactionPurpose = .giftCode(peerIds: [component.peerId], boostPeer: nil, currency: currency, amount: amount, text: self.textInputState.text.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
|
|
|
|
let entities = generateChatInputTextEntities(self.textInputState.text)
|
|
|
|
var finalPrice: Int64
|
|
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: self.textInputState.text.string, entities: entities)
|
|
} else {
|
|
fatalError()
|
|
}
|
|
case let .starGift(starGift):
|
|
finalPrice = starGift.price
|
|
if self.includeUpgrade, let upgradeStars = starGift.upgradeStars {
|
|
finalPrice += upgradeStars
|
|
}
|
|
source = .starGift(hideName: self.hideName, includeUpgrade: self.includeUpgrade, peerId: peerId, giftId: starGift.id, text: self.textInputState.text.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` { _ -> Signal<BotCheckoutController.InputData, SendBotPaymentFormError> in
|
|
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(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 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()
|
|
|
|
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
|
var errorText: String?
|
|
switch error {
|
|
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),
|
|
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() {
|
|
func hasFirstResponder(_ view: UIView) -> Bool {
|
|
if view.isFirstResponder {
|
|
return true
|
|
}
|
|
for subview in view.subviews {
|
|
if hasFirstResponder(subview) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
self.currentInputMode = .keyboard
|
|
if hasFirstResponder(self) {
|
|
if let titleView = self.introSection.findTaggedView(tag: self.textInputTag) as? ListMultilineTextFieldItemComponent.View {
|
|
if titleView.isActive {
|
|
titleView.deactivateInput()
|
|
} else {
|
|
self.endEditing(true)
|
|
}
|
|
}
|
|
} else {
|
|
self.state?.updated(transition: .spring(duration: 0.4))
|
|
}
|
|
}
|
|
|
|
func update(component: GiftSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
|
|
self.isUpdating = true
|
|
defer {
|
|
self.isUpdating = false
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
let _ = (component.context.engine.data.get(
|
|
TelegramEngine.EngineData.Item.Peer.Peer(id: component.peerId),
|
|
TelegramEngine.EngineData.Item.Peer.Peer(id: component.context.account.peerId),
|
|
TelegramEngine.EngineData.Item.Peer.SendPaidMessageStars(id: component.peerId)
|
|
)
|
|
|> deliverOnMainQueue).start(next: { [weak self] peer, accountPeer, sendPaidMessageStars in
|
|
guard let self else {
|
|
return
|
|
}
|
|
if let peer {
|
|
self.peerMap[peer.id] = peer
|
|
}
|
|
if let accountPeer {
|
|
self.peerMap[accountPeer.id] = accountPeer
|
|
}
|
|
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 = .keyboard
|
|
self.state?.updated(transition: .spring(duration: 0.4))
|
|
},
|
|
dismissTextInput: {
|
|
},
|
|
insertText: { [weak self] text in
|
|
guard let self else {
|
|
return
|
|
}
|
|
if let textInputView = self.introSection.findTaggedView(tag: self.textInputTag) as? ListMultilineTextFieldItemComponent.View {
|
|
textInputView.insertText(text: text)
|
|
}
|
|
},
|
|
backwardsDeleteText: { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
if let textInputView = self.introSection.findTaggedView(tag: self.textInputTag) as? ListMultilineTextFieldItemComponent.View {
|
|
if self.textInputState.isEditing {
|
|
textInputView.backwardsDeleteText()
|
|
}
|
|
}
|
|
},
|
|
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)
|
|
|> map(Optional.init)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
let environment = environment[EnvironmentType.self].value
|
|
let themeUpdated = self.environment?.theme !== environment.theme
|
|
self.environment = environment
|
|
|
|
self.component = component
|
|
self.state = state
|
|
|
|
if themeUpdated {
|
|
self.backgroundColor = environment.theme.list.blocksBackgroundColor
|
|
}
|
|
|
|
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
|
|
|
let navigationTitleString: String
|
|
if isSelfGift {
|
|
navigationTitleString = environment.strings.Gift_SendSelf_Title
|
|
} else if isChannelGift {
|
|
navigationTitleString = environment.strings.Gift_SendChannel_Title
|
|
} else {
|
|
navigationTitleString = environment.strings.Gift_Send_TitleTo(peerName).string
|
|
}
|
|
let navigationTitleSize = self.navigationTitle.update(
|
|
transition: transition,
|
|
component: AnyComponent(MultilineTextComponent(
|
|
text: .plain(NSAttributedString(string: navigationTitleString, font: Font.semibold(17.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)),
|
|
horizontalAlignment: .center
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width, height: 100.0)
|
|
)
|
|
let navigationTitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - navigationTitleSize.width) / 2.0), y: environment.statusBarHeight + floor((environment.navigationHeight - environment.statusBarHeight - navigationTitleSize.height) / 2.0)), size: navigationTitleSize)
|
|
if let navigationTitleView = self.navigationTitle.view {
|
|
if navigationTitleView.superview == nil {
|
|
if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar {
|
|
navigationBar.view.addSubview(navigationTitleView)
|
|
}
|
|
}
|
|
transition.setFrame(view: navigationTitleView, frame: navigationTitleFrame)
|
|
}
|
|
|
|
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
|
|
let sectionSpacing: CGFloat = 24.0
|
|
|
|
var contentHeight: CGFloat = 0.0
|
|
|
|
contentHeight += environment.navigationHeight
|
|
contentHeight += 26.0
|
|
|
|
if case let .starGift(starGift) = component.subject, let availability = starGift.availability {
|
|
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.itemBlocksSeparatorColor.withAlphaComponent(0.3),
|
|
activeColors: [UIColor(rgb: 0x5bc2ff), UIColor(rgb: 0x2d9eff)],
|
|
inactiveTitle: environment.strings.Gift_Send_Remains(remains),
|
|
inactiveValue: "",
|
|
inactiveTitleColor: environment.theme.list.itemSecondaryTextColor,
|
|
activeTitle: "",
|
|
activeValue: environment.strings.Gift_Send_Sold(sold),//totalString,
|
|
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 - 77.0), size: remainingCountSize)
|
|
if let remainingCountView = self.remainingCount.view {
|
|
if remainingCountView.superview == nil {
|
|
self.scrollView.addSubview(remainingCountView)
|
|
}
|
|
transition.setFrame(view: remainingCountView, frame: remainingCountFrame)
|
|
}
|
|
contentHeight += remainingCountSize.height
|
|
contentHeight -= 77.0
|
|
contentHeight += sectionSpacing
|
|
}
|
|
|
|
let giftConfiguration = GiftConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 })
|
|
|
|
var introSectionItems: [AnyComponentWithIdentity<Empty>] = []
|
|
introSectionItems.append(AnyComponentWithIdentity(id: introSectionItems.count, component: AnyComponent(Rectangle(color: .clear, height: 346.0, tag: self.introPlaceholderTag))))
|
|
|
|
if self.sendPaidMessageStars == nil {
|
|
introSectionItems.append(AnyComponentWithIdentity(id: introSectionItems.count, component: AnyComponent(ListMultilineTextFieldItemComponent(
|
|
externalState: self.textInputState,
|
|
context: component.context,
|
|
theme: environment.theme,
|
|
strings: environment.strings,
|
|
initialText: "",
|
|
resetText: self.resetText.flatMap {
|
|
return ListMultilineTextFieldItemComponent.ResetText(value: $0)
|
|
},
|
|
placeholder: environment.strings.Gift_Send_Customize_MessagePlaceholder,
|
|
autocapitalizationType: .sentences,
|
|
autocorrectionType: .yes,
|
|
returnKeyType: .done,
|
|
characterLimit: Int(giftConfiguration.maxCaptionLength),
|
|
displayCharacterLimit: true,
|
|
emptyLineHandling: .notAllowed,
|
|
formatMenuAvailability: .available([.bold, .italic, .underline, .strikethrough, .spoiler]),
|
|
updated: { _ in
|
|
},
|
|
returnKeyAction: { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
if let titleView = self.introSection.findTaggedView(tag: self.textInputTag) as? ListMultilineTextFieldItemComponent.View {
|
|
titleView.endEditing(true)
|
|
}
|
|
},
|
|
textUpdateTransition: .spring(duration: 0.4),
|
|
inputMode: self.currentInputMode,
|
|
toggleInputMode: { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
switch self.currentInputMode {
|
|
case .keyboard:
|
|
self.currentInputMode = .emoji
|
|
case .emoji:
|
|
self.currentInputMode = .keyboard
|
|
}
|
|
self.state?.updated(transition: .spring(duration: 0.4))
|
|
},
|
|
tag: self.textInputTag
|
|
))))
|
|
self.resetText = nil
|
|
}
|
|
|
|
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,
|
|
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.scrollView.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.textInputState.isEditing, let previousInputHeight = self.previousInputHeight {
|
|
inputHeight = previousInputHeight
|
|
} else {
|
|
inputHeight = environment.inputHeight
|
|
}
|
|
}
|
|
|
|
let listItemParams = ListViewItemLayoutParams(width: availableSize.width - sideInset * 2.0, leftInset: 0.0, rightInset: 0.0, availableHeight: 10000.0, isStandalone: true)
|
|
if let accountPeer = self.peerMap[component.context.account.peerId] {
|
|
var upgradeStars: Int64?
|
|
let subject: ChatGiftPreviewItem.Subject
|
|
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
|
|
}
|
|
|
|
var peers: [EnginePeer] = [accountPeer]
|
|
if let peer = self.peerMap[component.peerId] {
|
|
peers.append(peer)
|
|
}
|
|
|
|
let 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: self.textInputState.text.string,
|
|
entities: generateChatInputTextEntities(self.textInputState.text),
|
|
upgradeStars: self.includeUpgrade ? upgradeStars : nil,
|
|
chargeStars: self.textInputState.text.string.isEmpty ? nil : 250
|
|
),
|
|
params: listItemParams
|
|
)
|
|
),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
|
|
)
|
|
if let introContentView = self.introContent.view {
|
|
if introContentView.superview == nil {
|
|
if let placeholderView = self.introSection.findTaggedView(tag: self.introPlaceholderTag) {
|
|
placeholderView.addSubview(introContentView)
|
|
|
|
placeholderView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.previewTap)))
|
|
}
|
|
}
|
|
transition.setFrame(view: introContentView, frame: CGRect(origin: CGPoint(), size: introContentSize))
|
|
}
|
|
}
|
|
|
|
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,
|
|
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, completion: { stars in
|
|
starsContext.add(balance: StarsAmount(value: stars, nanos: 0))
|
|
})
|
|
controller.push(purchaseController)
|
|
})
|
|
}
|
|
)),
|
|
items: [
|
|
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
|
|
theme: environment.theme,
|
|
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.scrollView.addSubview(starsSectionView)
|
|
}
|
|
transition.setFrame(view: starsSectionView, frame: starsSectionFrame)
|
|
}
|
|
contentHeight += starsSectionSize.height
|
|
contentHeight += sectionSpacing
|
|
}
|
|
case let .starGift(gift):
|
|
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 {
|
|
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,
|
|
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] attributes in
|
|
guard let self, let component = self.component, let controller = self.environment?.controller(), let attributes else {
|
|
return
|
|
}
|
|
let previewController = GiftViewScreen(
|
|
context: component.context,
|
|
subject: .upgradePreview(attributes, peerName)
|
|
)
|
|
controller.push(previewController)
|
|
})
|
|
}
|
|
)),
|
|
items: [
|
|
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
|
|
theme: environment.theme,
|
|
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, action: { [weak self] _ in
|
|
guard let self 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.scrollView.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,
|
|
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,
|
|
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.scrollView.addSubview(hideSectionView)
|
|
}
|
|
transition.setFrame(view: hideSectionView, frame: hideSectionFrame)
|
|
}
|
|
contentHeight += hideSectionSize.height
|
|
}
|
|
|
|
contentHeight += 24.0
|
|
|
|
let buttonHeight: CGFloat = 50.0
|
|
let bottomPanelPadding: CGFloat = 12.0
|
|
let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding
|
|
let bottomPanelHeight = bottomPanelPadding + buttonHeight + bottomInset
|
|
|
|
let combinedBottomInset = max(inputHeight, environment.safeInsets.bottom)
|
|
contentHeight += max(bottomPanelHeight, combinedBottomInset)
|
|
|
|
if self.starImage == nil || self.starImage?.1 !== environment.theme {
|
|
self.starImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: environment.theme.list.itemCheckColors.foregroundColor)!, environment.theme)
|
|
}
|
|
|
|
let bottomPanelSize = self.buttonBackground.update(
|
|
transition: transition,
|
|
component: AnyComponent(BlurredBackgroundComponent(
|
|
color: environment.theme.rootController.tabBar.backgroundColor
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width, height: bottomPanelHeight)
|
|
)
|
|
self.buttonSeparator.backgroundColor = environment.theme.rootController.tabBar.separatorColor.cgColor
|
|
|
|
if let view = self.buttonBackground.view {
|
|
if view.superview == nil {
|
|
self.addSubview(view)
|
|
self.layer.addSublayer(self.buttonSeparator)
|
|
}
|
|
view.frame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelSize.height), size: bottomPanelSize)
|
|
self.buttonSeparator.frame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelSize.height), size: CGSize(width: availableSize.width, height: UIScreenPixel))
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
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.starImage?.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))
|
|
}
|
|
|
|
let buttonSize = self.button.update(
|
|
transition: transition,
|
|
component: AnyComponent(ButtonComponent(
|
|
background: ButtonComponent.Background(
|
|
color: environment.theme.list.itemCheckColors.fillColor,
|
|
foreground: environment.theme.list.itemCheckColors.foregroundColor,
|
|
pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9),
|
|
cornerRadius: 10.0
|
|
),
|
|
content: AnyComponentWithIdentity(
|
|
id: AnyHashable(buttonString),
|
|
component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString)))
|
|
),
|
|
isEnabled: buttonIsEnabled,
|
|
displaysProgress: self.inProgress,
|
|
action: { [weak self] in
|
|
self?.proceed()
|
|
}
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: buttonHeight)
|
|
)
|
|
if let buttonView = self.button.view {
|
|
if buttonView.superview == nil {
|
|
self.addSubview(buttonView)
|
|
}
|
|
buttonView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - buttonSize.width) / 2.0), y: availableSize.height - bottomPanelHeight + bottomPanelPadding), size: buttonSize)
|
|
}
|
|
|
|
let controller = environment.controller()
|
|
if inputHeight > 10.0 {
|
|
if self.inProgress {
|
|
let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: environment.theme.rootController.navigationBar.accentTextColor))
|
|
controller?.navigationItem.rightBarButtonItem = item
|
|
} else {
|
|
let rightBarButtonItem = UIBarButtonItem(title: environment.strings.Gift_Send_SendShort, style: .done, target: self, action: #selector(self.proceed))
|
|
rightBarButtonItem.isEnabled = buttonIsEnabled
|
|
controller?.navigationItem.setRightBarButton(rightBarButtonItem, animated: controller?.navigationItem.rightBarButtonItem == nil)
|
|
}
|
|
} else {
|
|
controller?.navigationItem.setRightBarButton(nil, animated: true)
|
|
}
|
|
|
|
if self.textInputState.isEditing, let emojiSuggestion = self.textInputState.currentEmojiSuggestion, emojiSuggestion.disposable == nil {
|
|
emojiSuggestion.disposable = (EmojiSuggestionsComponent.suggestionData(context: component.context, isSavedMessages: false, query: emojiSuggestion.position.value)
|
|
|> deliverOnMainQueue).start(next: { [weak self, weak emojiSuggestion] result in
|
|
guard let self, self.textInputState.currentEmojiSuggestion === emojiSuggestion else {
|
|
return
|
|
}
|
|
|
|
emojiSuggestion?.value = result
|
|
self.state?.updated()
|
|
})
|
|
}
|
|
|
|
var hasTrackingView = self.textInputState.hasTrackingView
|
|
if let currentEmojiSuggestion = self.textInputState.currentEmojiSuggestion, let value = currentEmojiSuggestion.value as? [TelegramMediaFile], value.isEmpty {
|
|
hasTrackingView = false
|
|
}
|
|
if !self.textInputState.isEditing {
|
|
hasTrackingView = false
|
|
}
|
|
|
|
if !hasTrackingView {
|
|
if let currentEmojiSuggestion = self.textInputState.currentEmojiSuggestion {
|
|
self.textInputState.currentEmojiSuggestion = nil
|
|
currentEmojiSuggestion.disposable?.dispose()
|
|
}
|
|
|
|
if let currentEmojiSuggestionView = self.currentEmojiSuggestionView {
|
|
self.currentEmojiSuggestionView = nil
|
|
|
|
currentEmojiSuggestionView.alpha = 0.0
|
|
currentEmojiSuggestionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak currentEmojiSuggestionView] _ in
|
|
currentEmojiSuggestionView?.removeFromSuperview()
|
|
})
|
|
}
|
|
}
|
|
|
|
if self.textInputState.isEditing, let emojiSuggestion = self.textInputState.currentEmojiSuggestion, let value = emojiSuggestion.value as? [TelegramMediaFile] {
|
|
let currentEmojiSuggestionView: ComponentHostView<Empty>
|
|
if let current = self.currentEmojiSuggestionView {
|
|
currentEmojiSuggestionView = current
|
|
} else {
|
|
currentEmojiSuggestionView = ComponentHostView<Empty>()
|
|
self.currentEmojiSuggestionView = currentEmojiSuggestionView
|
|
self.addSubview(currentEmojiSuggestionView)
|
|
|
|
currentEmojiSuggestionView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
|
}
|
|
|
|
let globalPosition: CGPoint
|
|
if let textView = (self.introSection.findTaggedView(tag: self.textInputTag) as? ListMultilineTextFieldItemComponent.View)?.textFieldView {
|
|
globalPosition = textView.convert(emojiSuggestion.localPosition, to: self)
|
|
} else {
|
|
globalPosition = .zero
|
|
}
|
|
|
|
let sideInset: CGFloat = 7.0
|
|
|
|
let viewSize = currentEmojiSuggestionView.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(EmojiSuggestionsComponent(
|
|
context: component.context,
|
|
userLocation: .other,
|
|
theme: EmojiSuggestionsComponent.Theme(theme: environment.theme, backgroundColor: environment.theme.list.itemBlocksBackgroundColor),
|
|
animationCache: component.context.animationCache,
|
|
animationRenderer: component.context.animationRenderer,
|
|
files: value,
|
|
action: { [weak self] file in
|
|
guard let self, let textView = (self.introSection.findTaggedView(tag: self.textInputTag) as? ListMultilineTextFieldItemComponent.View)?.textFieldView, let currentEmojiSuggestion = self.textInputState.currentEmojiSuggestion else {
|
|
return
|
|
}
|
|
|
|
AudioServicesPlaySystemSound(0x450)
|
|
|
|
let inputState = textView.getInputState()
|
|
let inputText = NSMutableAttributedString(attributedString: inputState.inputText)
|
|
|
|
var text: String?
|
|
var emojiAttribute: ChatTextInputTextCustomEmojiAttribute?
|
|
loop: for attribute in file.attributes {
|
|
switch attribute {
|
|
case let .CustomEmoji(_, _, displayText, _):
|
|
text = displayText
|
|
emojiAttribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: file.fileId.id, file: file)
|
|
break loop
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
if let emojiAttribute = emojiAttribute, let text = text {
|
|
let replacementText = NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: emojiAttribute])
|
|
|
|
let range = currentEmojiSuggestion.position.range
|
|
let previousText = inputText.attributedSubstring(from: range)
|
|
inputText.replaceCharacters(in: range, with: replacementText)
|
|
|
|
var replacedUpperBound = range.lowerBound
|
|
while true {
|
|
if inputText.attributedSubstring(from: NSRange(location: 0, length: replacedUpperBound)).string.hasSuffix(previousText.string) {
|
|
let replaceRange = NSRange(location: replacedUpperBound - previousText.length, length: previousText.length)
|
|
if replaceRange.location < 0 {
|
|
break
|
|
}
|
|
let adjacentString = inputText.attributedSubstring(from: replaceRange)
|
|
if adjacentString.string != previousText.string || adjacentString.attribute(ChatTextInputAttributes.customEmoji, at: 0, effectiveRange: nil) != nil {
|
|
break
|
|
}
|
|
inputText.replaceCharacters(in: replaceRange, with: NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: emojiAttribute.interactivelySelectedFromPackId, fileId: emojiAttribute.fileId, file: emojiAttribute.file)]))
|
|
replacedUpperBound = replaceRange.lowerBound
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
let selectionPosition = range.lowerBound + (replacementText.string as NSString).length
|
|
textView.updateText(inputText, selectionRange: selectionPosition ..< selectionPosition)
|
|
}
|
|
}
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
|
|
)
|
|
|
|
let viewFrame = CGRect(origin: CGPoint(x: min(availableSize.width - sideInset - viewSize.width, max(sideInset, floor(globalPosition.x - viewSize.width / 2.0))), y: globalPosition.y - 4.0 - viewSize.height), size: viewSize)
|
|
currentEmojiSuggestionView.frame = viewFrame
|
|
if let componentView = currentEmojiSuggestionView.componentView as? EmojiSuggestionsComponent.View {
|
|
componentView.adjustBackground(relativePositionX: floor(globalPosition.x + 10.0))
|
|
}
|
|
}
|
|
|
|
let previousBounds = self.scrollView.bounds
|
|
|
|
self.recenterOnTag = nil
|
|
if let hint = transition.userData(TextFieldComponent.AnimationHint.self), let targetView = hint.view {
|
|
if let textView = self.introSection.findTaggedView(tag: self.textInputTag) {
|
|
if targetView.isDescendant(of: textView) {
|
|
self.recenterOnTag = self.textInputTag
|
|
}
|
|
}
|
|
}
|
|
if self.recenterOnTag == nil && self.previousHadInputHeight != (environment.inputHeight > 0.0), case .keyboard = self.currentInputMode {
|
|
if self.textInputState.isEditing {
|
|
self.recenterOnTag = self.textInputTag
|
|
}
|
|
}
|
|
self.previousHadInputHeight = inputHeight > 0.0
|
|
self.previousInputHeight = inputHeight
|
|
|
|
self.ignoreScrolling = true
|
|
let contentSize = CGSize(width: availableSize.width, height: contentHeight)
|
|
if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) {
|
|
self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize)
|
|
}
|
|
if self.scrollView.contentSize != contentSize {
|
|
self.scrollView.contentSize = contentSize
|
|
}
|
|
let scrollInsets = UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: 0.0, right: 0.0)
|
|
if self.scrollView.scrollIndicatorInsets != scrollInsets {
|
|
self.scrollView.scrollIndicatorInsets = scrollInsets
|
|
}
|
|
|
|
if !previousBounds.isEmpty, !transition.animation.isImmediate {
|
|
let bounds = self.scrollView.bounds
|
|
if bounds.maxY != previousBounds.maxY {
|
|
let offsetY = previousBounds.maxY - bounds.maxY
|
|
transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true)
|
|
}
|
|
}
|
|
|
|
if let recenterOnTag = self.recenterOnTag {
|
|
self.recenterOnTag = nil
|
|
|
|
if let targetView = self.introSection.findTaggedView(tag: recenterOnTag) {
|
|
let caretRect = targetView.convert(targetView.bounds, to: self.scrollView)
|
|
var scrollViewBounds = self.scrollView.bounds
|
|
let minButtonDistance: CGFloat = 16.0
|
|
if -scrollViewBounds.minY + caretRect.maxY > availableSize.height - combinedBottomInset - minButtonDistance {
|
|
scrollViewBounds.origin.y = -(availableSize.height - combinedBottomInset - minButtonDistance - caretRect.maxY)
|
|
if scrollViewBounds.origin.y < 0.0 {
|
|
scrollViewBounds.origin.y = 0.0
|
|
}
|
|
}
|
|
if self.scrollView.bounds != scrollViewBounds {
|
|
transition.setBounds(view: self.scrollView, bounds: scrollViewBounds)
|
|
}
|
|
}
|
|
}
|
|
|
|
self.topOverscrollLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -3000.0), size: CGSize(width: availableSize.width, height: 3000.0))
|
|
self.ignoreScrolling = false
|
|
|
|
self.updateScrolling(transition: transition)
|
|
|
|
return availableSize
|
|
}
|
|
|
|
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,
|
|
starGiftsAvailable: false,
|
|
alwaysShowGiftButton: false
|
|
)
|
|
|
|
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 == .keyboard {
|
|
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
|
|
}
|
|
}
|
|
|
|
func makeView() -> View {
|
|
return View()
|
|
}
|
|
|
|
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
|
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
|
}
|
|
}
|
|
|
|
public final class GiftSetupScreen: ViewControllerComponentContainer {
|
|
public enum Subject: Equatable {
|
|
case premium(PremiumGiftProduct)
|
|
case starGift(StarGift.Gift)
|
|
}
|
|
|
|
private let context: AccountContext
|
|
|
|
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: .default, theme: .default, updatedPresentationData: nil)
|
|
|
|
self.title = ""
|
|
|
|
self.scrollToTop = { [weak self] in
|
|
guard let self, let componentView = self.node.hostView.componentView as? GiftSetupScreenComponent.View else {
|
|
return
|
|
}
|
|
componentView.scrollToTop()
|
|
}
|
|
}
|
|
|
|
required public init(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
@objc private func cancelPressed() {
|
|
self.dismiss()
|
|
}
|
|
|
|
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
|
super.containerLayoutUpdated(layout, transition: transition)
|
|
}
|
|
}
|
|
|
|
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 struct 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
|
|
}
|
|
}
|