mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2026-03-31 02:58:57 +00:00
2059 lines
105 KiB
Swift
2059 lines
105 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import Display
|
|
import ComponentFlow
|
|
import SwiftSignalKit
|
|
import Postbox
|
|
import TelegramCore
|
|
import AppBundle
|
|
import LocalMediaResources
|
|
import TelegramPresentationData
|
|
import TelegramStringFormatting
|
|
import ViewControllerComponent
|
|
import BundleIconComponent
|
|
import BalancedTextComponent
|
|
import MultilineTextComponent
|
|
import MultilineTextWithEntitiesComponent
|
|
import ButtonComponent
|
|
import PlainButtonComponent
|
|
import GiftItemComponent
|
|
import GiftAnimationComponent
|
|
import AccountContext
|
|
import GlassBarButtonComponent
|
|
import ResizableSheetComponent
|
|
import AnimatedTextComponent
|
|
import Markdown
|
|
import InfoParagraphComponent
|
|
import PresentationDataUtils
|
|
import GiftViewScreen
|
|
import PeerInfoCoverComponent
|
|
import LottieComponent
|
|
import TooltipUI
|
|
import TextFormat
|
|
import GlassBackgroundComponent
|
|
import ConfettiEffect
|
|
import TelegramNotices
|
|
|
|
private final class CraftGiftPageContent: Component {
|
|
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
|
|
|
class ExternalState {
|
|
fileprivate(set) var giftsMap: [Int64: GiftItem]
|
|
fileprivate(set) var starGiftsMap: [Int64: StarGift.Gift] = [:]
|
|
fileprivate(set) var displayFailure = false
|
|
|
|
fileprivate(set) var testFailOrSuccess: Bool?
|
|
|
|
public init() {
|
|
self.giftsMap = [:]
|
|
}
|
|
}
|
|
|
|
enum DisplayState {
|
|
case `default`
|
|
case crafting
|
|
case failure
|
|
}
|
|
|
|
let context: AccountContext
|
|
let craftContext: CraftGiftsContext
|
|
let resaleContext: () -> ResaleGiftsContext?
|
|
let colors: (UIColor, UIColor, UIColor, UIColor, UIColor)
|
|
let gift: StarGift.UniqueGift
|
|
let selectedGiftIds: [Int32: Int64]
|
|
let displayState: DisplayState
|
|
let displayInfo: Bool
|
|
let result: CraftTableComponent.Result?
|
|
let screenSize: CGSize
|
|
let externalState: ExternalState
|
|
let starsTopUpOptionsPromise: Promise<[StarsTopUpOption]?>
|
|
let selectGift: (Int32, GiftItem) -> Void
|
|
let removeGift: (Int32) -> Void
|
|
let craftAnotherGift: () -> Void
|
|
let dismiss: () -> Void
|
|
|
|
init(
|
|
context: AccountContext,
|
|
craftContext: CraftGiftsContext,
|
|
resaleContext: @escaping () -> ResaleGiftsContext?,
|
|
colors: (UIColor, UIColor, UIColor, UIColor, UIColor),
|
|
gift: StarGift.UniqueGift,
|
|
selectedGiftIds: [Int32: Int64],
|
|
displayState: DisplayState,
|
|
displayInfo: Bool,
|
|
result: CraftTableComponent.Result?,
|
|
screenSize: CGSize,
|
|
externalState: ExternalState,
|
|
starsTopUpOptionsPromise: Promise<[StarsTopUpOption]?>,
|
|
selectGift: @escaping (Int32, GiftItem) -> Void,
|
|
removeGift: @escaping (Int32) -> Void,
|
|
craftAnotherGift: @escaping () -> Void,
|
|
dismiss: @escaping () -> Void
|
|
) {
|
|
self.context = context
|
|
self.craftContext = craftContext
|
|
self.resaleContext = resaleContext
|
|
self.colors = colors
|
|
self.gift = gift
|
|
self.selectedGiftIds = selectedGiftIds
|
|
self.displayState = displayState
|
|
self.displayInfo = displayInfo
|
|
self.result = result
|
|
self.screenSize = screenSize
|
|
self.externalState = externalState
|
|
self.starsTopUpOptionsPromise = starsTopUpOptionsPromise
|
|
self.selectGift = selectGift
|
|
self.removeGift = removeGift
|
|
self.craftAnotherGift = craftAnotherGift
|
|
self.dismiss = dismiss
|
|
}
|
|
|
|
static func ==(lhs: CraftGiftPageContent, rhs: CraftGiftPageContent) -> Bool {
|
|
if lhs.context !== rhs.context {
|
|
return false
|
|
}
|
|
if lhs.gift != rhs.gift {
|
|
return false
|
|
}
|
|
if lhs.colors.0 != rhs.colors.0 || lhs.colors.1 != rhs.colors.1 || lhs.colors.2 != rhs.colors.2 || lhs.colors.3 != rhs.colors.3 {
|
|
return false
|
|
}
|
|
if lhs.selectedGiftIds != rhs.selectedGiftIds {
|
|
return false
|
|
}
|
|
if lhs.displayState != rhs.displayState {
|
|
return false
|
|
}
|
|
if lhs.displayInfo != rhs.displayInfo {
|
|
return false
|
|
}
|
|
if lhs.screenSize != rhs.screenSize {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
final class View: UIView, UIScrollViewDelegate {
|
|
private let tableContainer = UIView()
|
|
private let background = SimpleGradientLayer()
|
|
private let overlay = SimpleGradientLayer()
|
|
private let pattern = ComponentView<Empty>()
|
|
|
|
private let title = ComponentView<Empty>()
|
|
private let descriptionText = ComponentView<Empty>()
|
|
|
|
private var craftTable = ComponentView<Empty>()
|
|
|
|
private var attributeDials: [AnyHashable: ComponentView<Empty>] = [:]
|
|
private var attributeDialTags: [AnyHashable: GenericComponentViewTag] = [:]
|
|
private var variantsButton: ComponentView<Empty>?
|
|
private var variantsButtonMeasure = ComponentView<Empty>()
|
|
|
|
private let craftingTitle = ComponentView<Empty>()
|
|
private let craftingSubtitle = ComponentView<Empty>()
|
|
private let craftingDescription = ComponentView<Empty>()
|
|
private let craftingProbability = ComponentView<Empty>()
|
|
private var craftingProbabilityMeasure = ComponentView<Empty>()
|
|
|
|
private let failureTitle = ComponentView<Empty>()
|
|
private let failureDescription = ComponentView<Empty>()
|
|
private var failedGifts: [AnyHashable: ComponentView<Empty>] = [:]
|
|
|
|
private let infoContainer = UIView()
|
|
private var infoBackground = SimpleLayer()
|
|
private var infoHeader = ComponentView<Empty>()
|
|
private let infoTitle = ComponentView<Empty>()
|
|
private let infoDescription = ComponentView<Empty>()
|
|
private var infoList = ComponentView<Empty>()
|
|
|
|
private var craftState: CraftGiftsContext.State?
|
|
private var craftStateDisposable: Disposable?
|
|
|
|
private let upgradePreviewDisposable = DisposableSet()
|
|
private var upgradePreview: [StarGift.UniqueGift.Attribute]?
|
|
private var starGiftsMap: [Int64: StarGift.Gift] = [:]
|
|
|
|
private var availableGifts: [GiftItem] = []
|
|
private var giftMap: [Int64: GiftItem] = [:]
|
|
private var isCrafting = false
|
|
|
|
private var component: CraftGiftPageContent?
|
|
private weak var state: EmptyComponentState?
|
|
private var environment: ViewControllerComponentContainer.Environment?
|
|
private var isUpdating: Bool = false
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
|
|
self.background.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
|
self.background.cornerRadius = 40.0
|
|
self.background.type = .axial
|
|
self.background.startPoint = CGPoint(x: 0.5, y: 0.0)
|
|
self.background.endPoint = CGPoint(x: 0.5, y: 1.0)
|
|
self.layer.addSublayer(self.background)
|
|
|
|
self.overlay.type = .radial
|
|
self.overlay.startPoint = CGPoint(x: 0.5, y: 0.5)
|
|
self.overlay.endPoint = CGPoint(x: 0.0, y: 1.0)
|
|
self.layer.addSublayer(self.overlay)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
deinit {
|
|
self.craftStateDisposable?.dispose()
|
|
self.upgradePreviewDisposable.dispose()
|
|
}
|
|
|
|
func showAttributeInfo(tag: Any, text: String) {
|
|
guard let component = self.component, let controller = self.environment?.controller() as? GiftCraftScreen 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 + 1.0, y: absoluteLocation.y - 12.0), size: CGSize())
|
|
let tooltipController = TooltipScreen(account: component.context.account, sharedContext: component.context.sharedContext, text: .markdown(text: text), balancedTextLayout: true, style: .wide, location: .point(location, .bottom), displayDuration: .default, inset: 16.0, shouldDismissOnTouch: { _, _ in
|
|
return .dismiss(consume: false)
|
|
})
|
|
controller.present(tooltipController, in: .current)
|
|
}
|
|
|
|
func openUpgradeVariants() {
|
|
guard let component = self.component, let controller = self.environment?.controller(), let gift = self.starGiftsMap[component.gift.giftId] else {
|
|
return
|
|
}
|
|
|
|
let _ = (component.context.engine.payments.getStarGiftUpgradeAttributes(giftId: component.gift.giftId)
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { [weak controller] attributes in
|
|
guard let attributes else {
|
|
return
|
|
}
|
|
let variantsController = component.context.sharedContext.makeGiftUpgradeVariantsScreen(
|
|
context: component.context,
|
|
gift: .generic(gift),
|
|
crafted: true,
|
|
attributes: attributes,
|
|
selectedAttributes: nil,
|
|
focusedAttribute: nil
|
|
)
|
|
controller?.push(variantsController)
|
|
})
|
|
}
|
|
|
|
func update(component: CraftGiftPageContent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
|
|
self.isUpdating = true
|
|
defer {
|
|
self.isUpdating = false
|
|
}
|
|
|
|
if self.component == nil {
|
|
let initialGiftItem = GiftItem(
|
|
gift: component.gift,
|
|
reference: .slug(slug: component.gift.slug)
|
|
)
|
|
self.availableGifts = [
|
|
initialGiftItem
|
|
]
|
|
self.giftMap = [initialGiftItem.gift.id: initialGiftItem]
|
|
component.externalState.giftsMap = self.giftMap
|
|
|
|
self.craftStateDisposable = (component.craftContext.state
|
|
|> deliverOnMainQueue).start(next: { [weak self] state in
|
|
guard let self, let component = self.component else {
|
|
return
|
|
}
|
|
self.craftState = state
|
|
|
|
var items: [GiftItem] = []
|
|
var map: [Int64: GiftItem] = self.giftMap
|
|
var foundInitial = false
|
|
for gift in state.gifts {
|
|
guard let reference = gift.reference, case let .unique(uniqueGift) = gift.gift else {
|
|
continue
|
|
}
|
|
let giftItem = GiftItem(
|
|
gift: uniqueGift,
|
|
reference: reference
|
|
)
|
|
if uniqueGift.id == component.gift.id {
|
|
items.insert(giftItem, at: 0)
|
|
foundInitial = true
|
|
} else {
|
|
items.append(giftItem)
|
|
}
|
|
map[uniqueGift.id] = giftItem
|
|
}
|
|
|
|
if !foundInitial {
|
|
items.insert(initialGiftItem, at: 0)
|
|
map[initialGiftItem.gift.id] = initialGiftItem
|
|
}
|
|
self.availableGifts = items
|
|
self.giftMap = map
|
|
self.component?.externalState.giftsMap = self.giftMap
|
|
|
|
self.state?.updated(transition: .spring(duration: 0.4))
|
|
|
|
if state.gifts.count < 18, case .ready(true, _) = state.dataState {
|
|
component.craftContext.loadMore()
|
|
}
|
|
})
|
|
|
|
self.upgradePreviewDisposable.add((component.context.engine.payments.getStarGiftUpgradeAttributes(giftId: initialGiftItem.gift.giftId)
|
|
|> deliverOnMainQueue).start(next: { [weak self] attributes in
|
|
guard let self, let attributes else {
|
|
return
|
|
}
|
|
var filteredAttributes: [StarGift.UniqueGift.Attribute] = []
|
|
for attribute in attributes {
|
|
if case let .model(_, file, _, crafted) = attribute {
|
|
if crafted {
|
|
filteredAttributes.append(attribute)
|
|
self.upgradePreviewDisposable.add(freeMediaFileResourceInteractiveFetched(account: component.context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start())
|
|
}
|
|
}
|
|
}
|
|
self.upgradePreview = filteredAttributes
|
|
|
|
self.state?.updated()
|
|
}))
|
|
|
|
self.upgradePreviewDisposable.add((.single(nil) |> then(component.context.engine.payments.cachedStarGifts())
|
|
|> deliverOnMainQueue).start(next: { [weak self] starGifts in
|
|
guard let self, let component = self.component, let starGifts else {
|
|
return
|
|
}
|
|
var starGiftsMap: [Int64: StarGift.Gift] = [:]
|
|
for gift in starGifts {
|
|
if case let .generic(gift) = gift {
|
|
starGiftsMap[gift.id] = gift
|
|
}
|
|
}
|
|
self.starGiftsMap = starGiftsMap
|
|
component.externalState.starGiftsMap = starGiftsMap
|
|
self.state?.updated()
|
|
}))
|
|
}
|
|
|
|
transition.setGradientColors(layer: self.background, colors: [component.colors.0, component.colors.1])
|
|
transition.setGradientColors(layer: self.overlay, colors: [component.colors.2, component.colors.2.withAlphaComponent(0.0)])
|
|
|
|
let environment = environment[ViewControllerComponentContainer.Environment.self].value
|
|
|
|
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
|
|
|
|
self.component = component
|
|
self.state = state
|
|
self.environment = environment
|
|
|
|
let isCrafting = [.crafting, .failure].contains(component.displayState)
|
|
|
|
var selectedGifts: [Int32: GiftItem] = [:]
|
|
for (index, giftId) in component.selectedGiftIds {
|
|
if let gift = self.giftMap[giftId] {
|
|
selectedGifts[index] = gift
|
|
}
|
|
}
|
|
|
|
var craftContentHeight: CGFloat = 0.0
|
|
var infoContentHeight: CGFloat = 0.0
|
|
|
|
let anvilPath = getAppBundle().url(forResource: "Anvil", withExtension: "tgs")?.path ?? ""
|
|
let anvilFile = TelegramMediaFile(
|
|
fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: -123456789),
|
|
partialReference: nil,
|
|
resource: BundleResource(name: "Anvil", path: anvilPath),
|
|
previewRepresentations: [],
|
|
videoThumbnails: [],
|
|
immediateThumbnailData: nil,
|
|
mimeType: "application/x-tgsticker",
|
|
size: nil,
|
|
attributes: [
|
|
.FileName(fileName: "sticker.tgs"),
|
|
.CustomEmoji(isPremium: false, isSingleColor: true, alt: "", packReference: .animatedEmojiAnimations)
|
|
],
|
|
alternativeRepresentations: []
|
|
)
|
|
|
|
var backgroundTransition = transition
|
|
let backgroundSize = self.pattern.update(
|
|
transition: backgroundTransition,
|
|
component: AnyComponent(PeerInfoCoverComponent(
|
|
context: component.context,
|
|
subject: .custom(.clear, .clear, UIColor(rgb: 0x000000), anvilFile.fileId.id),
|
|
files: [anvilFile.fileId.id: anvilFile],
|
|
isDark: false,
|
|
avatarCenter: CGPoint(x: availableSize.width / 2.0, y: 169.0),
|
|
avatarSize: CGSize(width: 130.0, height: 130.0),
|
|
avatarScale: 1.0,
|
|
defaultHeight: 300.0,
|
|
gradientOnTop: true,
|
|
avatarTransitionFraction: 0.0,
|
|
patternTransitionFraction: 0.0,
|
|
patternIconScale: 1.5
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width, height: 169.0 * 2.0)
|
|
)
|
|
let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: isCrafting && !"".isEmpty ? floor((component.screenSize.height - backgroundSize.height) / 2.0) : 0.0), size: backgroundSize)
|
|
if let backgroundView = self.pattern.view {
|
|
if backgroundView.layer.superlayer == nil {
|
|
backgroundTransition = .immediate
|
|
backgroundView.clipsToBounds = true
|
|
backgroundView.isUserInteractionEnabled = false
|
|
self.layer.insertSublayer(backgroundView.layer, above: self.overlay)
|
|
}
|
|
backgroundTransition.setFrame(view: backgroundView, frame: backgroundFrame)
|
|
}
|
|
|
|
let titleSize = self.title.update(
|
|
transition: transition,
|
|
component: AnyComponent(
|
|
MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Gift_Craft_Title, font: Font.semibold(17.0), textColor: .white)))
|
|
),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
|
|
)
|
|
let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - titleSize.width) * 0.5), y: 16.0 + 22.0 - titleSize.height * 0.5), size: titleSize)
|
|
if let titleView = self.title.view {
|
|
if titleView.superview == nil {
|
|
self.addSubview(titleView)
|
|
}
|
|
transition.setFrame(view: titleView, frame: titleFrame)
|
|
transition.setAlpha(view: titleView, alpha: 1.0)
|
|
}
|
|
|
|
var selectedMainGift = component.gift
|
|
if component.selectedGiftIds[0] != selectedMainGift.id, let id = component.selectedGiftIds[0], let gift = self.giftMap[id]?.gift {
|
|
selectedMainGift = gift
|
|
}
|
|
|
|
let giftTitle = "\(selectedMainGift.title) #\(formatCollectibleNumber(selectedMainGift.number, dateTimeFormat: environment.dateTimeFormat))"
|
|
|
|
let descriptionFont = Font.regular(13.0)
|
|
let descriptionBoldFont = Font.semibold(13.0)
|
|
let descriptionColor = UIColor.white
|
|
let rawDescriptionString = environment.strings.Gift_Craft_Description(giftTitle).string
|
|
let descriptionString = parseMarkdownIntoAttributedString(rawDescriptionString, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: descriptionFont, textColor: descriptionColor), bold: MarkdownAttributeSet(font: descriptionBoldFont, textColor: descriptionColor), link: MarkdownAttributeSet(font: descriptionFont, textColor: descriptionColor), linkAttribute: { _ in return nil })).mutableCopy() as! NSMutableAttributedString
|
|
|
|
if let gift = self.starGiftsMap[component.gift.giftId] {
|
|
let range = (descriptionString.string as NSString).range(of: "$")
|
|
if range.location != NSNotFound {
|
|
descriptionString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: gift.file.fileId.id, file: gift.file, custom: nil, enableAnimation: false), range: range)
|
|
}
|
|
}
|
|
|
|
let descriptionTextSize = self.descriptionText.update(
|
|
transition: transition,
|
|
component: AnyComponent(
|
|
MultilineTextWithEntitiesComponent(
|
|
context: component.context,
|
|
animationCache: component.context.animationCache,
|
|
animationRenderer: component.context.animationRenderer,
|
|
placeholderColor: .white.withAlphaComponent(0.3),
|
|
text: .plain(descriptionString),
|
|
horizontalAlignment: .center,
|
|
maximumNumberOfLines: 0,
|
|
lineSpacing: 0.2
|
|
)
|
|
),
|
|
environment: {
|
|
},
|
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
|
|
)
|
|
craftContentHeight += 291.0
|
|
|
|
let descriptionTextFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - descriptionTextSize.width) * 0.5), y: craftContentHeight), size: descriptionTextSize)
|
|
if let descriptionTextView = self.descriptionText.view {
|
|
if descriptionTextView.superview == nil {
|
|
self.addSubview(descriptionTextView)
|
|
}
|
|
transition.setFrame(view: descriptionTextView, frame: descriptionTextFrame)
|
|
transition.setAlpha(view: descriptionTextView, alpha: isCrafting ? 0.0 : 1.0)
|
|
transition.setBlur(layer: descriptionTextView.layer, radius: isCrafting ? 10.0 : 0.0)
|
|
}
|
|
craftContentHeight += descriptionTextSize.height
|
|
craftContentHeight += 16.0
|
|
|
|
var attributes: [ResaleGiftsContext.Attribute: StarGift.UniqueGift.Attribute] = [:]
|
|
var backdropAttributeCount: [ResaleGiftsContext.Attribute: (Int32, Int)] = [:]
|
|
var patternAttributeCount: [ResaleGiftsContext.Attribute: (Int32, Int)] = [:]
|
|
for (index, gift) in selectedGifts {
|
|
for attribute in gift.gift.attributes {
|
|
switch attribute {
|
|
case let .backdrop(_, id, _, _, _, _, _):
|
|
let attributeId: ResaleGiftsContext.Attribute = .backdrop(id)
|
|
attributes[attributeId] = attribute
|
|
if let (minPosition, count) = backdropAttributeCount[attributeId] {
|
|
backdropAttributeCount[attributeId] = (min(index, minPosition), count + 1)
|
|
} else {
|
|
backdropAttributeCount[attributeId] = (index, 1)
|
|
}
|
|
case let .pattern(_, file, _):
|
|
let attributeId: ResaleGiftsContext.Attribute = .pattern(file.fileId.id)
|
|
attributes[attributeId] = attribute
|
|
if let (minPosition, count) = patternAttributeCount[attributeId] {
|
|
patternAttributeCount[attributeId] = (min(index, minPosition), count + 1)
|
|
} else {
|
|
patternAttributeCount[attributeId] = (index, 1)
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
var attributesCount: [ResaleGiftsContext.Attribute: (Int32, Int)] = [:]
|
|
for (attributeId, value) in backdropAttributeCount {
|
|
attributesCount[attributeId] = value
|
|
}
|
|
for (attributeId, value) in patternAttributeCount {
|
|
attributesCount[attributeId] = value
|
|
}
|
|
|
|
var backdropAttributes: [(ResaleGiftsContext.Attribute, Int)] = []
|
|
for (attributeId, count) in backdropAttributeCount {
|
|
backdropAttributes.append((attributeId, count.1))
|
|
}
|
|
backdropAttributes = backdropAttributes.sorted(by: { lhs, rhs in
|
|
if lhs.1 != rhs.1 {
|
|
return lhs.1 > rhs.1
|
|
} else {
|
|
return attributesCount[lhs.0]!.0 < attributesCount[rhs.0]!.0
|
|
}
|
|
})
|
|
var patternAttributes: [(ResaleGiftsContext.Attribute, Int)] = []
|
|
for (attributeId, count) in patternAttributeCount {
|
|
patternAttributes.append((attributeId, count.1))
|
|
}
|
|
patternAttributes = patternAttributes.sorted(by: { lhs, rhs in
|
|
if lhs.1 != rhs.1 {
|
|
return lhs.1 > rhs.1
|
|
} else {
|
|
return attributesCount[lhs.0]!.0 < attributesCount[rhs.0]!.0
|
|
}
|
|
})
|
|
|
|
var combinedAttributes: [ResaleGiftsContext.Attribute] = []
|
|
for (attributeId, _) in backdropAttributes {
|
|
combinedAttributes.append(attributeId)
|
|
}
|
|
for (attributeId, _) in patternAttributes {
|
|
combinedAttributes.append(attributeId)
|
|
}
|
|
|
|
let appConfiguration = component.context.currentAppConfiguration.with { $0 }
|
|
let giftCraftConfiguration = GiftCraftConfiguration.with(appConfiguration: appConfiguration)
|
|
|
|
var firstRowCount = 0
|
|
var secondRowCount = 0
|
|
switch combinedAttributes.count {
|
|
case 0, 1, 2, 3, 4:
|
|
firstRowCount = combinedAttributes.count
|
|
case 5:
|
|
firstRowCount = 2
|
|
secondRowCount = 3
|
|
case 6:
|
|
firstRowCount = 3
|
|
secondRowCount = 3
|
|
case 7:
|
|
firstRowCount = 3
|
|
secondRowCount = 4
|
|
case 8:
|
|
firstRowCount = 4
|
|
secondRowCount = 4
|
|
default:
|
|
break
|
|
}
|
|
|
|
let attributeDialSpacing: CGFloat = 18.0
|
|
let attributeDialSize = CGSize(width: 48.0, height: 48.0)
|
|
|
|
let attributeFirstRowTotalWidth = CGFloat(firstRowCount) * attributeDialSize.width + CGFloat(firstRowCount - 1) * attributeDialSpacing
|
|
let attributeSecondRowTotalWidth = CGFloat(secondRowCount) * attributeDialSize.width + CGFloat(secondRowCount - 1) * attributeDialSpacing
|
|
var attributeDialFrame: CGRect = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - attributeFirstRowTotalWidth) / 2.0), y: craftContentHeight), size: attributeDialSize)
|
|
|
|
craftContentHeight += attributeDialSize.height
|
|
craftContentHeight += 39.0
|
|
|
|
var validIds: [AnyHashable] = []
|
|
var attributeDialIndex = 0
|
|
for attribute in combinedAttributes {
|
|
let itemId = AnyHashable(attribute)
|
|
validIds.append(itemId)
|
|
|
|
var itemTransition = transition
|
|
let visibleItem: ComponentView<Empty>
|
|
if let current = self.attributeDials[itemId] {
|
|
visibleItem = current
|
|
} else {
|
|
visibleItem = ComponentView()
|
|
self.attributeDials[itemId] = visibleItem
|
|
itemTransition = .immediate
|
|
}
|
|
|
|
let tag: GenericComponentViewTag
|
|
if let current = self.attributeDialTags[itemId] {
|
|
tag = current
|
|
} else {
|
|
tag = GenericComponentViewTag()
|
|
self.attributeDialTags[itemId] = tag
|
|
}
|
|
|
|
let attributeCount = attributesCount[attribute]?.1 ?? 0
|
|
let permille = Int(giftCraftConfiguration.craftAttributePermilles[selectedGifts.count - 1][attributeCount - 1])
|
|
|
|
let dialContent: AnyComponentWithIdentity<Empty>
|
|
var dialContentSize: CGSize?
|
|
let tooltipText: String
|
|
switch attribute {
|
|
case .backdrop:
|
|
guard case let .backdrop(name, _, innerColor, outerColor, _, _, _) = attributes[attribute] else {
|
|
continue
|
|
}
|
|
dialContent = AnyComponentWithIdentity(
|
|
id: "color",
|
|
component: AnyComponent(
|
|
ColorSwatchComponent(
|
|
innerColor: UIColor(rgb: UInt32(bitPattern: innerColor)),
|
|
outerColor: UIColor(rgb: UInt32(bitPattern: outerColor))
|
|
)
|
|
)
|
|
)
|
|
tooltipText = environment.strings.Gift_Craft_BackdropTooltip("\(permille / 10)", name).string
|
|
case .pattern:
|
|
guard case let .pattern(name, file, _) = attributes[attribute] else {
|
|
continue
|
|
}
|
|
dialContent = AnyComponentWithIdentity(
|
|
id: "symbol",
|
|
component: AnyComponent(
|
|
LottieComponent(
|
|
content: LottieComponent.ResourceContent(
|
|
context: component.context,
|
|
file: file,
|
|
attemptSynchronously: true,
|
|
providesPlaceholder: true
|
|
),
|
|
color: .white,
|
|
size: CGSize(width: 32.0, height: 32.0)
|
|
)
|
|
)
|
|
)
|
|
dialContentSize = CGSize(width: 30.0, height: 30.0)
|
|
tooltipText = environment.strings.Gift_Craft_SymbolTooltip("\(permille / 10)", name).string
|
|
default:
|
|
continue
|
|
}
|
|
|
|
let _ = visibleItem.update(
|
|
transition: itemTransition,
|
|
component: AnyComponent(
|
|
PlainButtonComponent(
|
|
content: AnyComponent(
|
|
DialIndicatorComponent(
|
|
content: dialContent,
|
|
backgroundColor: .white.withAlphaComponent(0.1),
|
|
foregroundColor: .white,
|
|
diameter: 48.0,
|
|
contentSize: dialContentSize,
|
|
lineWidth: 4.0,
|
|
fontSize: 10.0,
|
|
progress: CGFloat(permille) / 10.0 / 100.0,
|
|
value: permille / 10,
|
|
suffix: "%"
|
|
)
|
|
),
|
|
action: { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
HapticFeedback().impact(.light)
|
|
|
|
#if DEBUG
|
|
switch attribute {
|
|
case .backdrop:
|
|
self.component?.externalState.testFailOrSuccess = true
|
|
case .pattern:
|
|
self.component?.externalState.testFailOrSuccess = false
|
|
default:
|
|
break
|
|
}
|
|
#endif
|
|
self.showAttributeInfo(tag: tag, text: tooltipText)
|
|
},
|
|
tag: tag
|
|
)
|
|
),
|
|
environment: {},
|
|
containerSize: availableSize
|
|
)
|
|
if let itemView = visibleItem.view {
|
|
if itemView.superview == nil {
|
|
self.addSubview(itemView)
|
|
|
|
if !transition.animation.isImmediate {
|
|
itemView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
|
}
|
|
}
|
|
itemTransition.setFrame(view: itemView, frame: attributeDialFrame)
|
|
transition.setAlpha(view: itemView, alpha: isCrafting ? 0.0 : 1.0)
|
|
transition.setBlur(layer: itemView.layer, radius: isCrafting ? 10.0 : 0.0)
|
|
}
|
|
|
|
attributeDialFrame.origin.x += attributeDialSize.width + attributeDialSpacing
|
|
attributeDialIndex += 1
|
|
|
|
if attributeDialIndex == firstRowCount {
|
|
attributeDialFrame.origin.x = floorToScreenPixels((availableSize.width - attributeSecondRowTotalWidth) / 2.0)
|
|
attributeDialFrame.origin.y += 66.0
|
|
}
|
|
}
|
|
|
|
var removeIds: [AnyHashable] = []
|
|
for (id, item) in self.attributeDials {
|
|
if !validIds.contains(id) {
|
|
removeIds.append(id)
|
|
if let itemView = item.view {
|
|
if !transition.animation.isImmediate {
|
|
itemView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.25, removeOnCompletion: false)
|
|
itemView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
|
itemView.removeFromSuperview()
|
|
})
|
|
} else {
|
|
itemView.removeFromSuperview()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for id in removeIds {
|
|
self.attributeDials.removeValue(forKey: id)
|
|
}
|
|
|
|
|
|
if secondRowCount == 0, case .default = component.displayState {
|
|
let variantsString = environment.strings.Gift_Craft_ViewVariants
|
|
let variantsButtonMeasure = self.variantsButtonMeasure.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: variantsString, font: Font.semibold(13.0), textColor: .clear)))),
|
|
environment: {},
|
|
containerSize: availableSize
|
|
)
|
|
|
|
let variantsButton: ComponentView<Empty>
|
|
if let current = self.variantsButton {
|
|
variantsButton = current
|
|
} else {
|
|
variantsButton = ComponentView<Empty>()
|
|
self.variantsButton = variantsButton
|
|
}
|
|
|
|
let variantsButtonSize = CGSize(width: variantsButtonMeasure.width + 87.0, height: 24.0)
|
|
if let gift = self.starGiftsMap[component.gift.giftId] {
|
|
var variant1: GiftItemComponent.Subject = .starGift(gift: gift, price: "")
|
|
var variant2: GiftItemComponent.Subject = .starGift(gift: gift, price: "")
|
|
var variant3: GiftItemComponent.Subject = .starGift(gift: gift, price: "")
|
|
|
|
if let upgradePreview = self.upgradePreview {
|
|
var i = 0
|
|
for attribute in upgradePreview {
|
|
if case .model = attribute {
|
|
switch i {
|
|
case 0:
|
|
variant1 = .preview(attributes: [attribute], rarity: nil)
|
|
case 1:
|
|
variant2 = .preview(attributes: [attribute], rarity: nil)
|
|
case 2:
|
|
variant3 = .preview(attributes: [attribute], rarity: nil)
|
|
default:
|
|
break
|
|
}
|
|
i += 1
|
|
}
|
|
}
|
|
}
|
|
|
|
let _ = variantsButton.update(
|
|
transition: transition,
|
|
component: AnyComponent(
|
|
GlassBarButtonComponent(
|
|
size: variantsButtonSize,
|
|
backgroundColor: component.colors.3,
|
|
isDark: true,
|
|
state: .tintedGlass,
|
|
component: AnyComponentWithIdentity(id: "content", component: AnyComponent(HStack([
|
|
AnyComponentWithIdentity(id: "icon1", component: AnyComponent(
|
|
GiftItemComponent(
|
|
context: component.context,
|
|
theme: environment.theme,
|
|
strings: environment.strings,
|
|
peer: nil,
|
|
subject: variant1,
|
|
isPlaceholder: false,
|
|
mode: .tableIcon
|
|
)
|
|
)),
|
|
AnyComponentWithIdentity(id: "icon2", component: AnyComponent(
|
|
GiftItemComponent(
|
|
context: component.context,
|
|
theme: environment.theme,
|
|
strings: environment.strings,
|
|
peer: nil,
|
|
subject: variant2,
|
|
isPlaceholder: false,
|
|
mode: .tableIcon
|
|
)
|
|
)),
|
|
AnyComponentWithIdentity(id: "icon3", component: AnyComponent(
|
|
GiftItemComponent(
|
|
context: component.context,
|
|
theme: environment.theme,
|
|
strings: environment.strings,
|
|
peer: nil,
|
|
subject: variant3,
|
|
isPlaceholder: false,
|
|
mode: .tableIcon
|
|
)
|
|
)),
|
|
AnyComponentWithIdentity(id: "text", component: AnyComponent(
|
|
MultilineTextComponent(text: .plain(NSAttributedString(string: variantsString, font: Font.semibold(13.0), textColor: .white)))
|
|
)),
|
|
AnyComponentWithIdentity(id: "arrow", component: AnyComponent(
|
|
BundleIconComponent(name: "Item List/InlineTextRightArrow", tintColor: .white)
|
|
))
|
|
], spacing: 3.0))),
|
|
action: { [weak self] _ in
|
|
HapticFeedback().impact(.light)
|
|
|
|
self?.openUpgradeVariants()
|
|
}
|
|
)
|
|
),
|
|
environment: {},
|
|
containerSize: availableSize
|
|
)
|
|
}
|
|
let variantsButtonFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - variantsButtonSize.width) / 2.0), y: craftContentHeight), size: variantsButtonSize)
|
|
var varitantsButtonTransition = transition
|
|
if let variantsButtonView = variantsButton.view {
|
|
if variantsButtonView.superview == nil {
|
|
varitantsButtonTransition = .immediate
|
|
if let descriptionView = self.descriptionText.view {
|
|
self.insertSubview(variantsButtonView, aboveSubview: descriptionView)
|
|
} else {
|
|
self.addSubview(variantsButtonView)
|
|
}
|
|
}
|
|
varitantsButtonTransition.setFrame(view: variantsButtonView, frame: variantsButtonFrame)
|
|
}
|
|
} else if let variantsButton = self.variantsButton {
|
|
self.variantsButton = nil
|
|
if let variantsButtonView = variantsButton.view {
|
|
transition.setBlur(layer: variantsButtonView.layer, radius: isCrafting ? 10.0 : 0.0)
|
|
variantsButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in
|
|
variantsButtonView.removeFromSuperview()
|
|
})
|
|
}
|
|
}
|
|
|
|
craftContentHeight += 145.0
|
|
|
|
let permilleValue = selectedGifts.reduce(0, { $0 + Int($1.value.gift.craftChancePermille ?? 0) })
|
|
if component.displayState == .crafting {
|
|
var craftingOriginY = craftContentHeight * 0.5 - 16.0
|
|
let offset: CGFloat = 0.0
|
|
|
|
let craftingTitleSize = self.craftingTitle.update(
|
|
transition: transition,
|
|
component: AnyComponent(
|
|
MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Gift_Craft_Crafting_Title, font: Font.bold(20.0), textColor: .white)))
|
|
),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
|
|
)
|
|
let craftingTitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - craftingTitleSize.width) * 0.5), y: craftingOriginY), size: craftingTitleSize)
|
|
if let craftingTitleView = self.craftingTitle.view {
|
|
if craftingTitleView.superview == nil {
|
|
transition.animateAlpha(view: craftingTitleView, from: 0.0, to: 1.0)
|
|
transition.animateBlur(layer: craftingTitleView.layer, fromRadius: 10.0, toRadius: 0.0)
|
|
transition.animatePosition(view: craftingTitleView, from: CGPoint(x: 0.0, y: offset), to: .zero, additive: true)
|
|
|
|
self.addSubview(craftingTitleView)
|
|
}
|
|
craftingTitleView.frame = craftingTitleFrame
|
|
}
|
|
craftingOriginY += craftingTitleSize.height
|
|
craftingOriginY += 6.0
|
|
|
|
let craftingSubtitleSize = self.craftingSubtitle.update(
|
|
transition: transition,
|
|
component: AnyComponent(
|
|
MultilineTextComponent(text: .plain(NSAttributedString(string: giftTitle, font: Font.semibold(13.0), textColor: .white.withAlphaComponent(0.5))))
|
|
),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
|
|
)
|
|
let craftingSubtitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - craftingSubtitleSize.width) * 0.5), y: craftingOriginY), size: craftingSubtitleSize)
|
|
if let craftingSubtitleView = self.craftingSubtitle.view {
|
|
if craftingSubtitleView.superview == nil {
|
|
transition.animateAlpha(view: craftingSubtitleView, from: 0.0, to: 1.0)
|
|
transition.animateBlur(layer: craftingSubtitleView.layer, fromRadius: 10.0, toRadius: 0.0)
|
|
transition.animatePosition(view: craftingSubtitleView, from: CGPoint(x: 0.0, y: offset), to: .zero, additive: true)
|
|
|
|
self.addSubview(craftingSubtitleView)
|
|
}
|
|
craftingSubtitleView.frame = craftingSubtitleFrame
|
|
}
|
|
craftingOriginY += craftingSubtitleSize.height
|
|
craftingOriginY += 21.0
|
|
|
|
let descriptionFont = Font.regular(13.0)
|
|
let descriptionBoldFont = Font.semibold(13.0)
|
|
let descriptionColor = UIColor.white.withAlphaComponent(0.5)
|
|
let rawDescriptionString = environment.strings.Gift_Craft_Crafting_Description
|
|
let descriptionString = parseMarkdownIntoAttributedString(rawDescriptionString, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: descriptionFont, textColor: descriptionColor), bold: MarkdownAttributeSet(font: descriptionBoldFont, textColor: descriptionColor), link: MarkdownAttributeSet(font: descriptionFont, textColor: descriptionColor), linkAttribute: { _ in return nil })).mutableCopy() as! NSMutableAttributedString
|
|
|
|
let craftingDescriptionSize = self.craftingDescription.update(
|
|
transition: transition,
|
|
component: AnyComponent(
|
|
MultilineTextComponent(
|
|
text: .plain(descriptionString),
|
|
horizontalAlignment: .center,
|
|
maximumNumberOfLines: 0,
|
|
lineSpacing: 0.2
|
|
)
|
|
),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
|
|
)
|
|
let craftingDescriptionFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - craftingDescriptionSize.width) * 0.5), y: craftingOriginY), size: craftingDescriptionSize)
|
|
if let craftingDescriptionView = self.craftingDescription.view {
|
|
if craftingDescriptionView.superview == nil {
|
|
transition.animateAlpha(view: craftingDescriptionView, from: 0.0, to: 1.0)
|
|
transition.animateBlur(layer: craftingDescriptionView.layer, fromRadius: 10.0, toRadius: 0.0)
|
|
transition.animatePosition(view: craftingDescriptionView, from: CGPoint(x: 0.0, y: offset), to: .zero, additive: true)
|
|
|
|
self.addSubview(craftingDescriptionView)
|
|
}
|
|
craftingDescriptionView.frame = craftingDescriptionFrame
|
|
}
|
|
craftingOriginY += craftingDescriptionSize.height
|
|
craftingOriginY += 24.0
|
|
|
|
let craftingProbabilityString = environment.strings.Gift_Craft_Crafting_SuccessChance("\(permilleValue / 10)").string
|
|
let craftingProbabilityMeasure = self.craftingProbabilityMeasure.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: craftingProbabilityString, font: Font.semibold(13.0), textColor: .clear)))),
|
|
environment: {},
|
|
containerSize: availableSize
|
|
)
|
|
|
|
let craftingProbabilitySize = CGSize(width: craftingProbabilityMeasure.width + 18.0, height: 24.0)
|
|
let _ = self.craftingProbability.update(
|
|
transition: transition,
|
|
component: AnyComponent(
|
|
GlassBarButtonComponent(
|
|
size: craftingProbabilitySize,
|
|
backgroundColor: component.colors.3.mixedWith(component.colors.1, alpha: 0.3),
|
|
isDark: true,
|
|
state: .tintedGlass,
|
|
component: AnyComponentWithIdentity(id: "text", component: AnyComponent(
|
|
MultilineTextComponent(text: .plain(NSAttributedString(string: craftingProbabilityString, font: Font.semibold(13.0), textColor: .white)))
|
|
)),
|
|
action: nil
|
|
)
|
|
),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
|
|
)
|
|
let craftingProbabilityFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - craftingProbabilitySize.width) * 0.5), y: craftingOriginY), size: craftingProbabilitySize)
|
|
if let craftingProbabilityView = self.craftingProbability.view {
|
|
if craftingProbabilityView.superview == nil {
|
|
transition.animateAlpha(view: craftingProbabilityView, from: 0.0, to: 1.0)
|
|
transition.animateBlur(layer: craftingProbabilityView.layer, fromRadius: 10.0, toRadius: 0.0)
|
|
transition.animatePosition(view: craftingProbabilityView, from: CGPoint(x: 0.0, y: offset), to: .zero, additive: true)
|
|
|
|
self.addSubview(craftingProbabilityView)
|
|
}
|
|
craftingProbabilityView.frame = craftingProbabilityFrame
|
|
}
|
|
} else {
|
|
if let craftingTitleView = self.craftingTitle.view {
|
|
transition.setAlpha(view: craftingTitleView, alpha: 0.0, completion: { _ in
|
|
craftingTitleView.removeFromSuperview()
|
|
})
|
|
transition.animateBlur(layer: craftingTitleView.layer, fromRadius: 0.0, toRadius: 10.0)
|
|
}
|
|
if let craftingSubtitleView = self.craftingSubtitle.view {
|
|
transition.setAlpha(view: craftingSubtitleView, alpha: 0.0, completion: { _ in
|
|
craftingSubtitleView.removeFromSuperview()
|
|
})
|
|
transition.animateBlur(layer: craftingSubtitleView.layer, fromRadius: 0.0, toRadius: 10.0)
|
|
}
|
|
if let craftingDescriptionView = self.craftingDescription.view {
|
|
transition.setAlpha(view: craftingDescriptionView, alpha: 0.0, completion: { _ in
|
|
craftingDescriptionView.removeFromSuperview()
|
|
})
|
|
transition.animateBlur(layer: craftingDescriptionView.layer, fromRadius: 0.0, toRadius: 10.0)
|
|
}
|
|
if let craftingProbabilityView = self.craftingProbability.view {
|
|
craftingProbabilityView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in
|
|
craftingProbabilityView.removeFromSuperview()
|
|
})
|
|
transition.animateBlur(layer: craftingProbabilityView.layer, fromRadius: 0.0, toRadius: 10.0)
|
|
}
|
|
}
|
|
|
|
if component.displayState == .failure {
|
|
var failureOriginY = craftContentHeight * 0.5 - 16.0
|
|
let offset: CGFloat = 0.0
|
|
|
|
let failureTitleSize = self.failureTitle.update(
|
|
transition: transition,
|
|
component: AnyComponent(
|
|
MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Gift_Craft_CraftingFailed_Title, font: Font.bold(20.0), textColor: UIColor(rgb: 0xff746d))))
|
|
),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
|
|
)
|
|
let failureTitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - failureTitleSize.width) * 0.5), y: failureOriginY), size: failureTitleSize)
|
|
if let failureTitleView = self.failureTitle.view {
|
|
if failureTitleView.superview == nil {
|
|
transition.animateAlpha(view: failureTitleView, from: 0.0, to: 1.0)
|
|
transition.animateBlur(layer: failureTitleView.layer, fromRadius: 10.0, toRadius: 0.0)
|
|
transition.animatePosition(view: failureTitleView, from: CGPoint(x: 0.0, y: offset), to: .zero, additive: true)
|
|
|
|
self.addSubview(failureTitleView)
|
|
}
|
|
failureTitleView.frame = failureTitleFrame
|
|
}
|
|
failureOriginY += failureTitleSize.height
|
|
failureOriginY += 17.0
|
|
|
|
let descriptionFont = Font.regular(13.0)
|
|
let descriptionBoldFont = Font.semibold(13.0)
|
|
let descriptionColor = UIColor(rgb: 0xf7af8c)
|
|
let rawDescriptionString = environment.strings.Gift_Craft_CraftingFailed_Text(Int32(component.selectedGiftIds.count))
|
|
let descriptionString = parseMarkdownIntoAttributedString(rawDescriptionString, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: descriptionFont, textColor: descriptionColor), bold: MarkdownAttributeSet(font: descriptionBoldFont, textColor: descriptionColor), link: MarkdownAttributeSet(font: descriptionFont, textColor: descriptionColor), linkAttribute: { _ in return nil })).mutableCopy() as! NSMutableAttributedString
|
|
|
|
let failureDescriptionSize = self.failureDescription.update(
|
|
transition: transition,
|
|
component: AnyComponent(
|
|
MultilineTextComponent(
|
|
text: .plain(descriptionString),
|
|
horizontalAlignment: .center,
|
|
maximumNumberOfLines: 0,
|
|
lineSpacing: 0.2
|
|
)
|
|
),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
|
|
)
|
|
let failureDescriptionFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - failureDescriptionSize.width) * 0.5), y: failureOriginY), size: failureDescriptionSize)
|
|
if let failureDescriptionView = self.failureDescription.view {
|
|
if failureDescriptionView.superview == nil {
|
|
transition.animateAlpha(view: failureDescriptionView, from: 0.0, to: 1.0)
|
|
transition.animateBlur(layer: failureDescriptionView.layer, fromRadius: 10.0, toRadius: 0.0)
|
|
transition.animatePosition(view: failureDescriptionView, from: CGPoint(x: 0.0, y: offset), to: .zero, additive: true)
|
|
|
|
self.addSubview(failureDescriptionView)
|
|
}
|
|
failureDescriptionView.frame = failureDescriptionFrame
|
|
}
|
|
failureOriginY += failureDescriptionSize.height
|
|
failureOriginY += 34.0
|
|
|
|
var indices: [Int] = []
|
|
for index in component.selectedGiftIds.keys.sorted() {
|
|
indices.append(Int(index))
|
|
}
|
|
var lostGifts: [GiftItem] = []
|
|
for index in indices {
|
|
if let giftId = component.selectedGiftIds[Int32(index)], let gift = self.giftMap[giftId] {
|
|
lostGifts.append(gift)
|
|
}
|
|
}
|
|
|
|
let itemSize = CGSize(width: 80.0, height: 80.0)
|
|
let itemSpacing: CGFloat = 16.0
|
|
var itemDelay: Double = 0.2
|
|
|
|
let totalItemsWidth: CGFloat = itemSize.width * CGFloat(lostGifts.count) + itemSpacing * CGFloat(lostGifts.count - 1)
|
|
var itemOriginX: CGFloat = floor((availableSize.width - totalItemsWidth) / 2.0)
|
|
|
|
for gift in lostGifts {
|
|
let itemId = AnyHashable(gift.gift.id)
|
|
var itemTransition = transition
|
|
let visibleItem: ComponentView<Empty>
|
|
if let current = self.failedGifts[itemId] {
|
|
visibleItem = current
|
|
} else {
|
|
visibleItem = ComponentView()
|
|
self.failedGifts[itemId] = visibleItem
|
|
itemTransition = .immediate
|
|
}
|
|
|
|
let ribbonText = "#\(gift.gift.number)"
|
|
let ribbonColor: GiftItemComponent.Ribbon.Color = .custom(0xff645b, 0xff645b)
|
|
|
|
let _ = visibleItem.update(
|
|
transition: itemTransition,
|
|
component: AnyComponent(
|
|
GiftItemComponent(
|
|
context: component.context,
|
|
style: .glass,
|
|
theme: environment.theme,
|
|
strings: environment.strings,
|
|
peer: nil,
|
|
subject: .uniqueGift(gift: gift.gift, price: nil),
|
|
ribbon: GiftItemComponent.Ribbon(text: ribbonText, font: .monospaced, color: ribbonColor, outline: nil),
|
|
badge: nil,
|
|
resellPrice: nil,
|
|
isHidden: false,
|
|
isSelected: false,
|
|
isPinned: false,
|
|
isEditing: false,
|
|
mode: .grid,
|
|
action: nil,
|
|
contextAction: nil
|
|
)
|
|
),
|
|
environment: {},
|
|
containerSize: itemSize
|
|
)
|
|
let itemFrame = CGRect(origin: CGPoint(x: itemOriginX, y: failureOriginY), size: itemSize)
|
|
if let itemView = visibleItem.view {
|
|
if itemView.superview == nil {
|
|
self.addSubview(itemView)
|
|
|
|
if !transition.animation.isImmediate {
|
|
itemView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.25, delay: itemDelay)
|
|
itemView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, delay: itemDelay)
|
|
}
|
|
}
|
|
itemTransition.setFrame(view: itemView, frame: itemFrame)
|
|
}
|
|
itemOriginX += itemSize.width + itemSpacing
|
|
itemDelay += 0.07
|
|
}
|
|
}
|
|
|
|
let tableSize = CGSize(width: availableSize.width, height: 320.0)
|
|
let craftTableSize = self.craftTable.update(
|
|
transition: transition,
|
|
component: AnyComponent(
|
|
CraftTableComponent(
|
|
context: component.context,
|
|
gifts: selectedGifts,
|
|
buttonColor: component.colors.3,
|
|
isCrafting: isCrafting,
|
|
result: component.result,
|
|
select: { [weak self] index in
|
|
guard let self, let component = self.component, let environment = self.environment, let genericGift = self.starGiftsMap[component.gift.giftId], let resaleContext = component.resaleContext() else {
|
|
return
|
|
}
|
|
|
|
HapticFeedback().impact(.light)
|
|
|
|
let selectController = SelectCraftGiftScreen(
|
|
context: component.context,
|
|
craftContext: component.craftContext,
|
|
resaleContext: resaleContext,
|
|
gift: selectedMainGift,
|
|
genericGift: genericGift,
|
|
selectedGiftIds: Set(component.selectedGiftIds.values),
|
|
selectingMainGift: index == 0,
|
|
starsTopUpOptions: component.starsTopUpOptionsPromise.get(),
|
|
selectGift: { [weak self] item in
|
|
guard let self, let component = self.component else {
|
|
return
|
|
}
|
|
if self.giftMap[item.gift.id] == nil {
|
|
self.giftMap[item.gift.id] = item
|
|
}
|
|
component.selectGift(index, item)
|
|
}
|
|
)
|
|
environment.controller()?.push(selectController)
|
|
},
|
|
remove: { [weak self] index in
|
|
guard let self else {
|
|
return
|
|
}
|
|
HapticFeedback().impact(.light)
|
|
|
|
self.component?.removeGift(index)
|
|
},
|
|
willFinish: { [weak self] success in
|
|
guard let self, let component = self.component else {
|
|
return
|
|
}
|
|
if !success {
|
|
component.externalState.displayFailure = true
|
|
}
|
|
self.state?.updated(transition: .easeInOut(duration: 0.5))
|
|
},
|
|
finished: { [weak self] view in
|
|
guard let self, let component = self.component, let environment = self.environment, let controller = environment.controller() as? GiftCraftScreen else {
|
|
return
|
|
}
|
|
var references: [StarGiftReference] = []
|
|
for gift in selectedGifts.values {
|
|
references.append(gift.reference)
|
|
}
|
|
Queue.mainQueue().after(0.5) {
|
|
controller.profileGiftsContext?.removeStarGifts(references: references)
|
|
}
|
|
if let _ = view {
|
|
if case let .gift(gift) = component.result {
|
|
let giftController = GiftViewScreen(
|
|
context: component.context,
|
|
subject: .profileGift(component.context.account.peerId, gift),
|
|
customAction: .init(title: environment.strings.Gift_Craft_CraftingFailed_CraftAnotherGift, action: {
|
|
component.craftAnotherGift()
|
|
})
|
|
)
|
|
if let navigationController = controller.navigationController {
|
|
navigationController.pushViewController(giftController, animated: true)
|
|
|
|
navigationController.view.addSubview(ConfettiView(frame: navigationController.view.bounds))
|
|
}
|
|
Queue.mainQueue().after(0.5) {
|
|
controller.profileGiftsContext?.insertStarGifts(gifts: [gift], afterPinned: true)
|
|
}
|
|
}
|
|
controller.view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, removeOnCompletion: false, completion: { _ in
|
|
controller.dismiss()
|
|
})
|
|
|
|
HapticFeedback().success()
|
|
} else {
|
|
Queue.mainQueue().after(0.35) {
|
|
HapticFeedback().error()
|
|
}
|
|
}
|
|
}
|
|
)
|
|
),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width, height: tableSize.height)
|
|
)
|
|
let craftTableFrame = CGRect(origin: CGPoint(x: 0.0, y: isCrafting && !"".isEmpty ? floor((component.screenSize.height - craftTableSize.height) / 2.0) : 10.0), size: craftTableSize)
|
|
if let craftTableView = self.craftTable.view {
|
|
if craftTableView.superview == nil {
|
|
craftTableView.layer.cornerRadius = 40.0
|
|
craftTableView.clipsToBounds = true
|
|
craftTableView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
|
self.addSubview(craftTableView)
|
|
}
|
|
transition.setFrame(view: craftTableView, frame: craftTableFrame)
|
|
}
|
|
|
|
transition.setAlpha(view: self.infoContainer, alpha: component.displayInfo ? 1.0 : 0.0)
|
|
|
|
let infoHeaderSize = self.infoHeader.update(
|
|
transition: transition,
|
|
component: AnyComponent(
|
|
GiftCompositionComponent(
|
|
context: component.context,
|
|
theme: environment.theme,
|
|
subject: .unique(nil, selectedMainGift),
|
|
animationOffset: nil,
|
|
animationScale: nil,
|
|
displayAnimationStars: false,
|
|
animateScaleOnTransition: false,
|
|
externalState: nil,
|
|
requestUpdate: { _ in
|
|
}
|
|
)
|
|
),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width, height: 260.0)
|
|
)
|
|
let infoHeaderFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - infoHeaderSize.width) * 0.5), y: 0.0), size: infoHeaderSize)
|
|
if let infoHeaderView = self.infoHeader.view {
|
|
if infoHeaderView.superview == nil {
|
|
self.infoContainer.layer.allowsGroupOpacity = true
|
|
self.addSubview(self.infoContainer)
|
|
|
|
self.infoContainer.layer.addSublayer(self.infoBackground)
|
|
|
|
infoHeaderView.layer.cornerRadius = 40.0
|
|
infoHeaderView.clipsToBounds = true
|
|
infoHeaderView.layer.allowsGroupOpacity = true
|
|
infoHeaderView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
|
self.infoContainer.addSubview(infoHeaderView)
|
|
}
|
|
transition.setFrame(view: infoHeaderView, frame: infoHeaderFrame)
|
|
|
|
if self.subviews.last !== self.infoContainer {
|
|
self.bringSubviewToFront(self.infoContainer)
|
|
}
|
|
}
|
|
infoContentHeight += infoHeaderSize.height
|
|
infoContentHeight += 16.0
|
|
|
|
let infoTitleSize = self.infoTitle.update(
|
|
transition: transition,
|
|
component: AnyComponent(
|
|
MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Gift_Craft_Info_Title, font: Font.bold(20.0), textColor: .white)))
|
|
),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
|
|
)
|
|
let infoTitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - infoTitleSize.width) * 0.5), y: infoHeaderSize.height - 87.0), size: infoTitleSize)
|
|
if let infoTitleView = self.infoTitle.view {
|
|
if infoTitleView.superview == nil {
|
|
self.infoContainer.addSubview(infoTitleView)
|
|
}
|
|
transition.setFrame(view: infoTitleView, frame: infoTitleFrame)
|
|
}
|
|
|
|
let infoDescriptionTextSize = self.infoDescription.update(
|
|
transition: transition,
|
|
component: AnyComponent(MultilineTextComponent(
|
|
text: .markdown(
|
|
text: environment.strings.Gift_Craft_Info_Description,
|
|
attributes: MarkdownAttributes(
|
|
body: MarkdownAttributeSet(font: Font.regular(14.0), textColor: UIColor(rgb: 0xffffff, alpha: 0.6)),
|
|
bold: MarkdownAttributeSet(font: Font.semibold(14.0), textColor: UIColor(rgb: 0xffffff, alpha: 0.6)),
|
|
link: MarkdownAttributeSet(font: Font.regular(14.0), textColor: UIColor(rgb: 0xffffff, alpha: 0.6)),
|
|
linkAttribute: { _ in return nil }
|
|
)
|
|
),
|
|
horizontalAlignment: .center,
|
|
maximumNumberOfLines: 3,
|
|
lineSpacing: 0.2
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
|
|
)
|
|
let infoDescriptionTextFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - infoDescriptionTextSize.width) * 0.5), y: infoHeaderSize.height - 56.0), size: infoDescriptionTextSize)
|
|
if let infoDescriptionTextView = self.infoDescription.view {
|
|
if infoDescriptionTextView.superview == nil {
|
|
self.infoContainer.addSubview(infoDescriptionTextView)
|
|
}
|
|
transition.setFrame(view: infoDescriptionTextView, frame: infoDescriptionTextFrame)
|
|
}
|
|
|
|
|
|
self.infoBackground.backgroundColor = environment.theme.list.modalPlainBackgroundColor.cgColor
|
|
|
|
let infoBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 80.0), size: CGSize(width: availableSize.width, height: 1000.0))
|
|
transition.setFrame(layer: self.infoBackground, frame: infoBackgroundFrame)
|
|
|
|
let titleColor = environment.theme.list.itemPrimaryTextColor
|
|
let textColor = environment.theme.list.itemSecondaryTextColor
|
|
let accentColor = environment.theme.list.itemAccentColor
|
|
var items: [AnyComponentWithIdentity<Empty>] = []
|
|
items.append(
|
|
AnyComponentWithIdentity(
|
|
id: "paragraph1",
|
|
component: AnyComponent(InfoParagraphComponent(
|
|
title: environment.strings.Gift_Craft_Info_Paragraph1_Title,
|
|
titleColor: titleColor,
|
|
text: environment.strings.Gift_Craft_Info_Paragraph1_Text,
|
|
textColor: textColor,
|
|
accentColor: accentColor,
|
|
iconName: "Premium/Craft/Rare",
|
|
iconColor: environment.theme.list.itemAccentColor
|
|
))
|
|
)
|
|
)
|
|
items.append(
|
|
AnyComponentWithIdentity(
|
|
id: "paragraph2",
|
|
component: AnyComponent(InfoParagraphComponent(
|
|
title: environment.strings.Gift_Craft_Info_Paragraph2_Title,
|
|
titleColor: titleColor,
|
|
text: environment.strings.Gift_Craft_Info_Paragraph2_Text,
|
|
textColor: textColor,
|
|
accentColor: accentColor,
|
|
iconName: "Premium/Craft/Chance",
|
|
iconColor: accentColor
|
|
))
|
|
)
|
|
)
|
|
items.append(
|
|
AnyComponentWithIdentity(
|
|
id: "paragraph3",
|
|
component: AnyComponent(InfoParagraphComponent(
|
|
title: environment.strings.Gift_Craft_Info_Paragraph3_Title,
|
|
titleColor: titleColor,
|
|
text: environment.strings.Gift_Craft_Info_Paragraph3_Text,
|
|
textColor: textColor,
|
|
accentColor: accentColor,
|
|
iconName: "Premium/Craft/Result",
|
|
iconColor: accentColor
|
|
))
|
|
)
|
|
)
|
|
|
|
let infoListSize = self.infoList.update(
|
|
transition: transition,
|
|
component: AnyComponent(
|
|
List(items)
|
|
),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width - 64.0, height: 10000)
|
|
)
|
|
let infoListFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - infoListSize.width) / 2.0), y: infoContentHeight), size: infoListSize)
|
|
if let infoListView = self.infoList.view {
|
|
if infoListView.superview == nil {
|
|
self.infoContainer.addSubview(infoListView)
|
|
}
|
|
transition.setFrame(view: infoListView, frame: infoListFrame)
|
|
}
|
|
|
|
if component.displayInfo {
|
|
infoContentHeight += infoListSize.height
|
|
infoContentHeight += 95.0
|
|
}
|
|
transition.setFrame(view: self.infoContainer, frame: CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: infoContentHeight)))
|
|
|
|
transition.setFrame(layer: self.background, frame: CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: craftContentHeight)))
|
|
transition.setFrame(layer: self.overlay, frame: CGRect(origin: CGPoint(x: 0.0, y: isCrafting && !"".isEmpty ? floor((component.screenSize.height - availableSize.width) / 2.0) : 169.0 - availableSize.width * 0.5), size: CGSize(width: availableSize.width, height: availableSize.width)))
|
|
|
|
let effectiveContentHeight: CGFloat
|
|
if component.displayInfo {
|
|
effectiveContentHeight = infoContentHeight
|
|
} else {
|
|
effectiveContentHeight = craftContentHeight
|
|
}
|
|
|
|
return CGSize(width: availableSize.width, height: effectiveContentHeight)
|
|
}
|
|
}
|
|
|
|
func makeView() -> View {
|
|
return View(frame: CGRect())
|
|
}
|
|
|
|
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
|
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
|
}
|
|
}
|
|
|
|
private final class SheetContainerComponent: CombinedComponent {
|
|
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
|
|
|
let context: AccountContext
|
|
let craftContext: CraftGiftsContext
|
|
let gift: StarGift.UniqueGift
|
|
|
|
init(
|
|
context: AccountContext,
|
|
craftContext: CraftGiftsContext,
|
|
gift: StarGift.UniqueGift
|
|
) {
|
|
self.context = context
|
|
self.craftContext = craftContext
|
|
self.gift = gift
|
|
}
|
|
|
|
static func ==(lhs: SheetContainerComponent, rhs: SheetContainerComponent) -> Bool {
|
|
if lhs.context !== rhs.context {
|
|
return false
|
|
}
|
|
if lhs.gift != rhs.gift {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
final class State: ComponentState {
|
|
private let context: AccountContext
|
|
private let giftId: Int64
|
|
|
|
var displayInfo = false
|
|
var isCrafting = false
|
|
var inProgress = false
|
|
var displayFailure = false
|
|
var result: CraftTableComponent.Result?
|
|
var selectedGiftIds: [Int32: Int64] = [:]
|
|
|
|
let starsTopUpOptionsPromise = Promise<[StarsTopUpOption]?>(nil)
|
|
|
|
private var _resaleContext: ResaleGiftsContext?
|
|
var resaleContext: ResaleGiftsContext {
|
|
if let current = self._resaleContext {
|
|
return current
|
|
} else {
|
|
let resaleContext = ResaleGiftsContext(account: self.context.account, giftId: self.giftId, forCrafting: true)
|
|
self._resaleContext = resaleContext
|
|
return resaleContext
|
|
}
|
|
}
|
|
|
|
let preloadDisposable = DisposableSet()
|
|
|
|
init(context: AccountContext, gift: StarGift.UniqueGift) {
|
|
self.context = context
|
|
self.giftId = gift.giftId
|
|
self.selectedGiftIds[0] = gift.id
|
|
|
|
super.init()
|
|
|
|
let _ = (ApplicationSpecificNotice.getGiftCraftingTips(accountManager: context.sharedContext.accountManager)
|
|
|> deliverOnMainQueue).start(next: { [weak self] count in
|
|
guard let self else {
|
|
return
|
|
}
|
|
if count < 1 {
|
|
self.displayInfo = true
|
|
self.updated()
|
|
|
|
let _ = ApplicationSpecificNotice.incrementGiftCraftingTips(accountManager: context.sharedContext.accountManager).start()
|
|
}
|
|
})
|
|
|
|
self.starsTopUpOptionsPromise.set(context.engine.payments.starsTopUpOptions() |> map(Optional.init))
|
|
}
|
|
|
|
deinit {
|
|
self.preloadDisposable.dispose()
|
|
}
|
|
}
|
|
|
|
func makeState() -> State {
|
|
return State(context: self.context, gift: self.gift)
|
|
}
|
|
|
|
static var body: Body {
|
|
let sheet = Child(ResizableSheetComponent<EnvironmentType>.self)
|
|
let animateOut = StoredActionSlot(Action<Void>.self)
|
|
|
|
let externalState = CraftGiftPageContent.ExternalState()
|
|
let playButtonAnimation = ActionSlot<Void>()
|
|
|
|
return { context in
|
|
let component = context.component
|
|
let environment = context.environment[EnvironmentType.self]
|
|
let state = context.state
|
|
|
|
let strings = environment.strings
|
|
let dateTimeFormat = environment.dateTimeFormat
|
|
|
|
if externalState.displayFailure {
|
|
state.displayFailure = true
|
|
state.inProgress = false
|
|
}
|
|
|
|
let controller = environment.controller
|
|
|
|
let craftContext = context.component.craftContext
|
|
|
|
let dismiss: (Bool) -> Void = { animated in
|
|
if animated {
|
|
animateOut.invoke(Action { _ in
|
|
if let controller = controller() {
|
|
controller.dismiss(completion: nil)
|
|
}
|
|
})
|
|
} else {
|
|
if let controller = controller() {
|
|
controller.dismiss(completion: nil)
|
|
}
|
|
}
|
|
}
|
|
|
|
let navigationController = environment.controller()?.navigationController as? NavigationController
|
|
let profileGiftsContext = (environment.controller() as? GiftCraftScreen)?.profileGiftsContext
|
|
let resaleContext = state.resaleContext
|
|
let starsTopUpOptionsPromise = state.starsTopUpOptionsPromise
|
|
let craftAnotherGift = { [weak navigationController] in
|
|
guard let navigationController else {
|
|
return
|
|
}
|
|
if let genericGift = externalState.starGiftsMap[component.gift.giftId] {
|
|
HapticFeedback().impact(.light)
|
|
|
|
let selectController = SelectCraftGiftScreen(
|
|
context: component.context,
|
|
craftContext: component.craftContext,
|
|
resaleContext: resaleContext,
|
|
gift: component.gift,
|
|
genericGift: genericGift,
|
|
selectedGiftIds: Set(),
|
|
selectingMainGift: true,
|
|
starsTopUpOptions: starsTopUpOptionsPromise.get(),
|
|
selectGift: { [weak navigationController] item in
|
|
if let navigationController{
|
|
let craftController = GiftCraftScreen(context: component.context, gift: item.gift, profileGiftsContext: profileGiftsContext)
|
|
navigationController.pushViewController(craftController)
|
|
}
|
|
}
|
|
)
|
|
navigationController.pushViewController(selectController)
|
|
}
|
|
}
|
|
|
|
let theme = environment.theme
|
|
|
|
var colors: (UIColor, UIColor, UIColor, UIColor, UIColor) = (
|
|
UIColor(rgb: 0x263245),
|
|
UIColor(rgb: 0x232e3f),
|
|
UIColor(rgb: 0x304059),
|
|
UIColor(rgb: 0x425168),
|
|
theme.list.itemCheckColors.fillColor
|
|
)
|
|
var permilleValue: Int32 = 0
|
|
for id in state.selectedGiftIds.values {
|
|
if let gift = externalState.giftsMap[id] {
|
|
permilleValue += gift.gift.craftChancePermille ?? 0
|
|
}
|
|
}
|
|
if permilleValue >= 950 {
|
|
colors.0 = UIColor(rgb: 0x1b3b3d)
|
|
colors.1 = UIColor(rgb: 0x1a2f38)
|
|
colors.2 = UIColor(rgb: 0x22464a)
|
|
colors.3 = UIColor(rgb: 0x2d4e50)
|
|
if !state.displayInfo {
|
|
colors.4 = UIColor(rgb: 0x33bf54)
|
|
}
|
|
}
|
|
if state.displayFailure {
|
|
colors.0 = UIColor(rgb: 0x46231a)
|
|
colors.1 = UIColor(rgb: 0x381b1a)
|
|
colors.2 = UIColor(rgb: 0x51291f)
|
|
colors.3 = UIColor(rgb: 0x683e34)
|
|
if !state.displayInfo {
|
|
colors.4 = UIColor(rgb: 0x683e34)
|
|
}
|
|
}
|
|
|
|
var selectedMainGift = component.gift
|
|
if state.selectedGiftIds[0] != selectedMainGift.id, let id = state.selectedGiftIds[0], let gift = externalState.giftsMap[id]?.gift {
|
|
selectedMainGift = gift
|
|
}
|
|
|
|
var buttonColor = colors.3
|
|
if state.displayInfo, let backdropAttribute = selectedMainGift.attributes.first(where: { attribute in
|
|
if case .backdrop = attribute {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}), case let .backdrop(_, _, _, outerColor, _, _, _) = backdropAttribute {
|
|
buttonColor = UIColor(rgb: UInt32(bitPattern: outerColor)).mixedWith(.white, alpha: 0.2)
|
|
}
|
|
|
|
var backgroundColor = colors.1
|
|
if state.displayInfo {
|
|
backgroundColor = environment.theme.list.plainBackgroundColor
|
|
}
|
|
|
|
let giftTitle = "\(selectedMainGift.title) #\(formatCollectibleNumber(selectedMainGift.number, dateTimeFormat: environment.dateTimeFormat))"
|
|
|
|
let buttonContent: AnyComponentWithIdentity<Empty>
|
|
if state.displayInfo {
|
|
var buttonTitle: [AnyComponentWithIdentity<Empty>] = []
|
|
buttonTitle.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(LottieComponent(
|
|
content: LottieComponent.AppBundleContent(name: "anim_ok"),
|
|
color: environment.theme.list.itemCheckColors.foregroundColor,
|
|
startingPosition: .begin,
|
|
size: CGSize(width: 28.0, height: 28.0),
|
|
playOnce: playButtonAnimation
|
|
))))
|
|
buttonTitle.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(ButtonTextContentComponent(
|
|
text: strings.Gift_Craft_Info_Understood,
|
|
badge: 0,
|
|
textColor: environment.theme.list.itemCheckColors.foregroundColor,
|
|
badgeBackground: environment.theme.list.itemCheckColors.foregroundColor,
|
|
badgeForeground: environment.theme.list.itemCheckColors.fillColor
|
|
))))
|
|
buttonContent = AnyComponentWithIdentity(id: "info", component: AnyComponent(
|
|
HStack(buttonTitle, spacing: 2.0)
|
|
))
|
|
} else if state.displayFailure {
|
|
buttonContent = AnyComponentWithIdentity(id: "fail", component: AnyComponent(
|
|
MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Gift_Craft_CraftingFailed_CraftAnotherGift, font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor)))
|
|
))
|
|
} else {
|
|
var buttonAnimatedItems: [AnimatedTextComponent.Item] = []
|
|
|
|
let rawString = environment.strings.Gift_Craft_Crafting_SuccessChance("{p}").string
|
|
var startIndex = rawString.startIndex
|
|
while true {
|
|
if let range = rawString.range(of: "{", range: startIndex ..< rawString.endIndex) {
|
|
if range.lowerBound != startIndex {
|
|
buttonAnimatedItems.append(AnimatedTextComponent.Item(id: AnyHashable(buttonAnimatedItems.count), content: .text(String(rawString[startIndex ..< range.lowerBound]))))
|
|
}
|
|
|
|
startIndex = range.upperBound
|
|
if let endRange = rawString.range(of: "}", range: startIndex ..< rawString.endIndex) {
|
|
let controlString = rawString[range.upperBound ..< endRange.lowerBound]
|
|
if controlString == "p" {
|
|
buttonAnimatedItems.append(AnimatedTextComponent.Item(id: AnyHashable(buttonAnimatedItems.count), content: .number(Int(permilleValue / 10), minDigits: 1)))
|
|
}
|
|
startIndex = endRange.upperBound
|
|
}
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
if startIndex != rawString.endIndex {
|
|
buttonAnimatedItems.append(AnimatedTextComponent.Item(id: AnyHashable(buttonAnimatedItems.count), content: .text(String(rawString[startIndex ..< rawString.endIndex]))))
|
|
}
|
|
|
|
buttonContent = AnyComponentWithIdentity(id: "craft", component: AnyComponent(
|
|
VStack([
|
|
AnyComponentWithIdentity(
|
|
id: AnyHashable("label"),
|
|
component: AnyComponent(
|
|
HStack([
|
|
AnyComponentWithIdentity(
|
|
id: AnyHashable("label"),
|
|
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Gift_Craft_Craft(giftTitle).string, font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor))))
|
|
)
|
|
], spacing: 2.0)
|
|
)
|
|
),
|
|
AnyComponentWithIdentity(
|
|
id: AnyHashable("level"),
|
|
component: AnyComponent(
|
|
AnimatedTextComponent(
|
|
font: Font.with(size: 13.0, weight: .medium, traits: .monospacedNumbers),
|
|
color: environment.theme.list.itemCheckColors.foregroundColor,
|
|
items: buttonAnimatedItems,
|
|
noDelay: true
|
|
)
|
|
)
|
|
)
|
|
], spacing: 0.0)
|
|
))
|
|
}
|
|
|
|
var displayState: CraftGiftPageContent.DisplayState = .default
|
|
if state.displayFailure {
|
|
displayState = .failure
|
|
} else if state.isCrafting {
|
|
displayState = .crafting
|
|
}
|
|
|
|
let hideButtons = displayState == .crafting
|
|
|
|
let sheet = sheet.update(
|
|
component: ResizableSheetComponent<EnvironmentType>(
|
|
content: AnyComponent<EnvironmentType>(
|
|
CraftGiftPageContent(
|
|
context: component.context,
|
|
craftContext: component.craftContext,
|
|
resaleContext: { [weak state] in
|
|
return state?.resaleContext
|
|
},
|
|
colors: colors,
|
|
gift: component.gift,
|
|
selectedGiftIds: state.selectedGiftIds,
|
|
displayState: displayState,
|
|
displayInfo: state.displayInfo,
|
|
result: state.result,
|
|
screenSize: context.availableSize,
|
|
externalState: externalState,
|
|
starsTopUpOptionsPromise: state.starsTopUpOptionsPromise,
|
|
selectGift: { [weak state] index, gift in
|
|
guard let state else {
|
|
return
|
|
}
|
|
state.selectedGiftIds[index] = gift.gift.id
|
|
state.updated(transition: .spring(duration: 0.4))
|
|
},
|
|
removeGift: { [weak state] index in
|
|
guard let state else {
|
|
return
|
|
}
|
|
state.selectedGiftIds[index] = nil
|
|
state.updated(transition: .spring(duration: 0.4))
|
|
},
|
|
craftAnotherGift: craftAnotherGift,
|
|
dismiss: {
|
|
dismiss(true)
|
|
}
|
|
)
|
|
),
|
|
leftItem: hideButtons ? nil : AnyComponent(
|
|
GlassBarButtonComponent(
|
|
size: CGSize(width: 44.0, height: 44.0),
|
|
backgroundColor: buttonColor,
|
|
isDark: true,
|
|
state: .tintedGlass,
|
|
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
|
|
BundleIconComponent(
|
|
name: "Navigation/Close",
|
|
tintColor: .white
|
|
)
|
|
)),
|
|
action: { [weak state] _ in
|
|
guard let state else {
|
|
return
|
|
}
|
|
if state.displayInfo {
|
|
state.displayInfo = false
|
|
state.updated(transition: .spring(duration: 0.3))
|
|
} else {
|
|
dismiss(true)
|
|
}
|
|
}
|
|
)
|
|
),
|
|
rightItem: hideButtons || state.displayInfo ? nil : AnyComponent(
|
|
GlassBarButtonComponent(
|
|
size: CGSize(width: 44.0, height: 44.0),
|
|
backgroundColor: buttonColor,
|
|
isDark: true,
|
|
state: .tintedGlass,
|
|
component: AnyComponentWithIdentity(id: "info", component: AnyComponent(
|
|
BundleIconComponent(
|
|
name: "Navigation/Question",
|
|
tintColor: .white
|
|
)
|
|
)),
|
|
action: { [weak state] _ in
|
|
guard let state, !state.inProgress else {
|
|
return
|
|
}
|
|
state.displayInfo = true
|
|
state.updated(transition: .spring(duration: 0.3))
|
|
playButtonAnimation.invoke(Void())
|
|
}
|
|
)
|
|
),
|
|
hasTopEdgeEffect: false,
|
|
bottomItem: hideButtons ? nil : AnyComponent(
|
|
ButtonComponent(
|
|
background: ButtonComponent.Background(
|
|
style: .glass,
|
|
color: colors.4,
|
|
foreground: environment.theme.list.itemCheckColors.foregroundColor,
|
|
pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
|
|
),
|
|
content: buttonContent,
|
|
isEnabled: state.displayInfo ? true : state.selectedGiftIds.count > 0,
|
|
displaysProgress: state.inProgress,
|
|
action: { [weak state] in
|
|
guard let state else {
|
|
return
|
|
}
|
|
if state.displayInfo {
|
|
state.displayInfo = false
|
|
state.updated(transition: .spring(duration: 0.3))
|
|
} else if state.displayFailure {
|
|
craftAnotherGift()
|
|
dismiss(true)
|
|
} else {
|
|
HapticFeedback().impact(.medium)
|
|
|
|
state.inProgress = true
|
|
state.updated(transition: .spring(duration: 0.3))
|
|
|
|
if let testFailOrSuccess = externalState.testFailOrSuccess {
|
|
Queue.mainQueue().after(0.5, {
|
|
state.isCrafting = true
|
|
if testFailOrSuccess {
|
|
state.result = .gift(ProfileGiftsContext.State.StarGift(gift: .unique(component.gift), reference: nil, fromPeer: nil, date: 0, text: "", entities: nil, nameHidden: false, savedToProfile: false, pinnedToTop: false, convertStars: nil, canUpgrade: false, canExportDate: nil, upgradeStars: nil, transferStars: nil, canTransferDate: nil, canResaleDate: nil, collectionIds: nil, prepaidUpgradeHash: nil, upgradeSeparate: false, dropOriginalDetailsStars: nil, number: nil, isRefunded: false, canCraftAt: nil))
|
|
} else {
|
|
state.result = .fail
|
|
}
|
|
state.updated(transition: .spring(duration: 0.8))
|
|
})
|
|
return
|
|
}
|
|
|
|
var indices: [Int] = []
|
|
for index in state.selectedGiftIds.keys.sorted() {
|
|
indices.append(Int(index))
|
|
}
|
|
var references: [StarGiftReference] = []
|
|
for index in indices {
|
|
if let giftId = state.selectedGiftIds[Int32(index)], let gift = externalState.giftsMap[giftId] {
|
|
references.append(gift.reference)
|
|
}
|
|
}
|
|
let _ = (craftContext.craft(references: references)
|
|
|> deliverOnMainQueue).start(next: { [weak state] result in
|
|
guard let state else {
|
|
return
|
|
}
|
|
state.isCrafting = true
|
|
state.result = .gift(result)
|
|
state.updated(transition: .spring(duration: 0.8))
|
|
|
|
if case let .unique(uniqueGift) = result.gift {
|
|
for attribute in uniqueGift.attributes {
|
|
switch attribute {
|
|
case let .model(_, file, _, _):
|
|
state.preloadDisposable.add(freeMediaFileResourceInteractiveFetched(account: component.context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start())
|
|
case let .pattern(_, file, _):
|
|
state.preloadDisposable.add(freeMediaFileResourceInteractiveFetched(account: component.context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start())
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
Queue.mainQueue().after(1.0) {
|
|
craftContext.reload()
|
|
}
|
|
}, error: { error in
|
|
switch error {
|
|
case .craftFailed:
|
|
state.isCrafting = true
|
|
state.result = .fail
|
|
state.updated(transition: .spring(duration: 0.8))
|
|
|
|
Queue.mainQueue().after(1.0) {
|
|
craftContext.reload()
|
|
}
|
|
default:
|
|
if let navigationController = controller()?.navigationController {
|
|
var text: String = strings.Login_UnknownError
|
|
switch error {
|
|
case let .tooEarly(canCraftDate):
|
|
let dateString = stringForFullDate(timestamp: canCraftDate, strings: strings, dateTimeFormat: dateTimeFormat)
|
|
text = strings.Gift_Craft_Unavailable_Text(dateString).string
|
|
case .unavailable:
|
|
text = strings.Gift_Craft_Error_NotAvailable
|
|
default:
|
|
break
|
|
}
|
|
dismiss(true)
|
|
let alertController = textAlertController(context: component.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strings.Common_OK, action: {})])
|
|
(navigationController.topViewController as? ViewController)?.present(alertController, in: .window(.root))
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
)
|
|
),
|
|
backgroundColor: .color(backgroundColor),
|
|
isFullscreen: false,
|
|
animateOut: animateOut
|
|
),
|
|
environment: {
|
|
environment
|
|
ResizableSheetComponentEnvironment(
|
|
theme: theme,
|
|
statusBarHeight: environment.statusBarHeight,
|
|
safeInsets: environment.safeInsets,
|
|
metrics: environment.metrics,
|
|
deviceMetrics: environment.deviceMetrics,
|
|
isDisplaying: environment.value.isVisible,
|
|
isCentered: environment.metrics.widthClass == .regular,
|
|
screenSize: context.availableSize,
|
|
regularMetricsSize: CGSize(width: 430.0, height: 900.0),
|
|
dismiss: { animated in
|
|
dismiss(animated)
|
|
}
|
|
)
|
|
},
|
|
availableSize: context.availableSize,
|
|
transition: context.transition
|
|
)
|
|
|
|
context.add(sheet
|
|
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
|
|
)
|
|
|
|
return context.availableSize
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
public class GiftCraftScreen: ViewControllerComponentContainer {
|
|
fileprivate weak var profileGiftsContext: ProfileGiftsContext?
|
|
|
|
public init(
|
|
context: AccountContext,
|
|
gift: StarGift.UniqueGift,
|
|
profileGiftsContext: ProfileGiftsContext?
|
|
) {
|
|
self.profileGiftsContext = profileGiftsContext
|
|
|
|
let craftContext = CraftGiftsContext(account: context.account, giftId: gift.giftId)
|
|
super.init(
|
|
context: context,
|
|
component: SheetContainerComponent(
|
|
context: context,
|
|
craftContext: craftContext,
|
|
gift: gift
|
|
),
|
|
navigationBarAppearance: .none,
|
|
statusBarStyle: .ignore,
|
|
theme: .default
|
|
)
|
|
|
|
self.navigationPresentation = .flatModal
|
|
}
|
|
|
|
required public init(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
fileprivate func dismissAllTooltips() {
|
|
self.window?.forEachController({ controller in
|
|
if let controller = controller as? TooltipScreen {
|
|
controller.dismiss(inPlace: false)
|
|
}
|
|
})
|
|
self.forEachController({ controller in
|
|
if let controller = controller as? TooltipScreen {
|
|
controller.dismiss(inPlace: false)
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
|
|
public func dismissAnimated() {
|
|
self.dismissAllTooltips()
|
|
|
|
if let view = self.node.hostView.findTaggedView(tag: ResizableSheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? ResizableSheetComponent<ViewControllerComponentContainer.Environment>.View {
|
|
view.dismissAnimated()
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private struct GiftCraftConfiguration {
|
|
static var defaultValue: GiftCraftConfiguration {
|
|
return GiftCraftConfiguration(
|
|
craftAttributePermilles: [[90], [80, 200], [70, 190, 460], [60, 180, 450, 1000]]
|
|
)
|
|
}
|
|
|
|
let craftAttributePermilles: [[Int32]]
|
|
|
|
fileprivate init(
|
|
craftAttributePermilles: [[Int32]]
|
|
) {
|
|
self.craftAttributePermilles = craftAttributePermilles
|
|
}
|
|
|
|
static func with(appConfiguration: AppConfiguration) -> GiftCraftConfiguration {
|
|
if let data = appConfiguration.data {
|
|
var craftAttributePermilles: [[Int32]] = []
|
|
if let value = data["stargifts_craft_attribute_permilles"] as? [[Double]] {
|
|
craftAttributePermilles = value.map { innerArray in
|
|
innerArray.map { Int32($0) }
|
|
}
|
|
} else {
|
|
craftAttributePermilles = GiftCraftConfiguration.defaultValue.craftAttributePermilles
|
|
}
|
|
|
|
return GiftCraftConfiguration(
|
|
craftAttributePermilles: craftAttributePermilles
|
|
)
|
|
} else {
|
|
return .defaultValue
|
|
}
|
|
}
|
|
}
|