mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-22 22:25:57 +00:00
Update API
This commit is contained in:
@@ -0,0 +1,909 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import PresentationDataUtils
|
||||
import AccountContext
|
||||
import ComponentFlow
|
||||
import ViewControllerComponent
|
||||
import MultilineTextComponent
|
||||
import BalancedTextComponent
|
||||
import BundleIconComponent
|
||||
import Markdown
|
||||
import TelegramStringFormatting
|
||||
import PlainButtonComponent
|
||||
import BlurredBackgroundComponent
|
||||
import PremiumStarComponent
|
||||
import ConfettiEffect
|
||||
import TextFormat
|
||||
import GiftItemComponent
|
||||
import InAppPurchaseManager
|
||||
import TabSelectorComponent
|
||||
import GiftSetupScreen
|
||||
|
||||
final class GiftOptionsScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
let context: AccountContext
|
||||
let peerId: EnginePeer.Id
|
||||
let premiumOptions: [CachedPremiumGiftOption]
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
peerId: EnginePeer.Id,
|
||||
premiumOptions: [CachedPremiumGiftOption]
|
||||
) {
|
||||
self.context = context
|
||||
self.peerId = peerId
|
||||
self.premiumOptions = premiumOptions
|
||||
}
|
||||
|
||||
static func ==(lhs: GiftOptionsScreenComponent, rhs: GiftOptionsScreenComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.peerId != rhs.peerId {
|
||||
return false
|
||||
}
|
||||
if lhs.premiumOptions != rhs.premiumOptions {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private final class ScrollView: UIScrollView {
|
||||
override func touchesShouldCancel(in view: UIView) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public enum StarsFilter: Int {
|
||||
case all
|
||||
case limited
|
||||
case stars10
|
||||
case stars25
|
||||
case stars50
|
||||
case stars100
|
||||
}
|
||||
|
||||
final class View: UIView, UIScrollViewDelegate {
|
||||
private let topOverscrollLayer = SimpleLayer()
|
||||
private let scrollView: ScrollView
|
||||
|
||||
private let topPanel = ComponentView<Empty>()
|
||||
private let topSeparator = ComponentView<Empty>()
|
||||
private let cancelButton = ComponentView<Empty>()
|
||||
|
||||
private let header = ComponentView<Empty>()
|
||||
|
||||
private let premiumTitle = ComponentView<Empty>()
|
||||
private let premiumDescription = ComponentView<Empty>()
|
||||
private var premiumItems: [AnyHashable: ComponentView<Empty>] = [:]
|
||||
private var selectedPremiumGift: String?
|
||||
|
||||
private let starsTitle = ComponentView<Empty>()
|
||||
private let starsDescription = ComponentView<Empty>()
|
||||
private var starsItems: [AnyHashable: ComponentView<Empty>] = [:]
|
||||
private let tabSelector = ComponentView<Empty>()
|
||||
private var starsFilter: StarsFilter = .all
|
||||
|
||||
private var isUpdating: Bool = false
|
||||
|
||||
private var component: GiftOptionsScreenComponent?
|
||||
private(set) weak var state: State?
|
||||
private var environment: EnvironmentType?
|
||||
|
||||
private var starsItemsOrigin: CGFloat = 0.0
|
||||
|
||||
private var chevronImage: (UIImage, PresentationTheme)?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.scrollView = ScrollView()
|
||||
self.scrollView.showsVerticalScrollIndicator = true
|
||||
self.scrollView.showsHorizontalScrollIndicator = false
|
||||
self.scrollView.scrollsToTop = false
|
||||
self.scrollView.delaysContentTouches = false
|
||||
self.scrollView.canCancelContentTouches = true
|
||||
self.scrollView.contentInsetAdjustmentBehavior = .never
|
||||
if #available(iOS 13.0, *) {
|
||||
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
|
||||
}
|
||||
self.scrollView.alwaysBounceVertical = true
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.scrollView.delegate = self
|
||||
self.addSubview(self.scrollView)
|
||||
|
||||
self.scrollView.layer.addSublayer(self.topOverscrollLayer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
}
|
||||
|
||||
func scrollToTop() {
|
||||
self.scrollView.setContentOffset(CGPoint(), animated: true)
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
self.updateScrolling(transition: .immediate)
|
||||
}
|
||||
|
||||
private func updateScrolling(transition: ComponentTransition) {
|
||||
guard let environment = self.environment, let component = self.component else {
|
||||
return
|
||||
}
|
||||
|
||||
let availableWidth = self.scrollView.bounds.width
|
||||
let contentOffset = self.scrollView.contentOffset.y
|
||||
|
||||
let topPanelAlpha = min(20.0, max(0.0, contentOffset - 95.0)) / 20.0
|
||||
if let topPanelView = self.topPanel.view, let topSeparator = self.topSeparator.view {
|
||||
transition.setAlpha(view: topPanelView, alpha: topPanelAlpha)
|
||||
transition.setAlpha(view: topSeparator, alpha: topPanelAlpha)
|
||||
}
|
||||
|
||||
let topInset: CGFloat = environment.navigationHeight - 56.0
|
||||
|
||||
let premiumTitleInitialPosition = (topInset + 160.0)
|
||||
let premiumTitleOffsetDelta = premiumTitleInitialPosition - (environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0)
|
||||
let premiumTitleOffset = contentOffset + max(0.0, min(1.0, contentOffset / premiumTitleOffsetDelta)) * 10.0
|
||||
let premiumTitleFraction = max(0.0, min(1.0, premiumTitleOffset / premiumTitleOffsetDelta))
|
||||
let premiumTitleScale = 1.0 - premiumTitleFraction * 0.36
|
||||
var premiumTitleAdditionalOffset: CGFloat = 0.0
|
||||
|
||||
let starsTitleOffsetDelta = (topInset + 100.0) - (environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0)
|
||||
|
||||
let starsTitleOffset: CGFloat
|
||||
let starsTitleFraction: CGFloat
|
||||
if contentOffset > 350 {
|
||||
starsTitleOffset = contentOffset + max(0.0, min(1.0, (contentOffset - 350.0) / starsTitleOffsetDelta)) * 10.0
|
||||
starsTitleFraction = max(0.0, min(1.0, (starsTitleOffset - 350.0) / starsTitleOffsetDelta))
|
||||
if contentOffset > 380.0 {
|
||||
premiumTitleAdditionalOffset = contentOffset - 380.0
|
||||
}
|
||||
} else {
|
||||
starsTitleOffset = contentOffset
|
||||
starsTitleFraction = 0.0
|
||||
}
|
||||
let starsTitleScale = 1.0 - starsTitleFraction * 0.36
|
||||
if let starsTitleView = self.starsTitle.view {
|
||||
transition.setPosition(view: starsTitleView, position: CGPoint(x: availableWidth / 2.0, y: max(topInset + 455.0 - starsTitleOffset, environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0)))
|
||||
transition.setScale(view: starsTitleView, scale: starsTitleScale)
|
||||
}
|
||||
|
||||
if let premiumTitleView = self.premiumTitle.view {
|
||||
transition.setPosition(view: premiumTitleView, position: CGPoint(x: availableWidth / 2.0, y: max(premiumTitleInitialPosition - premiumTitleOffset, environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0) - premiumTitleAdditionalOffset))
|
||||
transition.setScale(view: premiumTitleView, scale: premiumTitleScale)
|
||||
}
|
||||
|
||||
|
||||
if let headerView = self.header.view {
|
||||
transition.setPosition(view: headerView, position: CGPoint(x: availableWidth / 2.0, y: topInset + headerView.bounds.height / 2.0 - 30.0 - premiumTitleOffset * premiumTitleScale))
|
||||
transition.setScale(view: headerView, scale: premiumTitleScale)
|
||||
}
|
||||
|
||||
let visibleBounds = self.scrollView.bounds.insetBy(dx: 0.0, dy: -10.0)
|
||||
if let starGifts = self.state?.starGifts {
|
||||
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
|
||||
|
||||
let optionSpacing: CGFloat = 10.0
|
||||
let optionWidth = (availableWidth - sideInset * 2.0 - optionSpacing * 2.0) / 3.0
|
||||
let starsOptionSize = CGSize(width: optionWidth, height: 154.0)
|
||||
|
||||
var validIds: [AnyHashable] = []
|
||||
var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: self.starsItemsOrigin), size: starsOptionSize)
|
||||
|
||||
let controller = environment.controller
|
||||
|
||||
for gift in starGifts {
|
||||
var isVisible = false
|
||||
if visibleBounds.intersects(itemFrame) {
|
||||
isVisible = true
|
||||
}
|
||||
|
||||
if isVisible {
|
||||
if self.starsFilter != .all {
|
||||
switch self.starsFilter {
|
||||
case .all:
|
||||
break
|
||||
case .limited:
|
||||
if gift.availability == nil {
|
||||
continue
|
||||
}
|
||||
case .stars10:
|
||||
if gift.price != 10 {
|
||||
continue
|
||||
}
|
||||
case .stars25:
|
||||
if gift.price != 25 {
|
||||
continue
|
||||
}
|
||||
case .stars50:
|
||||
if gift.price != 50 {
|
||||
continue
|
||||
}
|
||||
case .stars100:
|
||||
if gift.price != 100 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let itemId = AnyHashable(gift.id)
|
||||
validIds.append(itemId)
|
||||
|
||||
var itemTransition = transition
|
||||
let visibleItem: ComponentView<Empty>
|
||||
if let current = self.starsItems[itemId] {
|
||||
visibleItem = current
|
||||
} else {
|
||||
visibleItem = ComponentView()
|
||||
if !transition.animation.isImmediate {
|
||||
itemTransition = .immediate
|
||||
}
|
||||
self.starsItems[itemId] = visibleItem
|
||||
}
|
||||
|
||||
let _ = visibleItem.update(
|
||||
transition: itemTransition,
|
||||
component: AnyComponent(
|
||||
PlainButtonComponent(
|
||||
content: AnyComponent(
|
||||
GiftItemComponent(
|
||||
context: component.context,
|
||||
theme: environment.theme,
|
||||
peer: nil,
|
||||
subject: .starGift(gift.id, gift.file),
|
||||
price: "⭐️ \(gift.price)",
|
||||
ribbon: gift.availability != nil ?
|
||||
GiftItemComponent.Ribbon(
|
||||
text: "Limited",
|
||||
color: UIColor(rgb: 0x58c1fe)
|
||||
)
|
||||
: nil
|
||||
)
|
||||
),
|
||||
effectAlignment: .center,
|
||||
action: { [weak self] in
|
||||
if let self, let component = self.component {
|
||||
controller()?.push(GiftSetupScreen(context: component.context, peerId: component.peerId, gift: gift))
|
||||
}
|
||||
},
|
||||
animateAlpha: false
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: starsOptionSize
|
||||
)
|
||||
if let itemView = visibleItem.view {
|
||||
if itemView.superview == nil {
|
||||
self.scrollView.addSubview(itemView)
|
||||
if !transition.animation.isImmediate {
|
||||
transition.animateAlpha(view: itemView, from: 0.0, to: 1.0)
|
||||
transition.animateScale(view: itemView, from: 0.01, to: 1.0)
|
||||
}
|
||||
}
|
||||
itemTransition.setFrame(view: itemView, frame: itemFrame)
|
||||
}
|
||||
}
|
||||
itemFrame.origin.x += itemFrame.width + optionSpacing
|
||||
if itemFrame.maxX > availableWidth {
|
||||
itemFrame.origin.x = sideInset
|
||||
itemFrame.origin.y += starsOptionSize.height + optionSpacing
|
||||
}
|
||||
}
|
||||
|
||||
var removeIds: [AnyHashable] = []
|
||||
for (id, item) in self.starsItems {
|
||||
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.starsItems.removeValue(forKey: id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: GiftOptionsScreenComponent, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
|
||||
self.isUpdating = true
|
||||
defer {
|
||||
self.isUpdating = false
|
||||
}
|
||||
|
||||
let environment = environment[EnvironmentType.self].value
|
||||
let controller = environment.controller
|
||||
let themeUpdated = self.environment?.theme !== environment.theme
|
||||
self.environment = environment
|
||||
|
||||
if self.component == nil {
|
||||
|
||||
}
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
if themeUpdated {
|
||||
self.backgroundColor = environment.theme.list.blocksBackgroundColor
|
||||
}
|
||||
|
||||
// let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let theme = environment.theme
|
||||
let strings = environment.strings
|
||||
|
||||
let textColor = theme.list.itemPrimaryTextColor
|
||||
let accentColor = theme.list.itemAccentColor
|
||||
|
||||
let textFont = Font.regular(15.0)
|
||||
let boldTextFont = Font.semibold(15.0)
|
||||
|
||||
let bottomContentInset: CGFloat = 24.0
|
||||
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
|
||||
let sectionSpacing: CGFloat = 24.0
|
||||
|
||||
let _ = bottomContentInset
|
||||
let _ = sectionSpacing
|
||||
|
||||
var contentHeight: CGFloat = 0.0
|
||||
contentHeight += environment.navigationHeight - 56.0 + 188.0
|
||||
|
||||
let headerSize = self.header.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
GiftAvatarComponent(
|
||||
context: component.context,
|
||||
theme: theme,
|
||||
peers: state.peer.flatMap { [$0] } ?? [],
|
||||
isVisible: true,
|
||||
hasIdleAnimations: true,
|
||||
color: UIColor(rgb: 0xf9b004),
|
||||
hasLargeParticles: true
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: min(414.0, availableSize.width), height: 220.0)
|
||||
)
|
||||
if let headerView = self.header.view {
|
||||
if headerView.superview == nil {
|
||||
self.addSubview(headerView)
|
||||
}
|
||||
transition.setBounds(view: headerView, bounds: CGRect(origin: .zero, size: headerSize))
|
||||
}
|
||||
|
||||
let topPanelSize = self.topPanel.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(BlurredBackgroundComponent(
|
||||
color: theme.rootController.navigationBar.blurredBackgroundColor
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: environment.navigationHeight)
|
||||
)
|
||||
|
||||
let topSeparatorSize = self.topSeparator.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(Rectangle(
|
||||
color: theme.rootController.navigationBar.separatorColor
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: UIScreenPixel)
|
||||
)
|
||||
let topPanelFrame = CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: topPanelSize.height))
|
||||
let topSeparatorFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelSize.height), size: CGSize(width: topSeparatorSize.width, height: topSeparatorSize.height))
|
||||
if let topPanelView = self.topPanel.view, let topSeparatorView = self.topSeparator.view {
|
||||
if topPanelView.superview == nil {
|
||||
self.addSubview(topPanelView)
|
||||
self.addSubview(topSeparatorView)
|
||||
}
|
||||
transition.setFrame(view: topPanelView, frame: topPanelFrame)
|
||||
transition.setFrame(view: topSeparatorView, frame: topSeparatorFrame)
|
||||
}
|
||||
|
||||
let cancelButtonSize = self.cancelButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
PlainButtonComponent(
|
||||
content: AnyComponent(
|
||||
MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: strings.Common_Cancel, font: Font.regular(17.0), textColor: theme.rootController.navigationBar.accentTextColor)),
|
||||
horizontalAlignment: .center
|
||||
)
|
||||
),
|
||||
effectAlignment: .center,
|
||||
action: {
|
||||
controller()?.dismiss()
|
||||
},
|
||||
animateScale: false
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: 100.0)
|
||||
)
|
||||
let cancelButtonFrame = CGRect(origin: CGPoint(x: environment.safeInsets.left + 16.0, y: environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0 - cancelButtonSize.height / 2.0), size: cancelButtonSize)
|
||||
if let cancelButtonView = self.cancelButton.view {
|
||||
if cancelButtonView.superview == nil {
|
||||
self.addSubview(cancelButtonView)
|
||||
}
|
||||
transition.setFrame(view: cancelButtonView, frame: cancelButtonFrame)
|
||||
}
|
||||
|
||||
let premiumTitleSize = self.premiumTitle.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: "Gift Premium", font: Font.bold(28.0), textColor: theme.rootController.navigationBar.primaryTextColor)),
|
||||
horizontalAlignment: .center
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: 100.0)
|
||||
)
|
||||
if let premiumTitleView = self.premiumTitle.view {
|
||||
if premiumTitleView.superview == nil {
|
||||
self.addSubview(premiumTitleView)
|
||||
}
|
||||
transition.setBounds(view: premiumTitleView, bounds: CGRect(origin: .zero, size: premiumTitleSize))
|
||||
}
|
||||
|
||||
if self.chevronImage == nil || self.chevronImage?.1 !== theme {
|
||||
self.chevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: accentColor)!, theme)
|
||||
}
|
||||
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 peerName = state.peer?.compactDisplayTitle ?? ""
|
||||
|
||||
let premiumDescriptionString = parseMarkdownIntoAttributedString("Give **\(peerName)** access to exclusive features with Telegram Premium. [See Features >]()", attributes: markdownAttributes).mutableCopy() as! NSMutableAttributedString
|
||||
if let range = premiumDescriptionString.string.range(of: ">"), let chevronImage = self.chevronImage?.0 {
|
||||
premiumDescriptionString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: premiumDescriptionString.string))
|
||||
}
|
||||
let premiumDescriptionSize = self.premiumDescription.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(BalancedTextComponent(
|
||||
text: .plain(premiumDescriptionString),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 0,
|
||||
lineSpacing: 0.2,
|
||||
highlightColor: accentColor.withAlphaComponent(0.2),
|
||||
highlightAction: { attributes in
|
||||
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
|
||||
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
},
|
||||
tapAction: { _, _ in
|
||||
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: 1000.0)
|
||||
)
|
||||
let premiumDescriptionFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - premiumDescriptionSize.width) / 2.0), y: contentHeight), size: premiumDescriptionSize)
|
||||
if let premiumDescriptionView = self.premiumDescription.view {
|
||||
if premiumDescriptionView.superview == nil {
|
||||
self.scrollView.addSubview(premiumDescriptionView)
|
||||
}
|
||||
transition.setFrame(view: premiumDescriptionView, frame: premiumDescriptionFrame)
|
||||
}
|
||||
contentHeight += premiumDescriptionSize.height
|
||||
contentHeight += 11.0
|
||||
|
||||
let optionSpacing: CGFloat = 10.0
|
||||
let optionWidth = (availableSize.width - sideInset * 2.0 - optionSpacing * 2.0) / 3.0
|
||||
|
||||
if let premiumProducts = state.premiumProducts {
|
||||
let premiumOptionSize = CGSize(width: optionWidth, height: 178.0)
|
||||
|
||||
var validIds: [AnyHashable] = []
|
||||
var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: premiumOptionSize)
|
||||
for product in premiumProducts {
|
||||
let itemId = AnyHashable(product.storeProduct.id)
|
||||
validIds.append(itemId)
|
||||
|
||||
var itemTransition = transition
|
||||
let visibleItem: ComponentView<Empty>
|
||||
if let current = self.premiumItems[itemId] {
|
||||
visibleItem = current
|
||||
} else {
|
||||
visibleItem = ComponentView()
|
||||
if !transition.animation.isImmediate {
|
||||
itemTransition = .immediate
|
||||
}
|
||||
self.premiumItems[itemId] = visibleItem
|
||||
}
|
||||
|
||||
let title: String
|
||||
switch product.months {
|
||||
case 6:
|
||||
title = "6 months"
|
||||
case 12:
|
||||
title = "1 year"
|
||||
default:
|
||||
title = "3 months"
|
||||
}
|
||||
|
||||
let _ = visibleItem.update(
|
||||
transition: itemTransition,
|
||||
component: AnyComponent(
|
||||
PlainButtonComponent(
|
||||
content: AnyComponent(
|
||||
GiftItemComponent(
|
||||
context: component.context,
|
||||
theme: theme,
|
||||
peer: nil,
|
||||
subject: .premium(product.months),
|
||||
title: title,
|
||||
subtitle: "Premium",
|
||||
price: product.price,
|
||||
ribbon: product.discount.flatMap {
|
||||
GiftItemComponent.Ribbon(
|
||||
text: "-\($0)%",
|
||||
color: UIColor(rgb: 0xfa4846)
|
||||
)
|
||||
},
|
||||
isLoading: self.selectedPremiumGift == product.id
|
||||
)
|
||||
),
|
||||
effectAlignment: .center,
|
||||
action: { [weak self] in
|
||||
self?.selectedPremiumGift = product.id
|
||||
self?.state?.updated()
|
||||
|
||||
Queue.mainQueue().after(4.0, {
|
||||
self?.selectedPremiumGift = nil
|
||||
self?.state?.updated()
|
||||
})
|
||||
},
|
||||
animateAlpha: false
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: premiumOptionSize
|
||||
)
|
||||
if let itemView = visibleItem.view {
|
||||
if itemView.superview == nil {
|
||||
self.scrollView.addSubview(itemView)
|
||||
if !transition.animation.isImmediate {
|
||||
transition.animateAlpha(view: itemView, from: 0.0, to: 1.0)
|
||||
}
|
||||
}
|
||||
itemTransition.setFrame(view: itemView, frame: itemFrame)
|
||||
}
|
||||
itemFrame.origin.x += itemFrame.width + optionSpacing
|
||||
if itemFrame.maxX > availableSize.width {
|
||||
itemFrame.origin.x = sideInset
|
||||
itemFrame.origin.y += premiumOptionSize.height + optionSpacing
|
||||
}
|
||||
}
|
||||
|
||||
var removeIds: [AnyHashable] = []
|
||||
for (id, item) in self.premiumItems {
|
||||
if !validIds.contains(id) {
|
||||
removeIds.append(id)
|
||||
if let itemView = item.view {
|
||||
if !transition.animation.isImmediate {
|
||||
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.premiumItems.removeValue(forKey: id)
|
||||
}
|
||||
|
||||
contentHeight += ceil(CGFloat(premiumProducts.count) / 3.0) * premiumOptionSize.height
|
||||
contentHeight += 66.0
|
||||
}
|
||||
|
||||
|
||||
let starsTitleSize = self.starsTitle.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: "Send a Gift", font: Font.bold(28.0), textColor: theme.rootController.navigationBar.primaryTextColor)),
|
||||
horizontalAlignment: .center
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: 100.0)
|
||||
)
|
||||
if let starsTitleView = self.starsTitle.view {
|
||||
if starsTitleView.superview == nil {
|
||||
self.addSubview(starsTitleView)
|
||||
}
|
||||
transition.setBounds(view: starsTitleView, bounds: CGRect(origin: .zero, size: starsTitleSize))
|
||||
}
|
||||
|
||||
let starsDescriptionString = parseMarkdownIntoAttributedString("Give **\(peerName)** gifts that can be kept on the profile or converted to Stars. [What are Stars >]()", attributes: markdownAttributes).mutableCopy() as! NSMutableAttributedString
|
||||
if let range = starsDescriptionString.string.range(of: ">"), let chevronImage = self.chevronImage?.0 {
|
||||
starsDescriptionString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: starsDescriptionString.string))
|
||||
}
|
||||
let starsDescriptionSize = self.starsDescription.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(BalancedTextComponent(
|
||||
text: .plain(starsDescriptionString),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 0,
|
||||
lineSpacing: 0.2,
|
||||
highlightColor: accentColor.withAlphaComponent(0.2),
|
||||
highlightAction: { attributes in
|
||||
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
|
||||
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
},
|
||||
tapAction: { _, _ in
|
||||
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: 1000.0)
|
||||
)
|
||||
let starsDescriptionFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - starsDescriptionSize.width) / 2.0), y: contentHeight), size: starsDescriptionSize)
|
||||
if let starsDescriptionView = self.starsDescription.view {
|
||||
if starsDescriptionView.superview == nil {
|
||||
self.scrollView.addSubview(starsDescriptionView)
|
||||
}
|
||||
transition.setFrame(view: starsDescriptionView, frame: starsDescriptionFrame)
|
||||
}
|
||||
contentHeight += starsDescriptionSize.height
|
||||
contentHeight += 16.0
|
||||
|
||||
let tabSelectorSize = self.tabSelector.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(TabSelectorComponent(
|
||||
context: component.context,
|
||||
colors: TabSelectorComponent.Colors(
|
||||
foreground: theme.list.itemSecondaryTextColor,
|
||||
selection: theme.list.itemSecondaryTextColor.withMultipliedAlpha(0.15),
|
||||
simple: true
|
||||
),
|
||||
items: [
|
||||
TabSelectorComponent.Item(
|
||||
id: AnyHashable(StarsFilter.all.rawValue),
|
||||
title: "All Gifts"
|
||||
),
|
||||
TabSelectorComponent.Item(
|
||||
id: AnyHashable(StarsFilter.limited.rawValue),
|
||||
title: "Limited"
|
||||
),
|
||||
TabSelectorComponent.Item(
|
||||
id: AnyHashable(StarsFilter.stars10.rawValue),
|
||||
title: "⭐️10"
|
||||
),
|
||||
TabSelectorComponent.Item(
|
||||
id: AnyHashable(StarsFilter.stars25.rawValue),
|
||||
title: "⭐️25"
|
||||
),
|
||||
TabSelectorComponent.Item(
|
||||
id: AnyHashable(StarsFilter.stars50.rawValue),
|
||||
title: "⭐️50"
|
||||
),
|
||||
TabSelectorComponent.Item(
|
||||
id: AnyHashable(StarsFilter.stars100.rawValue),
|
||||
title: "⭐️100"
|
||||
)
|
||||
],
|
||||
selectedId: AnyHashable(self.starsFilter.rawValue),
|
||||
setSelectedId: { [weak self] id in
|
||||
guard let self, let idValue = id.base as? Int, let starsFilter = StarsFilter(rawValue: idValue) else {
|
||||
return
|
||||
}
|
||||
if self.starsFilter != starsFilter {
|
||||
self.starsFilter = starsFilter
|
||||
self.state?.updated(transition: .easeInOut(duration: 0.25))
|
||||
}
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - 10.0 * 2.0, height: 50.0)
|
||||
)
|
||||
if let tabSelectorView = self.tabSelector.view {
|
||||
if tabSelectorView.superview == nil {
|
||||
self.scrollView.addSubview(tabSelectorView)
|
||||
}
|
||||
transition.setFrame(view: tabSelectorView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - tabSelectorSize.width) / 2.0), y: contentHeight), size: tabSelectorSize))
|
||||
}
|
||||
contentHeight += tabSelectorSize.height
|
||||
contentHeight += 19.0
|
||||
|
||||
if let starGifts = state.starGifts {
|
||||
self.starsItemsOrigin = contentHeight
|
||||
|
||||
let starsOptionSize = CGSize(width: optionWidth, height: 154.0)
|
||||
contentHeight += ceil(CGFloat(starGifts.count) / 3.0) * starsOptionSize.height
|
||||
contentHeight += 66.0
|
||||
}
|
||||
|
||||
contentHeight += bottomContentInset
|
||||
contentHeight += environment.safeInsets.bottom
|
||||
|
||||
let previousBounds = self.scrollView.bounds
|
||||
|
||||
let contentSize = CGSize(width: availableSize.width, height: contentHeight)
|
||||
if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) {
|
||||
self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize)
|
||||
}
|
||||
if self.scrollView.contentSize != contentSize {
|
||||
self.scrollView.contentSize = contentSize
|
||||
}
|
||||
let scrollInsets = UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: 0.0, right: 0.0)
|
||||
if self.scrollView.scrollIndicatorInsets != scrollInsets {
|
||||
self.scrollView.scrollIndicatorInsets = scrollInsets
|
||||
}
|
||||
|
||||
if !previousBounds.isEmpty, !transition.animation.isImmediate {
|
||||
let bounds = self.scrollView.bounds
|
||||
if bounds.maxY != previousBounds.maxY {
|
||||
let offsetY = previousBounds.maxY - bounds.maxY
|
||||
transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true)
|
||||
}
|
||||
}
|
||||
|
||||
self.topOverscrollLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -3000.0), size: CGSize(width: availableSize.width, height: 3000.0))
|
||||
|
||||
self.updateScrolling(transition: transition)
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View()
|
||||
}
|
||||
|
||||
final class State: ComponentState {
|
||||
private let context: AccountContext
|
||||
private var disposable: Disposable?
|
||||
private var updateDisposable: Disposable?
|
||||
|
||||
fileprivate var peer: EnginePeer?
|
||||
fileprivate var premiumProducts: [PremiumGiftProduct]?
|
||||
fileprivate var starGifts: [StarGift]?
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
peerId: EnginePeer.Id,
|
||||
premiumOptions: [CachedPremiumGiftOption]
|
||||
) {
|
||||
self.context = context
|
||||
|
||||
super.init()
|
||||
|
||||
let availableProducts: Signal<[InAppPurchaseManager.Product], NoError>
|
||||
if let inAppPurchaseManager = context.inAppPurchaseManager {
|
||||
availableProducts = inAppPurchaseManager.availableProducts
|
||||
} else {
|
||||
availableProducts = .single([])
|
||||
}
|
||||
|
||||
self.disposable = combineLatest(
|
||||
queue: Queue.mainQueue(),
|
||||
context.engine.data.get(
|
||||
TelegramEngine.EngineData.Item.Peer.Peer.init(id: peerId)
|
||||
),
|
||||
availableProducts,
|
||||
context.engine.payments.cachedStarGifts()
|
||||
).start(next: { [weak self] peer, availableProducts, starGifts in
|
||||
guard let self, let peer else {
|
||||
return
|
||||
}
|
||||
self.peer = peer
|
||||
|
||||
let shortestOptionPrice: (Int64, NSDecimalNumber)
|
||||
if let product = availableProducts.first(where: { $0.id.hasSuffix(".monthly") }) {
|
||||
shortestOptionPrice = (Int64(Float(product.priceCurrencyAndAmount.amount)), product.priceValue)
|
||||
} else {
|
||||
shortestOptionPrice = (1, NSDecimalNumber(decimal: 1))
|
||||
}
|
||||
|
||||
var premiumProducts: [PremiumGiftProduct] = []
|
||||
for option in premiumOptions {
|
||||
if let product = availableProducts.first(where: { $0.id == option.storeProductId }), !product.isSubscription {
|
||||
let fraction = Float(product.priceCurrencyAndAmount.amount) / Float(option.months) / Float(shortestOptionPrice.0)
|
||||
let discountValue = Int(round((1.0 - fraction) * 20.0) * 5.0)
|
||||
premiumProducts.append(PremiumGiftProduct(giftOption: option, storeProduct: product, discount: discountValue > 0 ? discountValue : nil))
|
||||
}
|
||||
}
|
||||
self.premiumProducts = premiumProducts.sorted(by: { $0.months < $1.months })
|
||||
|
||||
self.starGifts = starGifts
|
||||
|
||||
self.updated()
|
||||
})
|
||||
|
||||
self.updateDisposable = self.context.engine.payments.keepStarGiftsUpdated().start()
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disposable?.dispose()
|
||||
self.updateDisposable?.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
func makeState() -> State {
|
||||
return State(context: self.context, peerId: self.peerId, premiumOptions: self.premiumOptions)
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
public final class GiftOptionsScreen: ViewControllerComponentContainer, GiftOptionsScreenProtocol {
|
||||
private let context: AccountContext
|
||||
|
||||
public init(context: AccountContext, peerId: EnginePeer.Id, premiumOptions: [CachedPremiumGiftOption]) {
|
||||
self.context = context
|
||||
|
||||
super.init(context: context, component: GiftOptionsScreenComponent(
|
||||
context: context,
|
||||
peerId: peerId,
|
||||
premiumOptions: premiumOptions
|
||||
), navigationBarAppearance: .none, theme: .default, updatedPresentationData: nil)
|
||||
|
||||
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.context.sharedContext.currentPresentationData.with { $0 }.strings.Common_Back, style: .plain, target: nil, action: nil)
|
||||
|
||||
|
||||
self.scrollToTop = { [weak self] in
|
||||
guard let self, let componentView = self.node.hostView.componentView as? GiftOptionsScreenComponent.View else {
|
||||
return
|
||||
}
|
||||
componentView.scrollToTop()
|
||||
}
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
private struct PremiumGiftProduct: Equatable {
|
||||
let giftOption: CachedPremiumGiftOption
|
||||
let storeProduct: InAppPurchaseManager.Product
|
||||
let discount: Int?
|
||||
|
||||
var id: String {
|
||||
return self.storeProduct.id
|
||||
}
|
||||
|
||||
var months: Int32 {
|
||||
return self.giftOption.months
|
||||
}
|
||||
|
||||
var price: String {
|
||||
return self.storeProduct.price
|
||||
}
|
||||
|
||||
var pricePerMonth: String {
|
||||
return self.storeProduct.pricePerMonth(Int(self.months))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user