Ilya Laktyushin e5762bd9c8 Various fixes
2025-05-18 04:35:56 +04:00

4442 lines
235 KiB
Swift

import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SwiftSignalKit
import AccountContext
import TelegramPresentationData
import PresentationDataUtils
import ComponentFlow
import ViewControllerComponent
import SheetComponent
import MultilineTextComponent
import MultilineTextWithEntitiesComponent
import BundleIconComponent
import ButtonComponent
import Markdown
import BalancedTextComponent
import AvatarNode
import TextFormat
import TelegramStringFormatting
import StarsAvatarComponent
import EmojiTextAttachmentView
import EmojiStatusComponent
import UndoUI
import ConfettiEffect
import PlainButtonComponent
import CheckComponent
import TooltipUI
import GiftAnimationComponent
import LottieComponent
import ContextUI
import TelegramNotices
import PremiumLockButtonSubtitleComponent
import StarsBalanceOverlayComponent
private final class GiftViewSheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let subject: GiftViewScreen.Subject
let animateOut: ActionSlot<Action<()>>
let getController: () -> ViewController?
init(
context: AccountContext,
subject: GiftViewScreen.Subject,
animateOut: ActionSlot<Action<()>>,
getController: @escaping () -> ViewController?
) {
self.context = context
self.subject = subject
self.animateOut = animateOut
self.getController = getController
}
static func ==(lhs: GiftViewSheetContent, rhs: GiftViewSheetContent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.subject != rhs.subject {
return false
}
return true
}
final class State: ComponentState {
let modelButtonTag = GenericComponentViewTag()
let backdropButtonTag = GenericComponentViewTag()
let symbolButtonTag = GenericComponentViewTag()
let statusTag = GenericComponentViewTag()
private let context: AccountContext
private(set) var subject: GiftViewScreen.Subject
private let getController: () -> ViewController?
private var disposable: Disposable?
var initialized = false
var recipientPeerIdPromise = ValuePromise<EnginePeer.Id?>(nil)
var recipientPeerId: EnginePeer.Id? {
didSet {
self.recipientPeerIdPromise.set(self.recipientPeerId)
}
}
var peerMap: [EnginePeer.Id: EnginePeer] = [:]
var starGiftsMap: [Int64: StarGift.Gift] = [:]
var cachedCircleImage: UIImage?
var cachedStarImage: (UIImage, PresentationTheme)?
var cachedSmallStarImage: (UIImage, PresentationTheme)?
var cachedChevronImage: (UIImage, PresentationTheme)?
var cachedSmallChevronImage: (UIImage, PresentationTheme)?
var inProgress = false
var inUpgradePreview = false
var upgradeForm: BotPaymentForm?
var upgradeFormDisposable: Disposable?
var upgradeDisposable: Disposable?
let levelsDisposable = MetaDisposable()
var buyForm: BotPaymentForm?
var buyFormDisposable: Disposable?
var buyDisposable: Disposable?
var resellTooEarlyTimestamp: Int32?
var inWearPreview = false
var pendingWear = false
var pendingTakeOff = false
var sampleGiftAttributes: [StarGift.UniqueGift.Attribute]?
let sampleDisposable = DisposableSet()
var keepOriginalInfo = false
private var optionsDisposable: Disposable?
private(set) var options: [StarsTopUpOption] = [] {
didSet {
self.optionsPromise.set(self.options)
}
}
private let optionsPromise = ValuePromise<[StarsTopUpOption]?>(nil)
private let animateOut: ActionSlot<Action<()>>
init(
context: AccountContext,
subject: GiftViewScreen.Subject,
animateOut: ActionSlot<Action<()>>,
getController: @escaping () -> ViewController?
) {
self.context = context
self.subject = subject
self.animateOut = animateOut
self.getController = getController
super.init()
if let arguments = subject.arguments {
if let upgradeStars = arguments.upgradeStars, upgradeStars > 0, !arguments.nameHidden {
self.keepOriginalInfo = true
}
var peerIds: [EnginePeer.Id] = [context.account.peerId]
if let peerId = arguments.peerId {
peerIds.append(peerId)
}
if let fromPeerId = arguments.fromPeerId, !peerIds.contains(fromPeerId) {
peerIds.append(fromPeerId)
}
if case let .message(message) = subject {
for media in message.media {
peerIds.append(contentsOf: media.peerIds)
}
}
if case let .unique(gift) = arguments.gift {
if case let .peerId(peerId) = gift.owner {
peerIds.append(peerId)
}
for attribute in gift.attributes {
if case let .originalInfo(senderPeerId, recipientPeerId, _, _, _) = attribute {
if let senderPeerId {
peerIds.append(senderPeerId)
}
peerIds.append(recipientPeerId)
break
}
}
if let _ = arguments.resellStars {
self.buyFormDisposable = (context.engine.payments.fetchBotPaymentForm(source: .starGiftResale(slug: gift.slug, toPeerId: context.account.peerId), themeParams: nil)
|> deliverOnMainQueue).start(next: { [weak self] paymentForm in
guard let self else {
return
}
self.buyForm = paymentForm
self.updated()
}, error: { [weak self] error in
guard let self else {
return
}
if case let .starGiftResellTooEarly(remaining) = error {
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
self.resellTooEarlyTimestamp = currentTime + remaining
}
})
}
} else if case let .generic(gift) = arguments.gift {
if arguments.canUpgrade || arguments.upgradeStars != nil {
self.sampleDisposable.add((context.engine.payments.starGiftUpgradePreview(giftId: gift.id)
|> deliverOnMainQueue).start(next: { [weak self] attributes in
guard let self else {
return
}
self.sampleGiftAttributes = attributes
for attribute in attributes {
switch attribute {
case let .model(_, file, _):
self.sampleDisposable.add(freeMediaFileResourceInteractiveFetched(account: self.context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start())
case let .pattern(_, file, _):
self.sampleDisposable.add(freeMediaFileResourceInteractiveFetched(account: self.context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start())
default:
break
}
}
self.updated()
}))
if arguments.upgradeStars == nil, let reference = arguments.reference {
self.upgradeFormDisposable = (context.engine.payments.fetchBotPaymentForm(source: .starGiftUpgrade(keepOriginalInfo: false, reference: reference), themeParams: nil)
|> deliverOnMainQueue).start(next: { [weak self] paymentForm in
guard let self else {
return
}
self.upgradeForm = paymentForm
self.updated()
})
}
}
}
let peerIdsSignal: Signal<[EnginePeer.Id], NoError>
if case let .uniqueGift(_, recipientPeerIdValue) = subject, let recipientPeerIdValue {
self.recipientPeerId = recipientPeerIdValue
self.recipientPeerIdPromise.set(recipientPeerIdValue)
peerIdsSignal = self.recipientPeerIdPromise.get()
|> map { recipientPeerId in
var peerIds = peerIds
if let recipientPeerId {
peerIds.append(recipientPeerId)
}
return peerIds
}
} else {
peerIdsSignal = .single(peerIds)
}
self.disposable = combineLatest(queue: Queue.mainQueue(),
peerIdsSignal
|> distinctUntilChanged
|> mapToSignal { peerIds in
return context.engine.data.get(EngineDataMap(
peerIds.map { peerId -> TelegramEngine.EngineData.Item.Peer.Peer in
return TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)
}
))
},
.single(nil) |> then(context.engine.payments.cachedStarGifts())
).startStrict(next: { [weak self] peers, starGifts in
if let strongSelf = self {
var peersMap: [EnginePeer.Id: EnginePeer] = [:]
for (peerId, maybePeer) in peers {
if let peer = maybePeer {
peersMap[peerId] = peer
}
}
strongSelf.peerMap = peersMap
var starGiftsMap: [Int64: StarGift.Gift] = [:]
if let starGifts {
for gift in starGifts {
if case let .generic(gift) = gift {
starGiftsMap[gift.id] = gift
}
}
}
strongSelf.starGiftsMap = starGiftsMap
strongSelf.initialized = true
strongSelf.updated(transition: .immediate)
}
})
}
var minRequiredAmount = StarsAmount(value: 100, nanos: 0)
if let resellStars = self.subject.arguments?.resellStars {
minRequiredAmount = StarsAmount(value: resellStars, nanos: 0)
}
if let starsContext = context.starsContext, let state = starsContext.currentState, state.balance < minRequiredAmount {
self.optionsDisposable = (context.engine.payments.starsTopUpOptions()
|> deliverOnMainQueue).start(next: { [weak self] options in
guard let self else {
return
}
self.options = options
})
}
}
deinit {
self.disposable?.dispose()
self.sampleDisposable.dispose()
self.upgradeFormDisposable?.dispose()
self.upgradeDisposable?.dispose()
self.buyFormDisposable?.dispose()
self.buyDisposable?.dispose()
self.levelsDisposable.dispose()
self.optionsDisposable?.dispose()
}
func openPeer(_ peer: EnginePeer, gifts: Bool = false, dismiss: Bool = true) {
guard let controller = self.getController() as? GiftViewScreen, let navigationController = controller.navigationController as? NavigationController else {
return
}
controller.dismissAllTooltips()
let context = self.context
let action = {
if gifts {
let profileGifts = ProfileGiftsContext(account: context.account, peerId: peer.id)
let _ = (profileGifts.state
|> filter { state in
if case .ready = state.dataState {
return true
}
return false
}
|> take(1)
|> deliverOnMainQueue).start(next: { [weak navigationController] _ in
if let profileController = context.sharedContext.makePeerInfoController(
context: context,
updatedPresentationData: nil,
peer: peer._asPeer(),
mode: peer.id == context.account.peerId ? .myProfileGifts : .gifts,
avatarInitiallyExpanded: false,
fromChat: false,
requestsContext: nil
) {
navigationController?.pushViewController(profileController)
}
let _ = profileGifts
})
} else {
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(
navigationController: navigationController,
chatController: nil,
context: context,
chatLocation: .peer(peer),
subject: nil,
botStart: nil,
updateTextInputState: nil,
keepStack: .always,
useExisting: true,
purposefulAction: nil,
scrollToEndIfExists: false,
activateMessageSearch: nil,
animated: true
))
}
}
if dismiss {
self.dismiss(animated: true)
Queue.mainQueue().after(0.4, {
action()
})
} else {
action()
}
}
func openAddress(_ address: String) {
guard let controller = self.getController() as? GiftViewScreen, let navigationController = controller.navigationController as? NavigationController else {
return
}
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let configuration = GiftViewConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
let url = configuration.explorerUrl + address
Queue.mainQueue().after(0.3) {
self.context.sharedContext.openExternalUrl(
context: self.context,
urlContext: .generic,
url: url,
forceExternal: false,
presentationData: presentationData,
navigationController: navigationController,
dismissInput: {}
)
}
self.dismiss(animated: true)
}
func copyAddress(_ address: String) {
guard let controller = self.getController() as? GiftViewScreen else {
return
}
UIPasteboard.general.string = address
controller.dismissAllTooltips()
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
controller.present(
UndoOverlayController(
presentationData: presentationData,
content: .copy(text: presentationData.strings.Gift_View_CopiedAddress),
elevatedLayout: false,
position: .bottom,
action: { _ in return true }
),
in: .current
)
HapticFeedback().tap()
}
func updateSavedToProfile(_ added: Bool) {
guard let controller = self.getController() as? GiftViewScreen, let arguments = self.subject.arguments, let reference = arguments.reference else {
return
}
var animationFile: TelegramMediaFile?
switch arguments.gift {
case let .generic(gift):
animationFile = gift.file
case let .unique(gift):
for attribute in gift.attributes {
if case let .model(_, file, _) = attribute {
animationFile = file
break
}
}
}
if let updateSavedToProfile = controller.updateSavedToProfile {
updateSavedToProfile(reference, added)
} else {
let _ = (self.context.engine.payments.updateStarGiftAddedToProfile(reference: reference, added: added)
|> deliverOnMainQueue).startStandalone()
}
controller.dismissAnimated()
let giftsPeerId: EnginePeer.Id?
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let text: String
if case let .peer(peerId, _) = arguments.reference, peerId.namespace == Namespaces.Peer.CloudChannel {
giftsPeerId = peerId
text = added ? presentationData.strings.Gift_Displayed_ChannelText : presentationData.strings.Gift_Hidden_ChannelText
} else {
giftsPeerId = context.account.peerId
text = added ? presentationData.strings.Gift_Displayed_NewText : presentationData.strings.Gift_Hidden_NewText
}
if let navigationController = controller.navigationController as? NavigationController {
Queue.mainQueue().after(0.5) {
if let lastController = navigationController.viewControllers.last as? ViewController, let animationFile {
let resultController = UndoOverlayController(
presentationData: presentationData,
content: .sticker(
context: self.context,
file: animationFile,
loop: false,
title: nil,
text: text,
undoText: presentationData.strings.Gift_Displayed_View,
customAction: nil
),
elevatedLayout: lastController is ChatController,
action: { [weak navigationController] action in
if case .undo = action, let navigationController, let giftsPeerId {
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: giftsPeerId))
|> deliverOnMainQueue).start(next: { [weak navigationController] peer in
guard let peer, let navigationController else {
return
}
if let controller = self.context.sharedContext.makePeerInfoController(
context: self.context,
updatedPresentationData: nil,
peer: peer._asPeer(),
mode: giftsPeerId == self.context.account.peerId ? .myProfileGifts : .gifts,
avatarInitiallyExpanded: false,
fromChat: false,
requestsContext: nil
) {
navigationController.pushViewController(controller, animated: true)
}
})
}
return true
}
)
lastController.present(resultController, in: .window(.root))
}
}
}
}
func convertToStars() {
guard let controller = self.getController() as? GiftViewScreen, let starsContext = context.starsContext, let arguments = self.subject.arguments, let reference = arguments.reference, let fromPeerName = arguments.fromPeerName, let convertStars = arguments.convertStars, let navigationController = controller.navigationController as? NavigationController else {
return
}
let configuration = GiftConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
let starsConvertMaxDate = arguments.date + configuration.convertToStarsPeriod
var isChannelGift = false
if case let .peer(peerId, _) = reference, peerId.namespace == Namespaces.Peer.CloudChannel {
isChannelGift = true
}
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
if currentTime > starsConvertMaxDate {
let days: Int32 = Int32(ceil(Float(configuration.convertToStarsPeriod) / 86400.0))
let alertController = textAlertController(
context: self.context,
title: presentationData.strings.Gift_Convert_Title,
text: presentationData.strings.Gift_Convert_Period_Unavailable_Text(presentationData.strings.Gift_Convert_Period_Unavailable_Days(days)).string,
actions: [
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})
],
parseMarkdown: true
)
controller.present(alertController, in: .window(.root))
} else {
let delta = starsConvertMaxDate - currentTime
let days: Int32 = Int32(ceil(Float(delta) / 86400.0))
let text = presentationData.strings.Gift_Convert_Period_Text(
fromPeerName,
presentationData.strings.Gift_Convert_Period_Stars(Int32(convertStars)),
presentationData.strings.Gift_Convert_Period_Days(days)
).string
let alertController = textAlertController(
context: self.context,
title: presentationData.strings.Gift_Convert_Title,
text: text,
actions: [
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}),
TextAlertAction(type: .defaultAction, title: presentationData.strings.Gift_Convert_Convert, action: { [weak self, weak controller, weak navigationController] in
guard let self else {
return
}
if let convertToStars = controller?.convertToStars {
convertToStars()
} else {
let _ = (self.context.engine.payments.convertStarGift(reference: reference)
|> deliverOnMainQueue).startStandalone()
}
controller?.dismissAnimated()
if let navigationController {
Queue.mainQueue().after(0.5) {
starsContext.load(force: true)
let text: String
if isChannelGift {
text = presentationData.strings.Gift_Convert_Success_ChannelText(
presentationData.strings.Gift_Convert_Success_ChannelText_Stars(Int32(convertStars))
).string
} else {
text = presentationData.strings.Gift_Convert_Success_Text(
presentationData.strings.Gift_Convert_Success_Text_Stars(Int32(convertStars))
).string
if let starsContext = self.context.starsContext {
navigationController.pushViewController(
self.context.sharedContext.makeStarsTransactionsScreen(
context: self.context,
starsContext: starsContext
),
animated: true
)
}
}
if let lastController = navigationController.viewControllers.last as? ViewController {
let resultController = UndoOverlayController(
presentationData: presentationData,
content: .universal(
animation: "StarsBuy",
scale: 0.066,
colors: [:],
title: presentationData.strings.Gift_Convert_Success_Title,
text: text,
customUndoText: nil,
timeout: nil
),
elevatedLayout: lastController is ChatController,
action: { _ in return true }
)
lastController.present(resultController, in: .window(.root))
}
}
}
})
],
parseMarkdown: true
)
controller.present(alertController, in: .window(.root))
}
}
func openStarsIntro() {
guard let controller = self.getController() else {
return
}
let introController = self.context.sharedContext.makeStarsIntroScreen(context: self.context)
controller.push(introController)
}
func sendGift(peerId: EnginePeer.Id) {
guard let controller = self.getController() else {
return
}
let _ = (self.context.engine.payments.premiumGiftCodeOptions(peerId: nil, onlyCached: true)
|> filter { !$0.isEmpty }
|> deliverOnMainQueue).start(next: { [weak self, weak controller] giftOptions in
guard let self, let controller else {
return
}
let premiumOptions = giftOptions.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) }
let giftController = self.context.sharedContext.makeGiftOptionsController(context: self.context, peerId: peerId, premiumOptions: premiumOptions, hasBirthday: false, completion: nil)
controller.push(giftController)
})
Queue.mainQueue().after(0.6, {
self.dismiss(animated: false)
})
}
func shareGift() {
guard let arguments = self.subject.arguments, case let .unique(gift) = arguments.gift, let controller = self.getController() as? GiftViewScreen else {
return
}
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
var shareStoryImpl: (() -> Void)?
if let shareStory = controller.shareStory {
shareStoryImpl = {
shareStory(gift)
}
}
let link = "https://t.me/nft/\(gift.slug)"
let shareController = self.context.sharedContext.makeShareController(
context: self.context,
subject: .url(link),
forceExternal: false,
shareStory: shareStoryImpl,
enqueued: { [weak self, weak controller] peerIds, _ in
guard let self else {
return
}
let _ = (self.context.engine.data.get(
EngineDataList(
peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init)
)
)
|> deliverOnMainQueue).startStandalone(next: { [weak self, weak controller] peerList in
guard let self else {
return
}
let peers = peerList.compactMap { $0 }
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let text: String
var savedMessages = false
if peerIds.count == 1, let peerId = peerIds.first, peerId == context.account.peerId {
text = presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One
savedMessages = true
} else {
if peers.count == 1, let peer = peers.first {
var peerName = peer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
peerName = peerName.replacingOccurrences(of: "**", with: "")
text = presentationData.strings.Conversation_ForwardTooltip_Chat_One(peerName).string
} else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last {
var firstPeerName = firstPeer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
firstPeerName = firstPeerName.replacingOccurrences(of: "**", with: "")
var secondPeerName = secondPeer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
secondPeerName = secondPeerName.replacingOccurrences(of: "**", with: "")
text = presentationData.strings.Conversation_ForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string
} else if let peer = peers.first {
var peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
peerName = peerName.replacingOccurrences(of: "**", with: "")
text = presentationData.strings.Conversation_ForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string
} else {
text = ""
}
}
controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: false, action: { [weak self, weak controller] action in
if let self, savedMessages, action == .info {
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId))
|> deliverOnMainQueue).start(next: { [weak self, weak controller] peer in
guard let peer else {
return
}
self?.openPeer(peer)
Queue.mainQueue().after(0.6) {
controller?.dismiss(animated: false, completion: nil)
}
})
}
return false
}, additionalView: nil), in: .current)
})
},
actionCompleted: { [weak controller] in
controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current)
}
)
controller.present(shareController, in: .window(.root))
}
func transferGift() {
guard let arguments = self.subject.arguments, let controller = self.getController() as? GiftViewScreen, case let .unique(gift) = arguments.gift, let reference = arguments.reference, let transferStars = arguments.transferStars else {
return
}
controller.dismissAllTooltips()
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
if let canTransferDate = arguments.canTransferDate, currentTime < canTransferDate {
let dateString = stringForFullDate(timestamp: canTransferDate, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat)
let alertController = textAlertController(
context: self.context,
title: presentationData.strings.Gift_Transfer_Unavailable_Title,
text: presentationData.strings.Gift_Transfer_Unavailable_Text(dateString).string,
actions: [
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})
],
parseMarkdown: true
)
controller.present(alertController, in: .window(.root))
return
}
let context = self.context
let _ = (self.context.account.stateManager.contactBirthdays
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self, weak controller] birthdays in
guard let self, let controller else {
return
}
var showSelf = false
if arguments.peerId?.namespace == Namespaces.Peer.CloudChannel {
showSelf = true
}
let tranfserGiftImpl = controller.transferGift
let transferController = self.context.sharedContext.makePremiumGiftController(context: context, source: .starGiftTransfer(birthdays, reference, gift, transferStars, arguments.canExportDate, showSelf), completion: { peerIds in
guard let peerId = peerIds.first else {
return .complete()
}
Queue.mainQueue().after(1.5, {
if transferStars > 0 {
context.starsContext?.load(force: true)
}
})
if let tranfserGiftImpl {
return tranfserGiftImpl(transferStars == 0, peerId)
} else {
return (context.engine.payments.transferStarGift(prepaid: transferStars == 0, reference: reference, peerId: peerId)
|> deliverOnMainQueue)
}
})
controller.push(transferController)
})
}
func resellGift(update: Bool = false) {
guard let arguments = self.subject.arguments, case let .unique(gift) = arguments.gift, let controller = self.getController() as? GiftViewScreen else {
return
}
controller.dismissAllTooltips()
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
if let canResaleDate = arguments.canResaleDate, currentTime < canResaleDate {
let dateString = stringForFullDate(timestamp: canResaleDate, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat)
let alertController = textAlertController(
context: self.context,
title: presentationData.strings.Gift_Resale_Unavailable_Title,
text: presentationData.strings.Gift_Resale_Unavailable_Text(dateString).string,
actions: [
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})
],
parseMarkdown: true
)
controller.present(alertController, in: .window(.root))
return
}
let giftTitle = "\(gift.title) #\(presentationStringsFormattedNumber(gift.number, presentationData.dateTimeFormat.groupingSeparator))"
let reference = arguments.reference ?? .slug(slug: gift.slug)
if let resellStars = gift.resellStars, resellStars > 0, !update {
let alertController = textAlertController(
context: context,
title: presentationData.strings.Gift_View_Resale_Unlist_Title,
text: presentationData.strings.Gift_View_Resale_Unlist_Text,
actions: [
TextAlertAction(type: .defaultAction, title: presentationData.strings.Gift_View_Resale_Unlist_Unlist, action: { [weak self, weak controller] in
guard let self, let controller else {
return
}
let _ = ((controller.updateResellStars?(nil) ?? context.engine.payments.updateStarGiftResalePrice(reference: reference, price: nil))
|> deliverOnMainQueue).startStandalone(error: { error in
}, completed: { [weak self, weak controller] in
guard let self, let controller else {
return
}
switch self.subject {
case let .profileGift(peerId, currentSubject):
self.subject = .profileGift(peerId, currentSubject.withGift(.unique(gift.withResellStars(nil))))
case let .uniqueGift(_, recipientPeerId):
self.subject = .uniqueGift(gift.withResellStars(nil), recipientPeerId)
default:
break
}
self.updated(transition: .easeInOut(duration: 0.2))
let text = presentationData.strings.Gift_View_Resale_Unlist_Success(giftTitle).string
let tooltipController = UndoOverlayController(
presentationData: presentationData,
content: .universalImage(
image: generateTintedImage(image: UIImage(bundleImageName: "Premium/Collectible/Unlist"), color: .white)!,
size: nil,
title: nil,
text: text,
customUndoText: nil,
timeout: 3.0
),
position: .bottom,
animateInAsReplacement: false,
appearance: UndoOverlayController.Appearance(sideInset: 16.0, bottomInset: 62.0),
action: { action in
return false
}
)
controller.present(tooltipController, in: .window(.root))
})
}),
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
})
],
actionLayout: .vertical
)
controller.present(alertController, in: .window(.root))
} else {
let resellController = self.context.sharedContext.makeStarGiftResellScreen(context: self.context, update: update, completion: { [weak self, weak controller] price in
guard let self, let controller else {
return
}
let _ = ((controller.updateResellStars?(price) ?? context.engine.payments.updateStarGiftResalePrice(reference: reference, price: price))
|> deliverOnMainQueue).startStandalone(error: { [weak self, weak controller] error in
guard let self else {
return
}
let title: String?
let text: String
switch error {
case .generic:
title = nil
text = presentationData.strings.Gift_Send_ErrorUnknown
case let .starGiftResellTooEarly(canResaleDate):
let dateString = stringForFullDate(timestamp: currentTime + canResaleDate, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat)
title = presentationData.strings.Gift_Resale_Unavailable_Title
text = presentationData.strings.Gift_Resale_Unavailable_Text(dateString).string
}
let alertController = textAlertController(
context: self.context,
title: title,
text: text,
actions: [
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})
],
parseMarkdown: true
)
controller?.present(alertController, in: .window(.root))
}, completed: { [weak self, weak controller] in
guard let self, let controller else {
return
}
switch self.subject {
case let .profileGift(peerId, currentSubject):
self.subject = .profileGift(peerId, currentSubject.withGift(.unique(gift.withResellStars(price))))
case let .uniqueGift(_, recipientPeerId):
self.subject = .uniqueGift(gift.withResellStars(price), recipientPeerId)
default:
break
}
self.updated(transition: .easeInOut(duration: 0.2))
var text = presentationData.strings.Gift_View_Resale_List_Success(giftTitle).string
if update {
let starsString = presentationData.strings.Gift_View_Resale_Relist_Success_Stars(Int32(price))
text = presentationData.strings.Gift_View_Resale_Relist_Success(giftTitle, starsString).string
}
let tooltipController = UndoOverlayController(
presentationData: presentationData,
content: .universalImage(
image: generateTintedImage(image: UIImage(bundleImageName: "Premium/Collectible/Sell"), color: .white)!,
size: nil,
title: nil,
text: text,
customUndoText: nil,
timeout: 3.0
),
position: .bottom,
animateInAsReplacement: false,
appearance: UndoOverlayController.Appearance(sideInset: 16.0, bottomInset: 62.0),
action: { action in
return false
}
)
controller.present(tooltipController, in: .window(.root))
})
})
controller.push(resellController)
}
}
func viewUpgradedGift(messageId: EngineMessage.Id) {
guard let controller = self.getController(), let navigationController = controller.navigationController as? NavigationController else {
return
}
let _ = (self.context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: messageId.peerId)
)
|> deliverOnMainQueue).start(next: { [weak self, weak navigationController] peer in
guard let self, let navigationController, let peer else {
return
}
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false), keepStack: .always, useExisting: true, purposefulAction: {}, peekData: nil, forceAnimatedScroll: true))
})
}
func showAttributeInfo(tag: Any, text: String) {
guard let controller = self.getController() as? GiftViewScreen else {
return
}
controller.dismissAllTooltips()
guard let sourceView = controller.node.hostView.findTaggedView(tag: tag), let absoluteLocation = sourceView.superview?.convert(sourceView.center, to: controller.view) else {
return
}
let location = CGRect(origin: CGPoint(x: absoluteLocation.x, y: absoluteLocation.y - 12.0), size: CGSize())
let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: .plain(text: text), style: .wide, location: .point(location, .bottom), displayDuration: .default, inset: 16.0, shouldDismissOnTouch: { _, _ in
return .dismiss(consume: false)
})
controller.present(tooltipController, in: .current)
}
func openMore(node: ASDisplayNode, gesture: ContextGesture?) {
guard let arguments = self.subject.arguments, case let .unique(gift) = arguments.gift else {
return
}
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let link = "https://t.me/nft/\(gift.slug)"
let _ = (self.context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: arguments.peerId ?? context.account.peerId)
)
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let self, let controller = self.getController() as? GiftViewScreen else {
return
}
var items: [ContextMenuItem] = []
let strings = presentationData.strings
if let _ = arguments.reference, case .unique = arguments.gift, let togglePinnedToTop = controller.togglePinnedToTop, let pinnedToTop = arguments.pinnedToTop {
items.append(.action(ContextMenuActionItem(text: pinnedToTop ? strings.PeerInfo_Gifts_Context_Unpin : strings.PeerInfo_Gifts_Context_Pin , icon: { theme in generateTintedImage(image: UIImage(bundleImageName: pinnedToTop ? "Chat/Context Menu/Unpin" : "Chat/Context Menu/Pin"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, f in
c?.dismiss(completion: { [weak self, weak controller] in
guard let self, let controller else {
return
}
let pinnedToTop = !pinnedToTop
if togglePinnedToTop(pinnedToTop) {
if pinnedToTop {
controller.dismissAnimated()
} else {
let toastText = strings.PeerInfo_Gifts_ToastUnpinned_Text
controller.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_toastunpin", scale: 0.06, colors: [:], title: nil, text: toastText, customUndoText: nil, timeout: 5), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current)
if case let .profileGift(peerId, gift) = self.subject {
self.subject = .profileGift(peerId, gift.withPinnedToTop(false))
}
}
}
})
})))
}
if case let .unique(gift) = arguments.gift, let resellStars = gift.resellStars, resellStars > 0 {
if arguments.reference != nil || gift.owner.peerId == context.account.peerId {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_View_Context_ChangePrice, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/PriceTag"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] c, _ in
c?.dismiss(completion: nil)
self?.resellGift(update: true)
})))
}
}
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_View_Context_CopyLink, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor)
}, action: { [weak controller] c, _ in
c?.dismiss(completion: nil)
UIPasteboard.general.string = link
controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current)
})))
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_View_Context_Share, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] c, _ in
c?.dismiss(completion: nil)
self?.shareGift()
})))
if let _ = arguments.transferStars {
if case let .channel(channel) = peer, !channel.flags.contains(.isCreator) {
} else {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_View_Context_Transfer, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Replace"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] c, _ in
c?.dismiss(completion: nil)
self?.transferGift()
})))
}
}
if let _ = arguments.resellStars, case let .uniqueGift(uniqueGift, recipientPeerId) = subject, let _ = recipientPeerId {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_View_Context_ViewInProfile, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/ShowIcon"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] c, _ in
c?.dismiss(completion: nil)
guard let self, case let .peerId(peerId) = uniqueGift.owner else {
return
}
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let self, let peer else {
return
}
self.openPeer(peer, gifts: true)
Queue.mainQueue().after(0.6) {
controller.dismiss(animated: false, completion: nil)
}
})
})))
}
let contextController = ContextController(presentationData: presentationData, source: .reference(GiftViewContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
controller.presentInGlobalOverlay(contextController)
})
}
func dismiss(animated: Bool) {
guard let controller = self.getController() as? GiftViewScreen else {
return
}
if animated {
controller.dismissAllTooltips()
controller.dismissBalanceOverlay()
self.animateOut.invoke(Action { [weak controller] _ in
controller?.dismiss(completion: nil)
})
} else {
controller.dismiss(animated: false)
}
}
func requestWearPreview() {
self.inWearPreview = true
self.updated(transition: .spring(duration: 0.4))
}
func commitWear(_ uniqueGift: StarGift.UniqueGift) {
self.pendingWear = true
self.pendingTakeOff = false
self.inWearPreview = false
self.updated(transition: .spring(duration: 0.4))
if let arguments = self.subject.arguments, let peerId = arguments.peerId, peerId.namespace == Namespaces.Peer.CloudChannel {
let _ = self.context.engine.peers.updatePeerStarGiftStatus(peerId: peerId, starGift: uniqueGift, expirationDate: nil).startStandalone()
} else {
let _ = self.context.engine.accountData.setStarGiftStatus(starGift: uniqueGift, expirationDate: nil).startStandalone()
}
let _ = ApplicationSpecificNotice.incrementStarGiftWearTips(accountManager: self.context.sharedContext.accountManager).startStandalone()
}
func commitTakeOff() {
self.pendingTakeOff = true
self.pendingWear = false
self.updated(transition: .spring(duration: 0.4))
if let arguments = self.subject.arguments, let peerId = arguments.peerId, peerId.namespace == Namespaces.Peer.CloudChannel {
let _ = self.context.engine.peers.updatePeerEmojiStatus(peerId: peerId, fileId: nil, expirationDate: nil).startStandalone()
} else {
let _ = self.context.engine.accountData.setEmojiStatus(file: nil, expirationDate: nil).startStandalone()
}
}
func requestUpgradePreview() {
guard let arguments = self.subject.arguments, arguments.canUpgrade || arguments.upgradeStars != nil else {
return
}
self.context.starsContext?.load(force: false)
self.inUpgradePreview = true
self.updated(transition: .spring(duration: 0.4))
if let controller = self.getController() as? GiftViewScreen {
controller.showBalance = true
}
}
func cancelUpgradePreview() {
self.inUpgradePreview = false
self.updated(transition: .spring(duration: 0.4))
if let controller = self.getController() as? GiftViewScreen {
controller.showBalance = false
}
}
func commitBuy(acceptedPrice: Int64? = nil, skipConfirmation: Bool = false) {
guard let resellStars = self.subject.arguments?.resellStars, let starsContext = self.context.starsContext, let starsState = starsContext.currentState, case let .unique(uniqueGift) = self.subject.arguments?.gift else {
return
}
let context = self.context
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
if let resellTooEarlyTimestamp = self.resellTooEarlyTimestamp {
guard let controller = self.getController() else {
return
}
let dateString = stringForFullDate(timestamp: resellTooEarlyTimestamp, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat)
let alertController = textAlertController(
context: context,
title: presentationData.strings.Gift_Buy_ErrorTooEarly_Title,
text: presentationData.strings.Gift_Buy_ErrorTooEarly_Text(dateString).string,
actions: [
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})
],
parseMarkdown: true
)
controller.present(alertController, in: .window(.root))
return
}
let giftTitle = "\(uniqueGift.title) #\(uniqueGift.number)"
let recipientPeerId = self.recipientPeerId ?? self.context.account.peerId
let action = {
let proceed: () -> Void = {
guard let controller = self.getController() as? GiftViewScreen else {
return
}
self.inProgress = true
self.updated()
let buyGiftImpl: ((String, EnginePeer.Id, Int64?) -> Signal<Never, BuyStarGiftError>)
if let buyGift = controller.buyGift {
buyGiftImpl = { slug, peerId, price in
return buyGift(slug, peerId, price)
|> afterCompleted {
context.starsContext?.load(force: true)
}
}
} else {
buyGiftImpl = { slug, peerId, price in
return self.context.engine.payments.buyStarGift(slug: slug, peerId: peerId, price: price)
|> afterCompleted {
context.starsContext?.load(force: true)
}
}
}
self.buyDisposable = (buyGiftImpl(uniqueGift.slug, recipientPeerId, acceptedPrice ?? resellStars)
|> deliverOnMainQueue).start(
error: { [weak self] error in
guard let self, let controller = self.getController() else {
return
}
self.inProgress = false
self.updated()
switch error {
case let .priceChanged(newPrice):
let errorTitle = presentationData.strings.Gift_Buy_ErrorPriceChanged_Title
let originalPriceString = presentationData.strings.Gift_Buy_ErrorPriceChanged_Text_Stars(Int32(resellStars))
let newPriceString = presentationData.strings.Gift_Buy_ErrorPriceChanged_Text_Stars(Int32(newPrice))
let errorText = presentationData.strings.Gift_Buy_ErrorPriceChanged_Text(originalPriceString, newPriceString).string
let alertController = textAlertController(
context: context,
title: errorTitle,
text: errorText,
actions: [
TextAlertAction(type: .defaultAction, title: presentationData.strings.Gift_Buy_Confirm_BuyFor(Int32(newPrice)), action: { [weak self] in
guard let self else {
return
}
self.commitBuy(acceptedPrice: newPrice, skipConfirmation: true)
}),
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
})
],
actionLayout: .vertical,
parseMarkdown: true
)
controller.present(alertController, in: .window(.root))
HapticFeedback().error()
default:
let alertController = textAlertController(context: context, title: nil, text: presentationData.strings.Gift_Buy_ErrorUnknown, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})], parseMarkdown: true)
controller.present(alertController, in: .window(.root))
}
},
completed: { [weak self, weak starsContext] in
guard let self,
let controller = self.getController() as? GiftViewScreen else {
return
}
self.inProgress = false
var animationFile: TelegramMediaFile?
for attribute in uniqueGift.attributes {
if case let .model(_, file, _) = attribute {
animationFile = file
break
}
}
if let navigationController = controller.navigationController as? NavigationController {
if recipientPeerId == self.context.account.peerId {
controller.dismissAnimated()
navigationController.view.addSubview(ConfettiView(frame: navigationController.view.bounds))
Queue.mainQueue().after(0.5, {
if let lastController = navigationController.viewControllers.last as? ViewController, let animationFile {
let resultController = UndoOverlayController(
presentationData: presentationData,
content: .sticker(context: context, file: animationFile, loop: false, title: presentationData.strings.Gift_View_Resale_SuccessYou_Title, text: presentationData.strings.Gift_View_Resale_SuccessYou_Text(giftTitle).string, undoText: nil, customAction: nil),
elevatedLayout: lastController is ChatController,
action: { _ in
return true
}
)
lastController.present(resultController, in: .window(.root))
}
})
} else {
var controllers = Array(navigationController.viewControllers.prefix(1))
let chatController = self.context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: recipientPeerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil)
chatController.hintPlayNextOutgoingGift()
controllers.append(chatController)
navigationController.setViewControllers(controllers, animated: true)
Queue.mainQueue().after(0.5, {
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: recipientPeerId))
|> deliverOnMainQueue).start(next: { [weak navigationController] peer in
if let peer, let lastController = navigationController?.viewControllers.last as? ViewController, let animationFile {
let resultController = UndoOverlayController(
presentationData: presentationData,
content: .sticker(context: context, file: animationFile, loop: false, title: presentationData.strings.Gift_View_Resale_Success_Title, text: presentationData.strings.Gift_View_Resale_Success_Text(peer.compactDisplayTitle).string, undoText: nil, customAction: nil),
elevatedLayout: lastController is ChatController,
action: { _ in
return true
}
)
lastController.present(resultController, in: .window(.root))
}
})
})
}
}
self.updated(transition: .spring(duration: 0.4))
Queue.mainQueue().after(0.5) {
starsContext?.load(force: true)
}
})
}
if let buyForm = self.buyForm, let price = buyForm.invoice.prices.first?.amount {
if starsState.balance < StarsAmount(value: price, nanos: 0) {
if self.options.isEmpty {
self.inProgress = true
self.updated()
}
let _ = (self.optionsPromise.get()
|> filter { $0 != nil }
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { [weak self] options in
guard let self, let controller = self.getController() else {
return
}
let purchaseController = self.context.sharedContext.makeStarsPurchaseScreen(
context: self.context,
starsContext: starsContext,
options: options ?? [],
purpose: .buyStarGift(requiredStars: price),
completion: { [weak self, weak starsContext] stars in
guard let self, let starsContext else {
return
}
self.inProgress = true
self.updated()
starsContext.add(balance: StarsAmount(value: stars, nanos: 0))
let _ = (starsContext.onUpdate
|> deliverOnMainQueue).start(next: { [weak self] in
guard let self else {
return
}
Queue.mainQueue().after(0.1, { [weak self] in
guard let self, let starsContext = self.context.starsContext, let starsState = starsContext.currentState else {
return
}
if starsState.balance < StarsAmount(value: price, nanos: 0) {
self.inProgress = false
self.updated()
self.commitBuy(skipConfirmation: true)
} else {
proceed()
}
});
})
}
)
controller.push(purchaseController)
})
} else {
proceed()
}
} else {
guard let controller = self.getController() else {
return
}
let alertController = textAlertController(context: context, title: nil, text: presentationData.strings.Gift_Buy_ErrorUnknown, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})], parseMarkdown: true)
controller.present(alertController, in: .window(.root))
}
}
if skipConfirmation {
action()
} else {
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: recipientPeerId))
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let self, let peer else {
return
}
let text: String
let starsString = presentationData.strings.Gift_Buy_Confirm_Text_Stars(Int32(resellStars))
if recipientPeerId == self.context.account.peerId {
text = presentationData.strings.Gift_Buy_Confirm_Text(giftTitle, starsString).string
} else {
text = presentationData.strings.Gift_Buy_Confirm_GiftText(giftTitle, starsString, peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string
}
let alertController = textAlertController(
context: self.context,
title: presentationData.strings.Gift_Buy_Confirm_Title,
text: text,
actions: [
TextAlertAction(type: .defaultAction, title: presentationData.strings.Gift_Buy_Confirm_BuyFor(Int32(resellStars)), action: {
action()
}),
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
})
],
actionLayout: .vertical,
parseMarkdown: true
)
if let controller = self.getController() as? GiftViewScreen {
controller.present(alertController, in: .window(.root))
}
})
}
}
func commitUpgrade() {
guard let arguments = self.subject.arguments, let peerId = arguments.peerId, let starsContext = self.context.starsContext, let starsState = starsContext.currentState else {
return
}
let proceed: (Int64?) -> Void = { formId in
guard let controller = self.getController() as? GiftViewScreen else {
return
}
self.inProgress = true
self.updated()
controller.showBalance = false
let context = self.context
let upgradeGiftImpl: ((Int64?, Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>)
if let upgradeGift = controller.upgradeGift {
upgradeGiftImpl = { formId, keepOriginalInfo in
return upgradeGift(formId, keepOriginalInfo)
|> afterCompleted {
if formId != nil {
context.starsContext?.load(force: true)
}
}
}
} else {
guard let reference = arguments.reference else {
return
}
upgradeGiftImpl = { formId, keepOriginalInfo in
return self.context.engine.payments.upgradeStarGift(formId: formId, reference: reference, keepOriginalInfo: keepOriginalInfo)
|> afterCompleted {
if formId != nil {
context.starsContext?.load(force: true)
}
}
}
}
self.upgradeDisposable = (upgradeGiftImpl(formId, self.keepOriginalInfo)
|> deliverOnMainQueue).start(next: { [weak self, weak starsContext] result in
guard let self, let controller = self.getController() as? GiftViewScreen else {
return
}
self.inProgress = false
self.inUpgradePreview = false
self.subject = .profileGift(peerId, result)
controller.animateSuccess()
self.updated(transition: .spring(duration: 0.4))
Queue.mainQueue().after(0.5) {
starsContext?.load(force: true)
}
})
}
if let upgradeStars = arguments.upgradeStars, upgradeStars > 0 {
proceed(nil)
} else if let upgradeForm = self.upgradeForm, let price = upgradeForm.invoice.prices.first?.amount {
if starsState.balance < StarsAmount(value: price, nanos: 0) {
let _ = (self.optionsPromise.get()
|> filter { $0 != nil }
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { [weak self] options in
guard let self, let controller = self.getController() else {
return
}
let purchaseController = self.context.sharedContext.makeStarsPurchaseScreen(
context: self.context,
starsContext: starsContext,
options: options ?? [],
purpose: .upgradeStarGift(requiredStars: price),
completion: { [weak self, weak starsContext] stars in
guard let self, let starsContext else {
return
}
self.inProgress = true
self.updated()
starsContext.add(balance: StarsAmount(value: stars, nanos: 0))
let _ = (starsContext.onUpdate
|> deliverOnMainQueue).start(next: {
proceed(upgradeForm.id)
})
}
)
controller.push(purchaseController)
})
} else {
proceed(upgradeForm.id)
}
}
if let controller = self.getController() as? GiftViewScreen {
controller.showBalance = true
}
}
}
func makeState() -> State {
return State(context: self.context, subject: self.subject, animateOut: self.animateOut, getController: self.getController)
}
static var body: Body {
let priceButton = Child(PlainButtonComponent.self)
let buttons = Child(ButtonsComponent.self)
let animation = Child(GiftCompositionComponent.self)
let title = Child(MultilineTextComponent.self)
let description = Child(MultilineTextComponent.self)
let transferButton = Child(PlainButtonComponent.self)
let wearButton = Child(PlainButtonComponent.self)
let resellButton = Child(PlainButtonComponent.self)
let wearAvatar = Child(AvatarComponent.self)
let wearPeerName = Child(MultilineTextComponent.self)
let wearPeerStatus = Child(MultilineTextComponent.self)
let wearTitle = Child(MultilineTextComponent.self)
let wearDescription = Child(MultilineTextComponent.self)
let wearPerks = Child(List<Empty>.self)
let hiddenText = Child(MultilineTextComponent.self)
let table = Child(TableComponent.self)
let additionalText = Child(MultilineTextComponent.self)
let button = Child(ButtonComponent.self)
let upgradeTitle = Child(MultilineTextComponent.self)
let upgradeDescription = Child(BalancedTextComponent.self)
let upgradePerks = Child(List<Empty>.self)
let upgradeKeepName = Child(PlainButtonComponent.self)
let spaceRegex = try? NSRegularExpression(pattern: "\\[(.*?)\\]", options: [])
let giftCompositionExternalState = GiftCompositionComponent.ExternalState()
return { context in
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
let component = context.component
let theme = environment.theme
let strings = environment.strings
let dateTimeFormat = environment.dateTimeFormat
let nameDisplayOrder = component.context.sharedContext.currentPresentationData.with { $0 }.nameDisplayOrder
let controller = environment.controller
let state = context.state
let subject = state.subject
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
var titleString: String
var animationFile: TelegramMediaFile?
let stars: Int64
let convertStars: Int64?
let text: String?
let entities: [MessageTextEntity]?
var limitRemains: Int32?
let limitTotal: Int32?
var incoming = false
var savedToProfile = false
var converted = false
var giftId: Int64 = 0
var date: Int32?
var soldOut = false
var nameHidden = false
var upgraded = false
var exported = false
var canUpgrade = false
var upgradeStars: Int64?
var uniqueGift: StarGift.UniqueGift?
var isSelfGift = false
var isChannelGift = false
var isMyUniqueGift = false
if case let .soldOutGift(gift) = subject {
animationFile = gift.file
stars = gift.price
text = nil
entities = nil
limitRemains = nil
limitTotal = gift.availability?.total
convertStars = nil
soldOut = true
titleString = strings.Gift_View_UnavailableTitle
} else if let arguments = subject.arguments {
switch arguments.gift {
case let .generic(gift):
animationFile = gift.file
stars = gift.price
text = arguments.text
entities = arguments.entities
limitRemains = gift.availability?.remains
limitTotal = gift.availability?.total
convertStars = arguments.convertStars
converted = arguments.converted
giftId = gift.id
date = arguments.date
upgraded = arguments.upgraded
canUpgrade = arguments.canUpgrade
upgradeStars = arguments.upgradeStars
case let .unique(gift):
stars = 0
text = nil
entities = nil
limitRemains = nil
limitTotal = nil
convertStars = nil
uniqueGift = gift
}
savedToProfile = arguments.savedToProfile
if let reference = arguments.reference, case .peer = reference {
isChannelGift = true
incoming = true
} else {
incoming = arguments.incoming || arguments.peerId == component.context.account.peerId
}
nameHidden = arguments.nameHidden
isSelfGift = arguments.messageId?.peerId == component.context.account.peerId
if case let .peerId(peerId) = uniqueGift?.owner, peerId == component.context.account.peerId || isChannelGift {
isMyUniqueGift = true
}
if isSelfGift {
titleString = strings.Gift_View_Self_Title
} else {
titleString = incoming ? strings.Gift_View_ReceivedTitle : strings.Gift_View_Title
}
} else {
animationFile = nil
stars = 0
text = nil
entities = nil
limitTotal = nil
convertStars = nil
titleString = ""
}
var showUpgradePreview = false
if state.inUpgradePreview, let _ = state.sampleGiftAttributes {
showUpgradePreview = true
} else if case .upgradePreview = component.subject {
showUpgradePreview = true
}
var showWearPreview = false
if state.inWearPreview {
showWearPreview = true
} else if case .wearPreview = component.subject {
showWearPreview = true
}
let buttons = buttons.update(
component: ButtonsComponent(
theme: theme,
isOverlay: showUpgradePreview || uniqueGift != nil,
showMoreButton: uniqueGift != nil && !showWearPreview,
closePressed: { [weak state] in
guard let state else {
return
}
if state.inWearPreview {
if let controller = controller() as? GiftViewScreen {
controller.dismissAllTooltips()
}
state.inWearPreview = false
state.updated(transition: .spring(duration: 0.4))
} else if state.inUpgradePreview {
state.cancelUpgradePreview()
} else {
state.dismiss(animated: true)
}
},
morePressed: { [weak state] node, gesture in
state?.openMore(node: node, gesture: gesture)
}
),
availableSize: CGSize(width: 30.0, height: 30.0),
transition: context.transition
)
var originY: CGFloat = 0.0
let headerHeight: CGFloat
let headerSubject: GiftCompositionComponent.Subject?
if let uniqueGift {
if showWearPreview {
headerHeight = 200.0
} else if case let .peerId(peerId) = uniqueGift.owner, peerId == component.context.account.peerId || isChannelGift {
headerHeight = 314.0
} else {
headerHeight = 240.0
}
headerSubject = .unique(uniqueGift)
} else if state.inUpgradePreview, let attributes = state.sampleGiftAttributes {
headerHeight = 258.0
headerSubject = .preview(attributes)
} else if case let .upgradePreview(attributes, _) = component.subject {
headerHeight = 258.0
headerSubject = .preview(attributes)
} else if let animationFile {
headerHeight = 210.0
headerSubject = .generic(animationFile)
} else {
headerHeight = 210.0
headerSubject = nil
}
var ownerPeerId: EnginePeer.Id?
if let uniqueGift, case let .peerId(peerId) = uniqueGift.owner {
ownerPeerId = peerId
}
let wearOwnerPeerId = ownerPeerId ?? component.context.account.peerId
var wearPeerNameChild: _UpdatedChildComponent?
if showWearPreview, let uniqueGift {
var peerName = ""
if let ownerPeer = state.peerMap[wearOwnerPeerId] {
peerName = ownerPeer.displayTitle(strings: strings, displayOrder: nameDisplayOrder)
}
wearPeerNameChild = wearPeerName.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(
string: peerName,
font: Font.bold(28.0),
textColor: .white,
paragraphAlignment: .center
)),
horizontalAlignment: .center,
maximumNumberOfLines: 1
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
let giftTitle: String
if case .wearPreview = component.subject {
giftTitle = uniqueGift.title
} else {
giftTitle = "\(uniqueGift.title) #\(presentationStringsFormattedNumber(uniqueGift.number, environment.dateTimeFormat.groupingSeparator))"
}
let wearTitle = wearTitle.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(
string: strings.Gift_Wear_Wear(giftTitle).string,
font: Font.bold(24.0),
textColor: theme.actionSheet.primaryTextColor,
paragraphAlignment: .center
)),
horizontalAlignment: .center,
maximumNumberOfLines: 1
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
let wearDescription = wearDescription.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(
string: strings.Gift_Wear_GetBenefits,
font: Font.regular(15.0),
textColor: theme.actionSheet.primaryTextColor,
paragraphAlignment: .center
)),
horizontalAlignment: .center,
maximumNumberOfLines: 1,
lineSpacing: 0.2
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 50.0, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
var titleOriginY = headerHeight + 18.0
context.add(wearTitle
.position(CGPoint(x: context.availableSize.width / 2.0, y: titleOriginY + wearTitle.size.height))
.appear(.default(alpha: true))
.disappear(.default(alpha: true))
)
titleOriginY += wearTitle.size.height
titleOriginY += 18.0
context.add(wearDescription
.position(CGPoint(x: context.availableSize.width / 2.0, y: titleOriginY + wearDescription.size.height))
.appear(.default(alpha: true))
.disappear(.default(alpha: true))
)
}
var animationOffset: CGPoint?
var animationScale: CGFloat?
if let wearPeerNameChild {
animationOffset = CGPoint(x: wearPeerNameChild.size.width / 2.0 + 20.0 - 12.0, y: 56.0)
animationScale = 0.19
}
if let headerSubject {
let animation = animation.update(
component: GiftCompositionComponent(
context: component.context,
theme: environment.theme,
subject: headerSubject,
animationOffset: animationOffset,
animationScale: animationScale,
displayAnimationStars: showWearPreview,
externalState: giftCompositionExternalState,
requestUpdate: { [weak state] in
state?.updated()
}
),
availableSize: CGSize(width: context.availableSize.width, height: headerHeight),
transition: context.transition
)
context.add(animation
.position(CGPoint(x: context.availableSize.width / 2.0, y: headerHeight / 2.0))
)
}
originY += headerHeight
let vibrantColor: UIColor
if let previewPatternColor = giftCompositionExternalState.previewPatternColor {
vibrantColor = previewPatternColor.withMultiplied(hue: 1.0, saturation: 1.02, brightness: 1.25).mixedWith(UIColor.white, alpha: 0.3)
} else {
vibrantColor = UIColor.white.withAlphaComponent(0.6)
}
if let wearPeerNameChild {
if let ownerPeer = state.peerMap[wearOwnerPeerId] {
let wearAvatar = wearAvatar.update(
component: AvatarComponent(
context: component.context,
theme: theme,
peer: ownerPeer
),
environment: {},
availableSize: CGSize(width: 100.0, height: 100.0),
transition: context.transition
)
context.add(wearAvatar
.position(CGPoint(x: context.availableSize.width / 2.0, y: 67.0))
.appear(.default(scale: true, alpha: true))
.disappear(.default(scale: true, alpha: true))
)
}
let wearPeerStatus = wearPeerStatus.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(
string: isChannelGift ? strings.Channel_Status : strings.Presence_online,
font: Font.regular(17.0),
textColor: vibrantColor,
paragraphAlignment: .center
)),
horizontalAlignment: .center,
maximumNumberOfLines: 5,
lineSpacing: 0.2
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 50.0, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
context.add(wearPeerNameChild
.position(CGPoint(x: context.availableSize.width / 2.0 - 12.0, y: 144.0))
.appear(.default(alpha: true))
.disappear(.default(alpha: true))
)
context.add(wearPeerStatus
.position(CGPoint(x: context.availableSize.width / 2.0, y: 174.0))
.appear(.default(alpha: true))
.disappear(.default(alpha: true))
)
originY += 18.0
originY += 28.0
originY += 18.0
originY += 20.0
originY += 24.0
let textColor = theme.actionSheet.primaryTextColor
let secondaryTextColor = theme.actionSheet.secondaryTextColor
let linkColor = theme.actionSheet.controlAccentColor
var items: [AnyComponentWithIdentity<Empty>] = []
items.append(
AnyComponentWithIdentity(
id: "badge",
component: AnyComponent(ParagraphComponent(
title: strings.Gift_Wear_Badge_Title,
titleColor: textColor,
text: isChannelGift ? strings.Gift_Wear_Badge_ChannelText : strings.Gift_Wear_Badge_Text,
textColor: secondaryTextColor,
accentColor: linkColor,
iconName: "Premium/Collectible/Badge",
iconColor: linkColor
))
)
)
items.append(
AnyComponentWithIdentity(
id: "design",
component: AnyComponent(ParagraphComponent(
title: strings.Gift_Wear_Design_Title,
titleColor: textColor,
text: isChannelGift ? strings.Gift_Wear_Design_ChannelText : strings.Gift_Wear_Design_Text,
textColor: secondaryTextColor,
accentColor: linkColor,
iconName: "Premium/BoostPerk/CoverColor",
iconColor: linkColor
))
)
)
items.append(
AnyComponentWithIdentity(
id: "proof",
component: AnyComponent(ParagraphComponent(
title: strings.Gift_Wear_Proof_Title,
titleColor: textColor,
text: isChannelGift ? strings.Gift_Wear_Proof_ChannelText : strings.Gift_Wear_Proof_Text,
textColor: secondaryTextColor,
accentColor: linkColor,
iconName: "Premium/Collectible/Proof",
iconColor: linkColor
))
)
)
let perksSideInset = sideInset + 16.0
let wearPerks = wearPerks.update(
component: List(items),
availableSize: CGSize(width: context.availableSize.width - perksSideInset * 2.0, height: 10000.0),
transition: context.transition
)
context.add(wearPerks
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY + wearPerks.size.height / 2.0))
.appear(.default(alpha: true))
.disappear(.default(alpha: true))
)
originY += wearPerks.size.height
originY += 16.0
} else if showUpgradePreview {
let title: String
let description: String
let uniqueText: String
let transferableText: String
let tradableText: String
if case let .upgradePreview(_, name) = component.subject {
title = environment.strings.Gift_Upgrade_IncludeTitle
description = environment.strings.Gift_Upgrade_IncludeDescription(name).string
uniqueText = strings.Gift_Upgrade_Unique_IncludeDescription
transferableText = strings.Gift_Upgrade_Transferable_IncludeDescription
tradableText = strings.Gift_Upgrade_Tradable_IncludeDescription
} else {
title = environment.strings.Gift_Upgrade_Title
description = environment.strings.Gift_Upgrade_Description
uniqueText = strings.Gift_Upgrade_Unique_Description
transferableText = strings.Gift_Upgrade_Transferable_Description
tradableText = strings.Gift_Upgrade_Tradable_Description
}
let upgradeTitle = upgradeTitle.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(
string: title,
font: Font.bold(20.0),
textColor: .white,
paragraphAlignment: .center
)),
horizontalAlignment: .center,
maximumNumberOfLines: 1
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
let upgradeDescription = upgradeDescription.update(
component: BalancedTextComponent(
text: .plain(NSAttributedString(
string: description,
font: Font.regular(13.0),
textColor: vibrantColor,
paragraphAlignment: .center
)),
horizontalAlignment: .center,
maximumNumberOfLines: 5,
lineSpacing: 0.2
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 50.0, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
let spacing: CGFloat = 6.0
let totalHeight: CGFloat = upgradeTitle.size.height + spacing + upgradeDescription.size.height
context.add(upgradeTitle
.position(CGPoint(x: context.availableSize.width / 2.0, y: floor(212.0 - totalHeight / 2.0 + upgradeTitle.size.height / 2.0)))
.appear(.default(alpha: true))
.disappear(.default(alpha: true))
)
context.add(upgradeDescription
.position(CGPoint(x: context.availableSize.width / 2.0, y: floor(212.0 + totalHeight / 2.0 - upgradeDescription.size.height / 2.0)))
.appear(.default(alpha: true))
.disappear(.default(alpha: true))
)
originY += 24.0
let textColor = theme.actionSheet.primaryTextColor
let secondaryTextColor = theme.actionSheet.secondaryTextColor
let linkColor = theme.actionSheet.controlAccentColor
var items: [AnyComponentWithIdentity<Empty>] = []
items.append(
AnyComponentWithIdentity(
id: "unique",
component: AnyComponent(ParagraphComponent(
title: strings.Gift_Upgrade_Unique_Title,
titleColor: textColor,
text: uniqueText,
textColor: secondaryTextColor,
accentColor: linkColor,
iconName: "Premium/Collectible/Unique",
iconColor: linkColor
))
)
)
items.append(
AnyComponentWithIdentity(
id: "transferable",
component: AnyComponent(ParagraphComponent(
title: strings.Gift_Upgrade_Transferable_Title,
titleColor: textColor,
text: transferableText,
textColor: secondaryTextColor,
accentColor: linkColor,
iconName: "Premium/Collectible/Transferable",
iconColor: linkColor
))
)
)
items.append(
AnyComponentWithIdentity(
id: "tradable",
component: AnyComponent(ParagraphComponent(
title: strings.Gift_Upgrade_Tradable_Title,
titleColor: textColor,
text: tradableText,
textColor: secondaryTextColor,
accentColor: linkColor,
iconName: "Premium/Collectible/Tradable",
iconColor: linkColor
))
)
)
let perksSideInset = sideInset + 16.0
let upgradePerks = upgradePerks.update(
component: List(items),
availableSize: CGSize(width: context.availableSize.width - perksSideInset * 2.0, height: 10000.0),
transition: context.transition
)
context.add(upgradePerks
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY + upgradePerks.size.height / 2.0))
.appear(.default(alpha: true))
.disappear(.default(alpha: true))
)
originY += upgradePerks.size.height
originY += 16.0
if case .upgradePreview = component.subject {
} else {
let checkTheme = CheckComponent.Theme(
backgroundColor: theme.list.itemCheckColors.fillColor,
strokeColor: theme.list.itemCheckColors.foregroundColor,
borderColor: theme.list.itemCheckColors.strokeColor,
overlayBorder: false,
hasInset: false,
hasShadow: false
)
let keepInfoText: String
if let nameHidden = subject.arguments?.nameHidden, nameHidden {
keepInfoText = isChannelGift ? strings.Gift_Upgrade_AddChannelName : strings.Gift_Upgrade_AddMyName
} else {
keepInfoText = text != nil ? strings.Gift_Upgrade_AddNameAndComment : strings.Gift_Upgrade_AddName
}
let upgradeKeepName = upgradeKeepName.update(
component: PlainButtonComponent(
content: AnyComponent(HStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(CheckComponent(
theme: checkTheme,
size: CGSize(width: 18.0, height: 18.0),
selected: state.keepOriginalInfo
))),
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: keepInfoText, font: Font.regular(13.0), textColor: theme.list.itemSecondaryTextColor))
)))
],
spacing: 10.0
)),
effectAlignment: .center,
action: { [weak state] in
guard let state else {
return
}
state.keepOriginalInfo = !state.keepOriginalInfo
state.updated(transition: .easeInOut(duration: 0.2))
},
animateAlpha: false,
animateScale: false
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 1000.0),
transition: context.transition
)
context.add(upgradeKeepName
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY + upgradeKeepName.size.height / 2.0))
.appear(.default(alpha: true))
.disappear(.default(alpha: true))
)
originY += upgradeKeepName.size.height
originY += 18.0
}
} else {
var descriptionText: String
if let uniqueGift {
titleString = uniqueGift.title
descriptionText = "\(strings.Gift_Unique_Collectible) #\(presentationStringsFormattedNumber(uniqueGift.number, environment.dateTimeFormat.groupingSeparator))"
} else if soldOut {
descriptionText = strings.Gift_View_UnavailableDescription
} else if upgraded {
descriptionText = strings.Gift_View_UpgradedDescription
} else if incoming {
if let _ = upgradeStars {
descriptionText = strings.Gift_View_FreeUpgradeDescription
} else if let convertStars, !upgraded {
if !converted {
if canUpgrade || upgradeStars != nil {
descriptionText = isChannelGift ? strings.Gift_View_KeepUpgradeOrConvertDescription_Channel(strings.Gift_View_KeepOrConvertDescription_Stars(Int32(convertStars))).string : strings.Gift_View_KeepUpgradeOrConvertDescription(strings.Gift_View_KeepOrConvertDescription_Stars(Int32(convertStars))).string
} else {
descriptionText = isChannelGift ? strings.Gift_View_KeepOrConvertDescription_Channel(strings.Gift_View_KeepOrConvertDescription_Stars(Int32(convertStars))).string : strings.Gift_View_KeepOrConvertDescription(strings.Gift_View_KeepOrConvertDescription_Stars(Int32(convertStars))).string
}
} else {
descriptionText = strings.Gift_View_ConvertedDescription(strings.Gift_View_ConvertedDescription_Stars(Int32(convertStars))).string
}
} else {
descriptionText = strings.Gift_View_BotDescription
}
} else if let peerId = subject.arguments?.peerId, let peer = state.peerMap[peerId] {
if let _ = upgradeStars {
descriptionText = strings.Gift_View_FreeUpgradeOtherDescription(peer.compactDisplayTitle).string
} else if case .message = subject, let convertStars {
descriptionText = strings.Gift_View_OtherDescription(peer.compactDisplayTitle, strings.Gift_View_OtherDescription_Stars(Int32(convertStars))).string
} else {
descriptionText = ""
}
} else {
descriptionText = ""
}
if let spaceRegex {
let nsRange = NSRange(descriptionText.startIndex..., in: descriptionText)
let matches = spaceRegex.matches(in: descriptionText, options: [], range: nsRange)
var modifiedString = descriptionText
for match in matches.reversed() {
let matchRange = Range(match.range, in: descriptionText)!
let matchedSubstring = String(descriptionText[matchRange])
let replacedSubstring = matchedSubstring.replacingOccurrences(of: " ", with: "\u{00A0}")
modifiedString.replaceSubrange(matchRange, with: replacedSubstring)
}
descriptionText = modifiedString
}
let title = title.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(
string: titleString,
font: uniqueGift != nil ? Font.bold(20.0) : Font.bold(25.0),
textColor: uniqueGift != nil ? .white : theme.actionSheet.primaryTextColor,
paragraphAlignment: .center
)),
horizontalAlignment: .center,
maximumNumberOfLines: 1
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
context.add(title
.position(CGPoint(x: context.availableSize.width / 2.0, y: uniqueGift != nil ? 190.0 : 177.0))
.appear(.default(alpha: true))
.disappear(.default(alpha: true))
)
if !descriptionText.isEmpty {
let linkColor = theme.actionSheet.controlAccentColor
if state.cachedSmallStarImage == nil || state.cachedSmallStarImage?.1 !== environment.theme {
state.cachedSmallStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Premium/Stars/ButtonStar"), color: .white)!, theme)
}
if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== environment.theme {
state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: linkColor)!, theme)
}
let textFont: UIFont
let textColor: UIColor
if let _ = uniqueGift {
textFont = Font.regular(13.0)
textColor = vibrantColor
} else {
textFont = soldOut ? Font.medium(15.0) : Font.regular(15.0)
textColor = soldOut ? theme.list.itemDestructiveColor : theme.list.itemPrimaryTextColor
}
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: textFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
})
descriptionText = descriptionText.replacingOccurrences(of: " >]", with: "\u{00A0}>]")
let attributedString = parseMarkdownIntoAttributedString(descriptionText, attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString
if let range = attributedString.string.range(of: "*"), let starImage = state.cachedSmallStarImage?.0 {
attributedString.addAttribute(.font, value: Font.regular(13.0), range: NSRange(range, in: attributedString.string))
attributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: attributedString.string))
attributedString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: attributedString.string))
}
if let range = attributedString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 {
attributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedString.string))
}
let description = description.update(
component: MultilineTextComponent(
text: .plain(attributedString),
horizontalAlignment: .center,
maximumNumberOfLines: 5,
lineSpacing: 0.2,
highlightColor: linkColor.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 state] attributes, _ in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
state?.openStarsIntro()
}
}
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 50.0, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
context.add(description
.position(CGPoint(x: context.availableSize.width / 2.0, y: 207.0 + description.size.height / 2.0))
.appear(.default(alpha: true))
.disappear(.default(alpha: true))
)
if uniqueGift != nil {
originY += 16.0
} else {
originY += description.size.height + 21.0
if soldOut {
originY -= 7.0
}
}
} else {
originY += 9.0
}
if nameHidden && uniqueGift == nil {
let textFont = Font.regular(13.0)
let textColor = theme.list.itemSecondaryTextColor
let hiddenDescription: String
if incoming {
hiddenDescription = text != nil ? strings.Gift_View_NameAndMessageHidden : strings.Gift_View_NameHidden
} else if let peerId = subject.arguments?.peerId, let peer = state.peerMap[peerId], subject.arguments?.fromPeerId != nil {
hiddenDescription = text != nil ? strings.Gift_View_Outgoing_NameAndMessageHidden(peer.compactDisplayTitle).string : strings.Gift_View_Outgoing_NameHidden(peer.compactDisplayTitle).string
} else {
hiddenDescription = ""
}
if !hiddenDescription.isEmpty {
let hiddenText = hiddenText.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(string: hiddenDescription, font: textFont, textColor: textColor)),
horizontalAlignment: .center,
maximumNumberOfLines: 2,
lineSpacing: 0.2
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
context.add(hiddenText
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY))
)
originY += hiddenText.size.height
originY += 11.0
}
}
let tableFont = Font.regular(15.0)
let tableBoldFont = Font.semibold(15.0)
let tableItalicFont = Font.italic(15.0)
let tableBoldItalicFont = Font.semiboldItalic(15.0)
let tableMonospaceFont = Font.monospace(15.0)
let tableLargeMonospaceFont = Font.monospace(16.0)
let tableTextColor = theme.list.itemPrimaryTextColor
let tableLinkColor = theme.list.itemAccentColor
var tableItems: [TableComponent.Item] = []
var isWearing = state.pendingWear
if !soldOut {
if let uniqueGift {
switch uniqueGift.owner {
case let .peerId(peerId):
if let peer = state.peerMap[peerId] {
let ownerComponent: AnyComponent<Empty>
if peer.id == component.context.account.peerId, peer.isPremium {
let animationContent: EmojiStatusComponent.Content
var color: UIColor?
var statusId: Int64 = 1
if state.pendingWear {
var fileId: Int64?
for attribute in uniqueGift.attributes {
if case let .model(_, file, _) = attribute {
fileId = file.fileId.id
}
if case let .backdrop(_, _, innerColor, _, _, _, _) = attribute {
color = UIColor(rgb: UInt32(bitPattern: innerColor))
}
}
if let fileId {
statusId = fileId
animationContent = .animation(content: .customEmoji(fileId: fileId), size: CGSize(width: 18.0, height: 18.0), placeholderColor: theme.list.mediaPlaceholderColor, themeColor: tableLinkColor, loopMode: .count(2))
} else {
animationContent = .premium(color: tableLinkColor)
}
} else if let emojiStatus = peer.emojiStatus, !state.pendingTakeOff {
animationContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 18.0, height: 18.0), placeholderColor: theme.list.mediaPlaceholderColor, themeColor: tableLinkColor, loopMode: .count(2))
if case let .starGift(id, _, _, _, _, innerColor, _, _, _) = emojiStatus.content {
color = UIColor(rgb: UInt32(bitPattern: innerColor))
if id == uniqueGift.id {
isWearing = true
state.pendingWear = false
}
}
} else {
animationContent = .premium(color: tableLinkColor)
state.pendingTakeOff = false
}
ownerComponent = AnyComponent(
HStack([
AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(Button(
content: AnyComponent(
PeerCellComponent(
context: component.context,
theme: theme,
strings: strings,
peer: peer
)
),
action: { [weak state] in
state?.openPeer(peer)
}
))
),
AnyComponentWithIdentity(
id: AnyHashable(statusId),
component: AnyComponent(EmojiStatusComponent(
context: component.context,
animationCache: component.context.animationCache,
animationRenderer: component.context.animationRenderer,
content: animationContent,
particleColor: color,
size: CGSize(width: 18.0, height: 18.0),
isVisibleForAnimations: true,
action: {
},
tag: state.statusTag
))
)
], spacing: 2.0)
)
} else {
ownerComponent = AnyComponent(Button(
content: AnyComponent(
PeerCellComponent(
context: component.context,
theme: theme,
strings: strings,
peer: peer
)
),
action: { [weak state] in
state?.openPeer(peer)
}
))
}
tableItems.append(.init(
id: "owner",
title: strings.Gift_Unique_Owner,
component: ownerComponent
))
}
case let .name(name):
tableItems.append(.init(
id: "name_owner",
title: strings.Gift_Unique_Owner,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: name, font: tableFont, textColor: tableTextColor)))
)
))
case let .address(address):
exported = true
func formatAddress(_ str: String) -> String {
guard str.count == 48 && !str.hasSuffix(".ton") else {
return str
}
var result = str
let middleIndex = result.index(result.startIndex, offsetBy: str.count / 2)
result.insert("\n", at: middleIndex)
return result
}
tableItems.append(.init(
id: "address_owner",
title: strings.Gift_Unique_Owner,
component: AnyComponent(
Button(
content: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: formatAddress(address), font: tableLargeMonospaceFont, textColor: tableLinkColor)), maximumNumberOfLines: 2, lineSpacing: 0.2)
),
action: { [weak state] in
state?.copyAddress(address)
}
)
)
))
}
} else if let peerId = subject.arguments?.fromPeerId, let peer = state.peerMap[peerId] {
var isBot = false
if case let .user(user) = peer, user.botInfo != nil {
isBot = true
}
let fromComponent: AnyComponent<Empty>
if incoming && !peer.isDeleted && !isBot && !isChannelGift {
fromComponent = AnyComponent(
HStack([
AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(Button(
content: AnyComponent(
PeerCellComponent(
context: component.context,
theme: theme,
strings: strings,
peer: peer
)
),
action: { [weak state] in
state?.openPeer(peer)
}
))
),
AnyComponentWithIdentity(
id: AnyHashable(1),
component: AnyComponent(Button(
content: AnyComponent(ButtonContentComponent(
context: component.context,
text: strings.Gift_View_Send,
color: theme.list.itemAccentColor
)),
action: { [weak state] in
state?.sendGift(peerId: peerId)
}
))
)
], spacing: 4.0)
)
} else {
fromComponent = AnyComponent(Button(
content: AnyComponent(
PeerCellComponent(
context: component.context,
theme: theme,
strings: strings,
peer: peer
)
),
action: { [weak state] in
state?.openPeer(peer)
}
))
}
if !isSelfGift {
tableItems.append(.init(
id: "from",
title: strings.Gift_View_From,
component: fromComponent
))
}
} else {
if !isSelfGift {
tableItems.append(.init(
id: "from_anon",
title: strings.Gift_View_From,
component: AnyComponent(
PeerCellComponent(
context: component.context,
theme: theme,
strings: strings,
peer: nil
)
)
))
}
}
}
if let uniqueGift {
if isMyUniqueGift, case let .peerId(peerId) = uniqueGift.owner {
var canTransfer = true
var canResell = true
if let peer = state.peerMap[peerId], case let .channel(channel) = peer {
if !channel.flags.contains(.isCreator) {
canTransfer = false
}
canResell = false
} else if subject.arguments?.transferStars == nil {
canTransfer = false
}
var buttonsCount = 1
if canTransfer {
buttonsCount += 1
}
if canResell {
buttonsCount += 1
}
let buttonSpacing: CGFloat = 10.0
let buttonWidth = floor(context.availableSize.width - sideInset * 2.0 - buttonSpacing * CGFloat(buttonsCount - 1)) / CGFloat(buttonsCount)
let buttonHeight: CGFloat = 58.0
var buttonOriginX = sideInset
if canTransfer {
let transferButton = transferButton.update(
component: PlainButtonComponent(
content: AnyComponent(
HeaderButtonComponent(
title: strings.Gift_View_Header_Transfer,
iconName: "Premium/Collectible/Transfer"
)
),
effectAlignment: .center,
action: { [weak state] in
state?.transferGift()
}
),
environment: {},
availableSize: CGSize(width: buttonWidth, height: buttonHeight),
transition: context.transition
)
context.add(transferButton
.position(CGPoint(x: buttonOriginX + buttonWidth / 2.0, y: headerHeight - buttonHeight / 2.0 - 16.0))
.appear(.default(scale: true, alpha: true))
.disappear(.default(scale: true, alpha: true))
)
buttonOriginX += buttonWidth + buttonSpacing
}
let wearButton = wearButton.update(
component: PlainButtonComponent(
content: AnyComponent(
HeaderButtonComponent(
title: isWearing ? strings.Gift_View_Header_TakeOff : strings.Gift_View_Header_Wear,
iconName: isWearing ? "Premium/Collectible/Unwear" : "Premium/Collectible/Wear"
)
),
effectAlignment: .center,
action: { [weak state] in
if let state {
if isWearing {
state.commitTakeOff()
state.showAttributeInfo(tag: state.statusTag, text: strings.Gift_View_TookOff("\(uniqueGift.title) #\(presentationStringsFormattedNumber(uniqueGift.number, environment.dateTimeFormat.groupingSeparator))").string)
} else {
if let controller = controller() as? GiftViewScreen {
controller.dismissAllTooltips()
}
let canWear: Bool
if isChannelGift, case let .channel(channel) = state.peerMap[wearOwnerPeerId] {
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 })
let requiredLevel = Int(BoostSubject.wearGift.requiredLevel(group: false, context: component.context, configuration: premiumConfiguration))
if let boostLevel = channel.approximateBoostLevel {
canWear = boostLevel >= requiredLevel
} else {
canWear = false
}
} else {
canWear = component.context.isPremium
}
let _ = (ApplicationSpecificNotice.getStarGiftWearTips(accountManager: component.context.sharedContext.accountManager)
|> deliverOnMainQueue).start(next: { [weak state] count in
guard let state else {
return
}
if !canWear || count < 3 {
state.requestWearPreview()
} else {
state.commitWear(uniqueGift)
state.showAttributeInfo(tag: state.statusTag, text: strings.Gift_View_PutOn("\(uniqueGift.title) #\(presentationStringsFormattedNumber(uniqueGift.number, environment.dateTimeFormat.groupingSeparator))").string)
}
})
}
}
}
),
environment: {},
availableSize: CGSize(width: buttonWidth, height: buttonHeight),
transition: context.transition
)
context.add(wearButton
.position(CGPoint(x: buttonOriginX + buttonWidth / 2.0, y: headerHeight - buttonHeight / 2.0 - 16.0))
.appear(.default(scale: true, alpha: true))
.disappear(.default(scale: true, alpha: true))
)
buttonOriginX += buttonWidth + buttonSpacing
if canResell {
let resellButton = resellButton.update(
component: PlainButtonComponent(
content: AnyComponent(
HeaderButtonComponent(
title: uniqueGift.resellStars == nil ? strings.Gift_View_Sell : strings.Gift_View_Unlist,
iconName: uniqueGift.resellStars == nil ? "Premium/Collectible/Sell" : "Premium/Collectible/Unlist"
)
),
effectAlignment: .center,
action: { [weak state] in
state?.resellGift()
}
),
environment: {},
availableSize: CGSize(width: buttonWidth, height: buttonHeight),
transition: context.transition
)
context.add(resellButton
.position(CGPoint(x: buttonOriginX + buttonWidth / 2.0, y: headerHeight - buttonHeight / 2.0 - 16.0))
.appear(.default(scale: true, alpha: true))
.disappear(.default(scale: true, alpha: true))
)
}
}
let order: [StarGift.UniqueGift.Attribute.AttributeType] = [
.model, .backdrop, .pattern, .originalInfo
]
var attributeMap: [StarGift.UniqueGift.Attribute.AttributeType: StarGift.UniqueGift.Attribute] = [:]
for attribute in uniqueGift.attributes {
attributeMap[attribute.attributeType] = attribute
}
var hasOriginalInfo = false
for type in order {
if let attribute = attributeMap[type] {
let id: String
let title: String?
let value: NSAttributedString
let percentage: Float?
let tag: AnyObject?
var hasBackground = false
switch attribute {
case let .model(name, _, rarity):
id = "model"
title = strings.Gift_Unique_Model
value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor)
percentage = Float(rarity) * 0.1
tag = state.modelButtonTag
case let .backdrop(name, _, _, _, _, _, rarity):
id = "backdrop"
title = strings.Gift_Unique_Backdrop
value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor)
percentage = Float(rarity) * 0.1
tag = state.backdropButtonTag
case let .pattern(name, _, rarity):
id = "pattern"
title = strings.Gift_Unique_Symbol
value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor)
percentage = Float(rarity) * 0.1
tag = state.symbolButtonTag
case let .originalInfo(senderPeerId, recipientPeerId, date, text, entities):
id = "originalInfo"
title = nil
hasBackground = true
let tableFont = Font.regular(13.0)
let tableBoldFont = Font.semibold(13.0)
let tableItalicFont = Font.italic(13.0)
let tableBoldItalicFont = Font.semiboldItalic(13.0)
let tableMonospaceFont = Font.monospace(13.0)
let senderName = (senderPeerId.flatMap { state.peerMap[$0]?.displayTitle(strings: strings, displayOrder: nameDisplayOrder) })
let recipientName = state.peerMap[recipientPeerId]?.displayTitle(strings: strings, displayOrder: nameDisplayOrder) ?? ""
let dateString = stringForMediumDate(timestamp: date, strings: strings, dateTimeFormat: dateTimeFormat, withTime: false)
if let text {
let attributedText = stringWithAppliedEntities(text, entities: entities ?? [], baseColor: tableTextColor, linkColor: tableLinkColor, baseFont: tableFont, linkFont: tableFont, boldFont: tableBoldFont, italicFont: tableItalicFont, boldItalicFont: tableBoldItalicFont, fixedFont: tableMonospaceFont, blockQuoteFont: tableFont, message: nil)
let format = senderName != nil ? strings.Gift_Unique_OriginalInfoSenderWithText(senderName!, recipientName, dateString, "") : strings.Gift_Unique_OriginalInfoWithText(recipientName, dateString, "")
let string = NSMutableAttributedString(string: format.string, font: tableFont, textColor: tableTextColor)
string.replaceCharacters(in: format.ranges[format.ranges.count - 1].range, with: attributedText)
if let senderPeerId {
string.addAttribute(.foregroundColor, value: tableLinkColor, range: format.ranges[0].range)
string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention), value: TelegramPeerMention(peerId: senderPeerId, mention: ""), range: format.ranges[0].range)
string.addAttribute(.foregroundColor, value: tableLinkColor, range: format.ranges[1].range)
string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention), value: TelegramPeerMention(peerId: recipientPeerId, mention: ""), range: format.ranges[1].range)
} else {
string.addAttribute(.foregroundColor, value: tableLinkColor, range: format.ranges[0].range)
string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention), value: TelegramPeerMention(peerId: recipientPeerId, mention: ""), range: format.ranges[0].range)
}
value = string
} else {
let format = senderName != nil ? strings.Gift_Unique_OriginalInfoSender(senderName!, recipientName, dateString) : strings.Gift_Unique_OriginalInfo(recipientName, dateString)
let string = NSMutableAttributedString(string: format.string, font: tableFont, textColor: tableTextColor)
if let senderPeerId {
string.addAttribute(.foregroundColor, value: tableLinkColor, range: format.ranges[0].range)
string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention), value: TelegramPeerMention(peerId: senderPeerId, mention: ""), range: format.ranges[0].range)
string.addAttribute(.foregroundColor, value: tableLinkColor, range: format.ranges[1].range)
string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention), value: TelegramPeerMention(peerId: recipientPeerId, mention: ""), range: format.ranges[1].range)
} else {
string.addAttribute(.foregroundColor, value: tableLinkColor, range: format.ranges[0].range)
string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention), value: TelegramPeerMention(peerId: recipientPeerId, mention: ""), range: format.ranges[0].range)
}
value = string
}
percentage = nil
tag = nil
hasOriginalInfo = true
}
var items: [AnyComponentWithIdentity<Empty>] = []
items.append(
AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(
MultilineTextWithEntitiesComponent(
context: component.context,
animationCache: component.context.animationCache,
animationRenderer: component.context.animationRenderer,
placeholderColor: theme.list.mediaPlaceholderColor,
text: .plain(value),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
insets: id == "originalInfo" ? UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0) : .zero,
highlightColor: tableLinkColor.withAlphaComponent(0.1),
handleSpoilers: true,
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)
} else {
return nil
}
},
tapAction: { [weak state] attributes, _ in
guard let state else {
return
}
if let mention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention, let peer = state.peerMap[mention.peerId] {
state.openPeer(peer)
}
}
)
)
)
)
if let percentage, let tag {
items.append(AnyComponentWithIdentity(
id: AnyHashable(1),
component: AnyComponent(Button(
content: AnyComponent(ButtonContentComponent(
context: component.context,
text: formatPercentage(percentage),
color: theme.list.itemAccentColor
)),
action: { [weak state] in
state?.showAttributeInfo(tag: tag, text: strings.Gift_Unique_AttributeDescription(formatPercentage(percentage)).string)
}
).tagged(tag))
))
}
let itemComponent = AnyComponent(
HStack(items, spacing: 4.0)
)
tableItems.append(.init(
id: id,
title: title,
hasBackground: hasBackground,
component: itemComponent
))
}
}
let issuedString = presentationStringsFormattedNumber(uniqueGift.availability.issued, environment.dateTimeFormat.groupingSeparator)
let totalString = presentationStringsFormattedNumber(uniqueGift.availability.total, environment.dateTimeFormat.groupingSeparator)
tableItems.insert(.init(
id: "availability",
title: strings.Gift_Unique_Availability,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_Unique_Issued("\(issuedString)/\(totalString)").string, font: tableFont, textColor: tableTextColor)))
)
), at: hasOriginalInfo ? tableItems.count - 1 : tableItems.count)
} else {
if case let .soldOutGift(gift) = subject, let soldOut = gift.soldOut {
tableItems.append(.init(
id: "firstDate",
title: strings.Gift_View_FirstSale,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: soldOut.firstSale, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor)))
)
))
tableItems.append(.init(
id: "lastDate",
title: strings.Gift_View_LastSale,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: soldOut.lastSale, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor)))
)
))
} else if let date {
tableItems.append(.init(
id: "date",
title: strings.Gift_View_Date,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: date, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor)))
)
))
}
var finalStars = stars
if let upgradeStars, upgradeStars > 0 {
finalStars += upgradeStars
}
let valueString = "\(presentationStringsFormattedNumber(abs(Int32(finalStars)), dateTimeFormat.groupingSeparator))⭐️"
let valueAttributedString = NSMutableAttributedString(string: valueString, font: tableFont, textColor: tableTextColor)
let range = (valueAttributedString.string as NSString).range(of: "⭐️")
if range.location != NSNotFound {
valueAttributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range)
valueAttributedString.addAttribute(.baselineOffset, value: 1.0, range: range)
}
var canConvert = true
if let reference = subject.arguments?.reference, case let .peer(peerId, _) = reference {
if let peer = state.peerMap[peerId], case let .channel(channel) = peer, !channel.flags.contains(.isCreator) {
canConvert = false
}
}
if let convertStars, incoming && !converted && canConvert {
tableItems.append(.init(
id: "value_convert",
title: strings.Gift_View_Value,
component: AnyComponent(
HStack([
AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(MultilineTextWithEntitiesComponent(
context: component.context,
animationCache: component.context.animationCache,
animationRenderer: component.context.animationRenderer,
placeholderColor: theme.list.mediaPlaceholderColor,
text: .plain(valueAttributedString),
maximumNumberOfLines: 0
))
),
AnyComponentWithIdentity(
id: AnyHashable(1),
component: AnyComponent(Button(
content: AnyComponent(ButtonContentComponent(
context: component.context,
text: strings.Gift_View_Sale(strings.Gift_View_Sale_Stars(Int32(convertStars))).string,
color: theme.list.itemAccentColor
)),
action: { [weak state] in
state?.convertToStars()
}
))
)
], spacing: 4.0)
),
insets: UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 12.0)
))
} else {
tableItems.append(.init(
id: "value",
title: strings.Gift_View_Value,
component: AnyComponent(MultilineTextWithEntitiesComponent(
context: component.context,
animationCache: component.context.animationCache,
animationRenderer: component.context.animationRenderer,
placeholderColor: theme.list.mediaPlaceholderColor,
text: .plain(valueAttributedString),
maximumNumberOfLines: 0
)),
insets: UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 12.0)
))
}
if let limitTotal {
var remains: Int32 = limitRemains ?? 0
if let gift = state.starGiftsMap[giftId], let availability = gift.availability {
remains = availability.remains
}
let remainsString = presentationStringsFormattedNumber(remains, environment.dateTimeFormat.groupingSeparator)
let totalString = presentationStringsFormattedNumber(limitTotal, environment.dateTimeFormat.groupingSeparator)
tableItems.append(.init(
id: "availability",
title: strings.Gift_View_Availability,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_View_Availability_NewOf("\(remainsString)", "\(totalString)").string, font: tableFont, textColor: tableTextColor)))
)
))
}
if !soldOut && canUpgrade {
var items: [AnyComponentWithIdentity<Empty>] = []
items.append(
AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_View_Status_NonUnique, font: tableFont, textColor: tableTextColor))))
)
)
if incoming {
items.append(
AnyComponentWithIdentity(
id: AnyHashable(1),
component: AnyComponent(Button(
content: AnyComponent(ButtonContentComponent(
context: component.context,
text: strings.Gift_View_Status_Upgrade,
color: theme.list.itemAccentColor
)),
action: { [weak state] in
state?.requestUpgradePreview()
}
))
)
)
}
tableItems.append(.init(
id: "status",
title: strings.Gift_View_Status,
component: AnyComponent(
HStack(items, spacing: 4.0)
),
insets: UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 12.0)
))
}
if let text {
let attributedText = stringWithAppliedEntities(text, entities: entities ?? [], baseColor: tableTextColor, linkColor: tableLinkColor, baseFont: tableFont, linkFont: tableFont, boldFont: tableBoldFont, italicFont: tableItalicFont, boldItalicFont: tableBoldItalicFont, fixedFont: tableMonospaceFont, blockQuoteFont: tableFont, message: nil)
tableItems.append(.init(
id: "text",
title: nil,
component: AnyComponent(
MultilineTextWithEntitiesComponent(
context: component.context,
animationCache: component.context.animationCache,
animationRenderer: component.context.animationRenderer,
placeholderColor: theme.list.mediaPlaceholderColor,
text: .plain(attributedText),
maximumNumberOfLines: 0,
insets: UIEdgeInsets(top: 1.0, left: 0.0, bottom: 1.0, right: 0.0),
handleSpoilers: true
)
)
))
}
}
let table = table.update(
component: TableComponent(
theme: environment.theme,
items: tableItems
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude),
transition: context.transition
)
context.add(table
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY + table.size.height / 2.0))
.appear(.default(alpha: true))
.disappear(.default(alpha: true))
)
originY += table.size.height + 23.0
}
var resellStars: Int64?
var selling = false
if let uniqueGift {
resellStars = uniqueGift.resellStars
if let resellStars {
if incoming || ownerPeerId == component.context.account.peerId {
let priceButton = priceButton.update(
component: PlainButtonComponent(
content: AnyComponent(
PriceButtonComponent(price: presentationStringsFormattedNumber(Int32(resellStars), environment.dateTimeFormat.groupingSeparator))
),
effectAlignment: .center,
action: { [weak state] in
state?.resellGift(update: true)
},
animateScale: false
),
availableSize: CGSize(width: 150.0, height: 30.0),
transition: context.transition
)
context.add(priceButton
.position(CGPoint(x: environment.safeInsets.left + 16.0 + priceButton.size.width / 2.0, y: 28.0))
.appear(.default(scale: true, alpha: true))
.disappear(.default(scale: true, alpha: true))
)
}
if case let .uniqueGift(_, recipientPeerId) = component.subject, recipientPeerId != nil {
} else if ownerPeerId != component.context.account.peerId {
selling = true
}
}
}
if ((incoming && !converted && !upgraded) || exported || selling) && (!showUpgradePreview && !showWearPreview) {
let linkColor = theme.actionSheet.controlAccentColor
if state.cachedSmallChevronImage == nil || state.cachedSmallChevronImage?.1 !== environment.theme {
state.cachedSmallChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: linkColor)!, theme)
}
var addressToOpen: String?
var descriptionText: String
if let uniqueGift, selling {
let ownerName: String
if case let .peerId(peerId) = uniqueGift.owner {
ownerName = state.peerMap[peerId]?.compactDisplayTitle ?? ""
} else {
ownerName = ""
}
descriptionText = strings.Gift_View_SellingGiftInfo(ownerName).string
} else if let uniqueGift, let address = uniqueGift.giftAddress, case .address = uniqueGift.owner {
addressToOpen = address
descriptionText = strings.Gift_View_TonGiftAddressInfo
} else if savedToProfile {
descriptionText = isChannelGift ? strings.Gift_View_DisplayedInfoHide_Channel : strings.Gift_View_DisplayedInfoHide
} else if let upgradeStars, upgradeStars > 0 && !upgraded {
descriptionText = isChannelGift ? strings.Gift_View_HiddenInfoShow_Channel : strings.Gift_View_HiddenInfoShow
} else {
if let _ = uniqueGift {
descriptionText = isChannelGift ? strings.Gift_View_UniqueHiddenInfo_Channel : strings.Gift_View_UniqueHiddenInfo
} else {
descriptionText = isChannelGift ? strings.Gift_View_HiddenInfo_Channel : strings.Gift_View_HiddenInfo
}
}
let textFont = Font.regular(13.0)
let textColor = theme.list.itemSecondaryTextColor
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: textFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
})
descriptionText = descriptionText.replacingOccurrences(of: " >]", with: "\u{00A0}>]")
let attributedString = parseMarkdownIntoAttributedString(descriptionText, attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString
if let range = attributedString.string.range(of: ">"), let chevronImage = state.cachedSmallChevronImage?.0 {
attributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedString.string))
}
originY -= 5.0
let additionalText = additionalText.update(
component: MultilineTextComponent(
text: .plain(attributedString),
horizontalAlignment: .center,
maximumNumberOfLines: 5,
lineSpacing: 0.2,
highlightColor: linkColor.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 state] attributes, _ in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
if let addressToOpen {
state?.openAddress(addressToOpen)
} else {
state?.updateSavedToProfile(!savedToProfile)
Queue.mainQueue().after(0.6, {
state?.dismiss(animated: false)
})
}
}
}
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
context.add(additionalText
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY + additionalText.size.height / 2.0))
.appear(.default(alpha: true))
.disappear(.default(alpha: true))
)
originY += additionalText.size.height
originY += 16.0
}
let buttonSize = CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0)
let buttonBackground = ButtonComponent.Background(
color: theme.list.itemCheckColors.fillColor,
foreground: theme.list.itemCheckColors.foregroundColor,
pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
)
let buttonChild: _UpdatedChildComponent
if showWearPreview, let uniqueGift {
let buttonContent: AnyComponentWithIdentity<Empty>
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 })
let requiredLevel = Int(BoostSubject.wearGift.requiredLevel(group: false, context: component.context, configuration: premiumConfiguration))
var canWear = true
if isChannelGift, case let .channel(channel) = state.peerMap[wearOwnerPeerId], (channel.approximateBoostLevel ?? 0) < requiredLevel {
canWear = false
buttonContent = AnyComponentWithIdentity(
id: AnyHashable("wear_channel"),
component: AnyComponent(
VStack([
AnyComponentWithIdentity(
id: AnyHashable("label"),
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_Wear_Start, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center))))
),
AnyComponentWithIdentity(
id: AnyHashable("level"),
component: AnyComponent(PremiumLockButtonSubtitleComponent(
count: requiredLevel,
theme: theme,
strings: strings
))
)
], spacing: 3.0)
)
)
} else if !isChannelGift && !component.context.isPremium {
canWear = false
buttonContent = AnyComponentWithIdentity(
id: AnyHashable("wear_premium"),
component: AnyComponent(
HStack([
AnyComponentWithIdentity(
id: AnyHashable("icon"),
component: AnyComponent(BundleIconComponent(name: "Chat/Stickers/Lock", tintColor: theme.list.itemCheckColors.foregroundColor))
),
AnyComponentWithIdentity(
id: AnyHashable("label"),
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_Wear_Start, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center))))
)
], spacing: 3.0)
)
)
} else {
buttonContent = AnyComponentWithIdentity(
id: AnyHashable("wear"),
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_Wear_Start, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center))))
)
}
buttonChild = button.update(
component: ButtonComponent(
background: buttonBackground,
content: buttonContent,
isEnabled: true,
displaysProgress: false,
action: { [weak state] in
if let state {
let context = component.context
if !canWear, let controller = controller() as? GiftViewScreen {
controller.dismissAllTooltips()
if isChannelGift {
state.levelsDisposable.set(combineLatest(
queue: Queue.mainQueue(),
context.engine.peers.getChannelBoostStatus(peerId: wearOwnerPeerId),
context.engine.peers.getMyBoostStatus()
).startStandalone(next: { [weak controller, weak state] boostStatus, myBoostStatus in
guard let controller, let state, let boostStatus, let myBoostStatus else {
return
}
state.dismiss(animated: true)
let levelsController = context.sharedContext.makePremiumBoostLevelsController(context: context, peerId: wearOwnerPeerId, subject: .wearGift, boostStatus: boostStatus, myBoostStatus: myBoostStatus, forceDark: false, openStats: nil)
controller.push(levelsController)
HapticFeedback().impact(.light)
}))
} else {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let text = strings.Gift_View_TooltipPremiumWearing
let tooltipController = UndoOverlayController(
presentationData: presentationData,
content: .premiumPaywall(title: nil, text: text, customUndoText: nil, timeout: nil, linkAction: nil),
position: .bottom,
animateInAsReplacement: false,
appearance: UndoOverlayController.Appearance(sideInset: 16.0, bottomInset: 62.0),
action: { [weak controller, weak state] action in
if case .info = action {
controller?.dismissAllTooltips()
let premiumController = context.sharedContext.makePremiumIntroController(context: context, source: .messageEffects, forceDark: false, dismissed: nil)
controller?.push(premiumController)
Queue.mainQueue().after(0.6, {
state?.dismiss(animated: false)
})
}
return false
}
)
controller.present(tooltipController, in: .window(.root))
}
} else {
state.commitWear(uniqueGift)
if case .wearPreview = component.subject {
state.dismiss(animated: true)
} else {
Queue.mainQueue().after(0.2) {
state.showAttributeInfo(tag: state.statusTag, text: strings.Gift_View_PutOn("\(uniqueGift.title) #\(presentationStringsFormattedNumber(uniqueGift.number, environment.dateTimeFormat.groupingSeparator))").string)
}
}
}
}
}),
availableSize: buttonSize,
transition: context.transition
)
} else if state.inUpgradePreview {
if state.cachedStarImage == nil || state.cachedStarImage?.1 !== theme {
state.cachedStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: theme.list.itemCheckColors.foregroundColor)!, theme)
}
var upgradeString = strings.Gift_Upgrade_Upgrade
if let upgradeForm = state.upgradeForm, let price = upgradeForm.invoice.prices.first?.amount {
upgradeString += " # \(presentationStringsFormattedNumber(Int32(price), environment.dateTimeFormat.groupingSeparator))"
}
let buttonTitle = subject.arguments?.upgradeStars != nil ? strings.Gift_Upgrade_Confirm : upgradeString
let buttonAttributedString = NSMutableAttributedString(string: buttonTitle, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)
if let range = buttonAttributedString.string.range(of: "#"), let starImage = state.cachedStarImage?.0 {
buttonAttributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: buttonAttributedString.string))
buttonAttributedString.addAttribute(.foregroundColor, value: 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))
}
buttonChild = button.update(
component: ButtonComponent(
background: buttonBackground,
content: AnyComponentWithIdentity(
id: AnyHashable("upgrade"),
component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString)))
),
isEnabled: true,
displaysProgress: state.inProgress,
action: { [weak state] in
state?.commitUpgrade()
}),
availableSize: buttonSize,
transition: context.transition
)
} else if upgraded, let upgradeMessageIdId = subject.arguments?.upgradeMessageId, let originalMessageId = subject.arguments?.messageId {
let upgradeMessageId = MessageId(peerId: originalMessageId.peerId, namespace: originalMessageId.namespace, id: upgradeMessageIdId)
let buttonTitle = strings.Gift_View_ViewUpgraded
buttonChild = button.update(
component: ButtonComponent(
background: buttonBackground,
content: AnyComponentWithIdentity(
id: AnyHashable("button"),
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: buttonTitle, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center))))
),
isEnabled: true,
displaysProgress: false,
action: { [weak state] in
state?.dismiss(animated: true)
state?.viewUpgradedGift(messageId: upgradeMessageId)
}),
availableSize: buttonSize,
transition: context.transition
)
} else if incoming && !converted && !upgraded, let upgradeStars, upgradeStars > 0 {
let buttonTitle = strings.Gift_View_UpgradeForFree
buttonChild = button.update(
component: ButtonComponent(
background: buttonBackground.withIsShimmering(true),
content: AnyComponentWithIdentity(
id: AnyHashable("freeUpgrade"),
component: AnyComponent(HStack([
AnyComponentWithIdentity(id: 0, component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: buttonTitle, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center))
))),
AnyComponentWithIdentity(id: 1, component: AnyComponent(
LottieComponent(
content: LottieComponent.AppBundleContent(
name: "GiftUpgrade"
),
size: CGSize(width: 30.0, height: 30.0),
loop: true
)
))
], spacing: 5.0))
),
isEnabled: true,
displaysProgress: state.inProgress,
action: { [weak state] in
state?.requestUpgradePreview()
}
),
availableSize: buttonSize,
transition: context.transition
)
} else if incoming && !converted && !savedToProfile {
let buttonTitle = isChannelGift ? strings.Gift_View_Display_Channel : strings.Gift_View_Display
buttonChild = button.update(
component: ButtonComponent(
background: buttonBackground,
content: AnyComponentWithIdentity(
id: AnyHashable("button"),
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: buttonTitle, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center))))
),
isEnabled: true,
displaysProgress: state.inProgress,
action: { [weak state] in
state?.updateSavedToProfile(!savedToProfile)
}),
availableSize: buttonSize,
transition: context.transition
)
} else if !incoming, let resellStars, !isMyUniqueGift {
if state.cachedStarImage == nil || state.cachedStarImage?.1 !== theme {
state.cachedStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: theme.list.itemCheckColors.foregroundColor)!, theme)
}
var upgradeString = strings.Gift_View_BuyFor
upgradeString += " # \(presentationStringsFormattedNumber(Int32(resellStars), environment.dateTimeFormat.groupingSeparator))"
let buttonTitle = subject.arguments?.upgradeStars != nil ? strings.Gift_Upgrade_Confirm : upgradeString
let buttonAttributedString = NSMutableAttributedString(string: buttonTitle, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)
if let range = buttonAttributedString.string.range(of: "#"), let starImage = state.cachedStarImage?.0 {
buttonAttributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: buttonAttributedString.string))
buttonAttributedString.addAttribute(.foregroundColor, value: 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))
}
buttonChild = button.update(
component: ButtonComponent(
background: buttonBackground,
content: AnyComponentWithIdentity(
id: AnyHashable("buy"),
component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString)))
),
isEnabled: true,
displaysProgress: state.inProgress,
action: { [weak state] in
state?.commitBuy()
}),
availableSize: buttonSize,
transition: context.transition
)
} else {
buttonChild = button.update(
component: ButtonComponent(
background: buttonBackground,
content: AnyComponentWithIdentity(
id: AnyHashable("ok"),
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Common_OK, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center))))
),
isEnabled: true,
displaysProgress: state.inProgress,
action: { [weak state] in
state?.dismiss(animated: true)
}),
availableSize: buttonSize,
transition: context.transition
)
}
let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: originY), size: buttonChild.size)
context.add(buttonChild
.position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY))
.cornerRadius(10.0)
)
originY += buttonChild.size.height
originY += 7.0
context.add(buttons
.position(CGPoint(x: context.availableSize.width - environment.safeInsets.left - 16.0 - buttons.size.width / 2.0, y: 28.0))
)
let effectiveBottomInset: CGFloat = environment.metrics.isTablet ? 0.0 : environment.safeInsets.bottom
return CGSize(width: context.availableSize.width, height: originY + 5.0 + effectiveBottomInset)
}
}
}
final class GiftViewSheetComponent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let subject: GiftViewScreen.Subject
init(
context: AccountContext,
subject: GiftViewScreen.Subject
) {
self.context = context
self.subject = subject
}
static func ==(lhs: GiftViewSheetComponent, rhs: GiftViewSheetComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.subject != rhs.subject {
return false
}
return true
}
static var body: Body {
let sheet = Child(SheetComponent<EnvironmentType>.self)
let animateOut = StoredActionSlot(Action<Void>.self)
let sheetExternalState = SheetComponent<EnvironmentType>.ExternalState()
return { context in
let environment = context.environment[EnvironmentType.self]
let controller = environment.controller
let sheet = sheet.update(
component: SheetComponent<EnvironmentType>(
content: AnyComponent<EnvironmentType>(GiftViewSheetContent(
context: context.component.context,
subject: context.component.subject,
animateOut: animateOut,
getController: controller
)),
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
followContentSizeChanges: true,
clipsContent: true,
externalState: sheetExternalState,
animateOut: animateOut,
onPan: {
if let controller = controller() as? GiftViewScreen {
controller.dismissAllTooltips()
}
},
willDismiss: {
if let controller = controller() as? GiftViewScreen {
controller.dismissBalanceOverlay()
}
}
),
environment: {
environment
SheetComponentEnvironment(
isDisplaying: environment.value.isVisible,
isCentered: environment.metrics.widthClass == .regular,
hasInputHeight: !environment.inputHeight.isZero,
regularMetricsSize: CGSize(width: 430.0, height: 900.0),
dismiss: { animated in
if animated {
if let controller = controller() as? GiftViewScreen {
controller.dismissAllTooltips()
controller.dismissBalanceOverlay()
animateOut.invoke(Action { _ in
controller.dismiss(completion: nil)
})
}
} else {
if let controller = controller() as? GiftViewScreen {
controller.dismissAllTooltips()
controller.dismissBalanceOverlay()
controller.dismiss(completion: nil)
}
}
}
)
},
availableSize: context.availableSize,
transition: context.transition
)
context.add(sheet
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
)
if let controller = controller(), !controller.automaticallyControlPresentationContextLayout {
let layout = ContainerViewLayout(
size: context.availableSize,
metrics: environment.metrics,
deviceMetrics: environment.deviceMetrics,
intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: max(environment.safeInsets.bottom, sheetExternalState.contentHeight), right: 0.0),
safeInsets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: 0.0, right: environment.safeInsets.right),
additionalInsets: .zero,
statusBarHeight: environment.statusBarHeight,
inputHeight: nil,
inputHeightIsInteractivellyChanging: false,
inVoiceOver: false
)
controller.presentationContext.containerLayoutUpdated(layout, transition: context.transition.containedViewLayoutTransition)
}
return context.availableSize
}
}
}
public class GiftViewScreen: ViewControllerComponentContainer {
public enum Subject: Equatable {
case message(EngineMessage)
case uniqueGift(StarGift.UniqueGift, EnginePeer.Id?)
case profileGift(EnginePeer.Id, ProfileGiftsContext.State.StarGift)
case soldOutGift(StarGift.Gift)
case upgradePreview([StarGift.UniqueGift.Attribute], String)
case wearPreview(StarGift.UniqueGift)
var arguments: (peerId: EnginePeer.Id?, fromPeerId: EnginePeer.Id?, fromPeerName: String?, messageId: EngineMessage.Id?, reference: StarGiftReference?, incoming: Bool, gift: StarGift, date: Int32, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, pinnedToTop: Bool?, converted: Bool, upgraded: Bool, refunded: Bool, canUpgrade: Bool, upgradeStars: Int64?, transferStars: Int64?, resellStars: Int64?, canExportDate: Int32?, upgradeMessageId: Int32?, canTransferDate: Int32?, canResaleDate: Int32?)? {
switch self {
case let .message(message):
if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction {
switch action.action {
case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, isRefunded, upgradeMessageId, peerId, senderId, savedId):
var reference: StarGiftReference
if let peerId, let savedId {
reference = .peer(peerId: peerId, id: savedId)
} else {
reference = .message(messageId: message.id)
}
return (message.id.peerId, senderId ?? message.author?.id, message.author?.compactDisplayTitle, message.id, reference, message.flags.contains(.Incoming), gift, message.timestamp, convertStars, text, entities, nameHidden, savedToProfile, nil, converted, upgraded, isRefunded, canUpgrade, upgradeStars, nil, nil, nil, upgradeMessageId, nil, nil)
case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, _, peerId, senderId, savedId, _, canTransferDate, canResaleDate):
var reference: StarGiftReference
if let peerId, let savedId {
reference = .peer(peerId: peerId, id: savedId)
} else {
reference = .message(messageId: message.id)
}
var incoming = false
if isUpgrade {
if message.author?.id != message.id.peerId {
incoming = true
}
} else if isTransferred {
if message.author?.id != message.id.peerId {
incoming = true
}
} else {
incoming = message.flags.contains(.Incoming)
}
var resellStars: Int64?
if case let .unique(uniqueGift) = gift {
resellStars = uniqueGift.resellStars
}
return (message.id.peerId, senderId ?? message.author?.id, message.author?.compactDisplayTitle, message.id, reference, incoming, gift, message.timestamp, nil, nil, nil, false, savedToProfile, nil, false, false, false, false, nil, transferStars, resellStars, canExportDate, nil, canTransferDate, canResaleDate)
default:
return nil
}
}
case let .uniqueGift(gift, _), let .wearPreview(gift):
return (nil, nil, nil, nil, nil, false, .unique(gift), 0, nil, nil, nil, false, false, nil, false, false, false, false, nil, nil, gift.resellStars, nil, nil, nil, nil)
case let .profileGift(peerId, gift):
var messageId: EngineMessage.Id?
if case let .message(messageIdValue) = gift.reference {
messageId = messageIdValue
}
var resellStars: Int64?
if case let .unique(uniqueGift) = gift.gift {
resellStars = uniqueGift.resellStars
}
return (peerId, gift.fromPeer?.id, gift.fromPeer?.compactDisplayTitle, messageId, gift.reference, false, gift.gift, gift.date, gift.convertStars, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, gift.pinnedToTop, false, false, false, gift.canUpgrade, gift.upgradeStars, gift.transferStars, resellStars, gift.canExportDate, nil, gift.canTransferDate, gift.canResaleDate)
case .soldOutGift:
return nil
case .upgradePreview:
return nil
}
return nil
}
}
private let context: AccountContext
private let subject: GiftViewScreen.Subject
fileprivate var showBalance = false {
didSet {
self.requestLayout(transition: .immediate)
}
}
private let balanceOverlay = ComponentView<Empty>()
fileprivate let updateSavedToProfile: ((StarGiftReference, Bool) -> Void)?
fileprivate let convertToStars: (() -> Void)?
fileprivate let transferGift: ((Bool, EnginePeer.Id) -> Signal<Never, TransferStarGiftError>)?
fileprivate let upgradeGift: ((Int64?, Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>)?
fileprivate let buyGift: ((String, EnginePeer.Id, Int64?) -> Signal<Never, BuyStarGiftError>)?
fileprivate let updateResellStars: ((Int64?) -> Signal<Never, UpdateStarGiftPriceError>)?
fileprivate let togglePinnedToTop: ((Bool) -> Bool)?
fileprivate let shareStory: ((StarGift.UniqueGift) -> Void)?
public var disposed: () -> Void = {}
public init(
context: AccountContext,
subject: GiftViewScreen.Subject,
allSubjects: [GiftViewScreen.Subject]? = nil,
index: Int? = nil,
forceDark: Bool = false,
updateSavedToProfile: ((StarGiftReference, Bool) -> Void)? = nil,
convertToStars: (() -> Void)? = nil,
transferGift: ((Bool, EnginePeer.Id) -> Signal<Never, TransferStarGiftError>)? = nil,
upgradeGift: ((Int64?, Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>)? = nil,
buyGift: ((String, EnginePeer.Id, Int64?) -> Signal<Never, BuyStarGiftError>)? = nil,
updateResellStars: ((Int64?) -> Signal<Never, UpdateStarGiftPriceError>)? = nil,
togglePinnedToTop: ((Bool) -> Bool)? = nil,
shareStory: ((StarGift.UniqueGift) -> Void)? = nil
) {
self.context = context
self.subject = subject
self.updateSavedToProfile = updateSavedToProfile
self.convertToStars = convertToStars
self.transferGift = transferGift
self.upgradeGift = upgradeGift
self.buyGift = buyGift
self.updateResellStars = updateResellStars
self.togglePinnedToTop = togglePinnedToTop
self.shareStory = shareStory
var items: [GiftPagerComponent.Item] = [GiftPagerComponent.Item(id: 0, subject: subject)]
if let allSubjects, !allSubjects.isEmpty {
items.removeAll()
for i in 0 ..< allSubjects.count {
items.append(GiftPagerComponent.Item(id: i, subject: allSubjects[i]))
}
}
var dismissTooltipsImpl: (() -> Void)?
super.init(
context: context,
component: GiftPagerComponent(
context: context,
items: items,
index: index ?? 0,
updated: { _, _ in
dismissTooltipsImpl?()
}
),
navigationBarAppearance: .none,
statusBarStyle: .ignore,
theme: forceDark ? .dark : .default
)
dismissTooltipsImpl = { [weak self] in
self?.dismissAllTooltips()
}
self.navigationPresentation = .flatModal
self.automaticallyControlPresentationContextLayout = false
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.disposed()
}
public override func viewDidLoad() {
super.viewDidLoad()
self.view.disablesInteractiveModalDismiss = true
if let arguments = self.subject.arguments, let _ = self.subject.arguments?.resellStars {
if case let .unique(uniqueGift) = arguments.gift, case .peerId(self.context.account.peerId) = uniqueGift.owner {
} else {
self.showBalance = true
}
}
}
public override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.dismissAllTooltips()
}
fileprivate func animateSuccess() {
self.navigationController?.view.addSubview(ConfettiView(frame: self.view.bounds))
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
self.present(UndoOverlayController(presentationData: presentationData, content: .universal(
animation: "GiftUpgraded",
scale: 0.066,
colors: [:],
title: presentationData.strings.Gift_Upgrade_Succeed_Title,
text: presentationData.strings.Gift_Upgrade_Succeed_Text,
customUndoText: nil,
timeout: 4.0
), elevatedLayout: false, position: .bottom, action: { _ in return true }), in: .current)
}
public func dismissAnimated() {
self.dismissAllTooltips()
if let view = self.node.hostView.findTaggedView(tag: SheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? SheetComponent<ViewControllerComponentContainer.Environment>.View {
view.dismissAnimated()
}
self.dismissBalanceOverlay()
}
fileprivate func dismissBalanceOverlay() {
if let view = self.balanceOverlay.view, view.superview != nil {
view.layer.animateScale(from: 1.0, to: 0.8, duration: 0.4, removeOnCompletion: false)
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
}
}
fileprivate func dismissAllTooltips() {
self.window?.forEachController({ controller in
if let controller = controller as? TooltipScreen {
controller.dismiss(inPlace: false)
}
if let controller = controller as? UndoOverlayController {
controller.dismiss()
}
})
self.forEachController({ controller in
if let controller = controller as? TooltipScreen {
controller.dismiss(inPlace: false)
}
if let controller = controller as? UndoOverlayController {
controller.dismiss()
}
return true
})
}
public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
if self.showBalance {
let context = self.context
let insets = layout.insets(options: .statusBar)
let balanceSize = self.balanceOverlay.update(
transition: .immediate,
component: AnyComponent(
StarsBalanceOverlayComponent(
context: context,
theme: context.sharedContext.currentPresentationData.with { $0 }.theme,
action: { [weak self] in
guard let self, let starsContext = context.starsContext, let navigationController = self.navigationController as? NavigationController else {
return
}
self.dismissAnimated()
let _ = (context.engine.payments.starsTopUpOptions()
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { options in
let controller = context.sharedContext.makeStarsPurchaseScreen(
context: context,
starsContext: starsContext,
options: options,
purpose: .generic,
completion: { _ in }
)
navigationController.pushViewController(controller)
})
}
)
),
environment: {},
containerSize: layout.size
)
if let view = self.balanceOverlay.view {
if view.superview == nil {
self.view.addSubview(view)
view.layer.animatePosition(from: CGPoint(x: 0.0, y: -64.0), to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
view.layer.animateSpring(from: 0.8 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5, initialVelocity: 0.0, removeOnCompletion: true, additive: false, completion: nil)
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
view.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - balanceSize.width) / 2.0), y: insets.top + 5.0), size: balanceSize)
}
} else if let view = self.balanceOverlay.view, view.superview != nil {
view.alpha = 0.0
view.layer.animateScale(from: 1.0, to: 0.8, duration: 0.4)
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, completion: { _ in
view.removeFromSuperview()
view.alpha = 1.0
})
}
}
}
private func formatPercentage(_ value: Float) -> String {
return String(format: "%0.1f%%", value).replacingOccurrences(of: ".0%", with: "%").replacingOccurrences(of: ",0%", with: "%")
}
private final class PeerCellComponent: Component {
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let peer: EnginePeer?
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, peer: EnginePeer?) {
self.context = context
self.theme = theme
self.strings = strings
self.peer = peer
}
static func ==(lhs: PeerCellComponent, rhs: PeerCellComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.peer != rhs.peer {
return false
}
return true
}
final class View: UIView {
private let avatarNode: AvatarNode
private let text = ComponentView<Empty>()
private var component: PeerCellComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 8.0))
super.init(frame: frame)
self.addSubnode(self.avatarNode)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: PeerCellComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let avatarSize = CGSize(width: 22.0, height: 22.0)
let spacing: CGFloat = 6.0
let peerName: String
let avatarOverride: AvatarNodeImageOverride?
if let peerValue = component.peer {
peerName = peerValue.compactDisplayTitle
avatarOverride = nil
} else {
peerName = component.strings.Gift_View_HiddenName
avatarOverride = .anonymousSavedMessagesIcon(isColored: true)
}
let avatarNaturalSize = CGSize(width: 40.0, height: 40.0)
self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: component.peer, overrideImage: avatarOverride)
self.avatarNode.bounds = CGRect(origin: .zero, size: avatarNaturalSize)
let textSize = self.text.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: peerName, font: Font.regular(15.0), textColor: component.peer != nil ? component.theme.list.itemAccentColor : component.theme.list.itemPrimaryTextColor, paragraphAlignment: .left))
)
),
environment: {},
containerSize: CGSize(width: availableSize.width - avatarSize.width - spacing, height: availableSize.height)
)
let size = CGSize(width: avatarSize.width + textSize.width + spacing, height: textSize.height)
let avatarFrame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((size.height - avatarSize.height) / 2.0)), size: avatarSize)
self.avatarNode.frame = avatarFrame
if let view = self.text.view {
if view.superview == nil {
self.addSubview(view)
}
let textFrame = CGRect(origin: CGPoint(x: avatarSize.width + spacing, y: floorToScreenPixels((size.height - textSize.height) / 2.0)), size: textSize)
transition.setFrame(view: view, frame: textFrame)
}
return size
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private final class ButtonContentComponent: Component {
let context: AccountContext
let text: String
let color: UIColor
public init(
context: AccountContext,
text: String,
color: UIColor
) {
self.context = context
self.text = text
self.color = color
}
public static func ==(lhs: ButtonContentComponent, rhs: ButtonContentComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.text != rhs.text {
return false
}
if lhs.color != rhs.color {
return false
}
return true
}
public final class View: UIView {
private var component: ButtonContentComponent?
private weak var componentState: EmptyComponentState?
private let backgroundLayer = SimpleLayer()
private let title = ComponentView<Empty>()
override init(frame: CGRect) {
super.init(frame: frame)
self.layer.addSublayer(self.backgroundLayer)
self.backgroundLayer.masksToBounds = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: ButtonContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.componentState = state
let attributedText = NSAttributedString(string: component.text, font: Font.regular(11.0), textColor: component.color)
let titleSize = self.title.update(
transition: transition,
component: AnyComponent(
MultilineTextWithEntitiesComponent(
context: component.context,
animationCache: component.context.animationCache,
animationRenderer: component.context.animationRenderer,
placeholderColor: .white,
text: .plain(attributedText)
)
),
environment: {},
containerSize: availableSize
)
let padding: CGFloat = 6.0
let size = CGSize(width: titleSize.width + padding * 2.0, height: 18.0)
let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
self.addSubview(titleView)
}
transition.setFrame(view: titleView, frame: titleFrame)
}
let backgroundColor = component.color.withAlphaComponent(0.1)
self.backgroundLayer.backgroundColor = backgroundColor.cgColor
transition.setFrame(layer: self.backgroundLayer, frame: CGRect(origin: .zero, size: size))
self.backgroundLayer.cornerRadius = size.height / 2.0
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private struct GiftConfiguration {
static var defaultValue: GiftConfiguration {
return GiftConfiguration(convertToStarsPeriod: 90 * 86400)
}
let convertToStarsPeriod: Int32
fileprivate init(convertToStarsPeriod: Int32) {
self.convertToStarsPeriod = convertToStarsPeriod
}
static func with(appConfiguration: AppConfiguration) -> GiftConfiguration {
if let data = appConfiguration.data {
var convertToStarsPeriod: Int32?
if let value = data["stargifts_convert_period_max"] as? Double {
convertToStarsPeriod = Int32(value)
}
return GiftConfiguration(convertToStarsPeriod: convertToStarsPeriod ?? GiftConfiguration.defaultValue.convertToStarsPeriod)
} else {
return .defaultValue
}
}
}
private final class ParagraphComponent: CombinedComponent {
let title: String
let titleColor: UIColor
let text: String
let textColor: UIColor
let accentColor: UIColor
let iconName: String
let iconColor: UIColor
let badge: String?
let action: () -> Void
public init(
title: String,
titleColor: UIColor,
text: String,
textColor: UIColor,
accentColor: UIColor,
iconName: String,
iconColor: UIColor,
badge: String? = nil,
action: @escaping () -> Void = {}
) {
self.title = title
self.titleColor = titleColor
self.text = text
self.textColor = textColor
self.accentColor = accentColor
self.iconName = iconName
self.iconColor = iconColor
self.badge = badge
self.action = action
}
static func ==(lhs: ParagraphComponent, rhs: ParagraphComponent) -> Bool {
if lhs.title != rhs.title {
return false
}
if lhs.titleColor != rhs.titleColor {
return false
}
if lhs.text != rhs.text {
return false
}
if lhs.textColor != rhs.textColor {
return false
}
if lhs.accentColor != rhs.accentColor {
return false
}
if lhs.iconName != rhs.iconName {
return false
}
if lhs.iconColor != rhs.iconColor {
return false
}
if lhs.badge != rhs.badge {
return false
}
return true
}
static var body: Body {
let title = Child(MultilineTextComponent.self)
let text = Child(MultilineTextComponent.self)
let icon = Child(BundleIconComponent.self)
let badgeBackground = Child(RoundedRectangle.self)
let badgeText = Child(MultilineTextComponent.self)
return { context in
let component = context.component
let leftInset: CGFloat = 32.0
let rightInset: CGFloat = 24.0
let textSideInset: CGFloat = leftInset + 8.0
let spacing: CGFloat = 5.0
let textTopInset: CGFloat = 9.0
let title = title.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(
string: component.title,
font: Font.semibold(15.0),
textColor: component.titleColor,
paragraphAlignment: .natural
)),
horizontalAlignment: .center,
maximumNumberOfLines: 1
),
availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
let textFont = Font.regular(15.0)
let boldTextFont = Font.semibold(15.0)
let textColor = component.textColor
let accentColor = component.accentColor
let markdownAttributes = MarkdownAttributes(
body: MarkdownAttributeSet(font: textFont, textColor: textColor),
bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor),
link: MarkdownAttributeSet(font: textFont, textColor: accentColor),
linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
}
)
let text = text.update(
component: MultilineTextComponent(
text: .markdown(text: component.text, attributes: markdownAttributes),
horizontalAlignment: .natural,
maximumNumberOfLines: 0,
lineSpacing: 0.2,
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
} else {
return nil
}
},
tapAction: { _, _ in
component.action()
}
),
availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: context.availableSize.height),
transition: .immediate
)
let icon = icon.update(
component: BundleIconComponent(
name: component.iconName,
tintColor: component.iconColor
),
availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height),
transition: .immediate
)
context.add(title
.position(CGPoint(x: textSideInset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0))
)
if let badge = component.badge {
let badgeText = badgeText.update(
component: MultilineTextComponent(text: .plain(NSAttributedString(string: badge, font: Font.semibold(11.0), textColor: .white))),
availableSize: context.availableSize,
transition: context.transition
)
let badgeWidth = badgeText.size.width + 7.0
let badgeBackground = badgeBackground.update(
component: RoundedRectangle(
color: component.accentColor,
cornerRadius: 5.0),
availableSize: CGSize(width: badgeWidth, height: 16.0),
transition: context.transition
)
context.add(badgeBackground
.position(CGPoint(x: textSideInset + title.size.width + badgeWidth / 2.0 + 5.0, y: textTopInset + title.size.height / 2.0))
)
context.add(badgeText
.position(CGPoint(x: textSideInset + title.size.width + badgeWidth / 2.0 + 5.0, y: textTopInset + title.size.height / 2.0))
)
}
context.add(text
.position(CGPoint(x: textSideInset + text.size.width / 2.0, y: textTopInset + title.size.height + spacing + text.size.height / 2.0))
)
context.add(icon
.position(CGPoint(x: 15.0, y: textTopInset + 18.0))
)
return CGSize(width: context.availableSize.width, height: textTopInset + title.size.height + text.size.height + 20.0)
}
}
}
private final class GiftViewContextReferenceContentSource: ContextReferenceContentSource {
private let controller: ViewController
private let sourceNode: ASDisplayNode
init(controller: ViewController, sourceNode: ASDisplayNode) {
self.controller = controller
self.sourceNode = sourceNode
}
func transitionInfo() -> ContextControllerReferenceViewInfo? {
return ContextControllerReferenceViewInfo(referenceView: self.sourceNode.view, contentAreaInScreenSpace: UIScreen.main.bounds)
}
}
private final class HeaderButtonComponent: CombinedComponent {
let title: String
let iconName: String
let isLocked: Bool
public init(
title: String,
iconName: String,
isLocked: Bool = false
) {
self.title = title
self.iconName = iconName
self.isLocked = isLocked
}
static func ==(lhs: HeaderButtonComponent, rhs: HeaderButtonComponent) -> Bool {
if lhs.title != rhs.title {
return false
}
if lhs.iconName != rhs.iconName {
return false
}
if lhs.isLocked != rhs.isLocked {
return false
}
return true
}
static var body: Body {
let background = Child(RoundedRectangle.self)
let title = Child(MultilineTextComponent.self)
let icon = Child(BundleIconComponent.self)
let lockIcon = Child(BundleIconComponent.self)
return { context in
let component = context.component
let background = background.update(
component: RoundedRectangle(
color: UIColor.white.withAlphaComponent(0.16),
cornerRadius: 10.0
),
availableSize: context.availableSize,
transition: .immediate
)
context.add(background
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
)
let icon = icon.update(
component: BundleIconComponent(
name: component.iconName,
tintColor: UIColor.white
),
availableSize: context.availableSize,
transition: .immediate
)
context.add(icon
.position(CGPoint(x: context.availableSize.width / 2.0, y: 22.0))
)
let title = title.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(
string: component.title,
font: Font.regular(11.0),
textColor: UIColor.white,
paragraphAlignment: .natural
)),
horizontalAlignment: .center,
maximumNumberOfLines: 1
),
availableSize: CGSize(width: context.availableSize.width - 16.0, height: context.availableSize.height),
transition: .immediate
)
var totalTitleWidth = title.size.width
var titleOriginX = context.availableSize.width / 2.0 - totalTitleWidth / 2.0
if component.isLocked {
let titleSpacing: CGFloat = 2.0
let lockIcon = lockIcon.update(
component: BundleIconComponent(
name: "Chat List/StatusLockIcon",
tintColor: UIColor.white
),
availableSize: context.availableSize,
transition: .immediate
)
totalTitleWidth += lockIcon.size.width + titleSpacing
titleOriginX = context.availableSize.width / 2.0 - totalTitleWidth / 2.0
context.add(lockIcon
.position(CGPoint(x: titleOriginX + lockIcon.size.width / 2.0, y: 42.0))
)
titleOriginX += lockIcon.size.width + titleSpacing
}
context.add(title
.position(CGPoint(x: titleOriginX + title.size.width / 2.0, y: 42.0))
)
return context.availableSize
}
}
}
private final class AvatarComponent: Component {
let context: AccountContext
let theme: PresentationTheme
let peer: EnginePeer
init(context: AccountContext, theme: PresentationTheme, peer: EnginePeer) {
self.context = context
self.theme = theme
self.peer = peer
}
static func ==(lhs: AvatarComponent, rhs: AvatarComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.peer != rhs.peer {
return false
}
return true
}
final class View: UIView {
private let avatarNode: AvatarNode
private var component: AvatarComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 42.0))
super.init(frame: frame)
self.addSubnode(self.avatarNode)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: AvatarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
self.avatarNode.frame = CGRect(origin: .zero, size: availableSize)
self.avatarNode.setPeer(
context: component.context,
theme: component.theme,
peer: component.peer,
synchronousLoad: true
)
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private struct GiftViewConfiguration {
public static var defaultValue: GiftViewConfiguration {
return GiftViewConfiguration(explorerUrl: "https://tonviewer.com")
}
public let explorerUrl: String
fileprivate init(explorerUrl: String) {
self.explorerUrl = explorerUrl
}
public static func with(appConfiguration: AppConfiguration) -> GiftViewConfiguration {
if let data = appConfiguration.data, let value = data["ton_blockchain_explorer_url"] as? String {
return GiftViewConfiguration(explorerUrl: value)
} else {
return .defaultValue
}
}
}