mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-11-25 17:43:18 +00:00
Gifts improvements
This commit is contained in:
parent
04d7a791c9
commit
2c56786809
@ -12941,3 +12941,8 @@ Sorry for the inconvenience.";
|
||||
"VerificationCodes.DescriptionText" = "This chat is used to receive verification codes from third-party services.";
|
||||
|
||||
"Conversation.CodeCopied" = "Code copied to clipboard";
|
||||
|
||||
"Stars.Purchase.StarGiftInfo" = "Buy Stars to send **%@** gifts that can be kept on the profile or converted to Stars.";
|
||||
|
||||
"SharedMedia.GiftCount_1" = "%@ gift";
|
||||
"SharedMedia.GiftCount_any" = "%@ gifts";
|
||||
|
||||
@ -1017,6 +1017,7 @@ public protocol SharedAccountContext: AnyObject {
|
||||
func makeStarsWithdrawalScreen(context: AccountContext, stats: StarsRevenueStats, completion: @escaping (Int64) -> Void) -> ViewController
|
||||
func makeStarsGiftScreen(context: AccountContext, message: EngineMessage) -> ViewController
|
||||
func makeStarsGiveawayBoostScreen(context: AccountContext, peerId: EnginePeer.Id, boost: ChannelBoostersContext.State.Boost) -> ViewController
|
||||
func makeStarsIntroScreen(context: AccountContext) -> ViewController
|
||||
func makeGiftViewScreen(context: AccountContext, message: EngineMessage) -> ViewController
|
||||
|
||||
func makeMiniAppListScreenInitialData(context: AccountContext) -> Signal<MiniAppListScreenInitialData, NoError>
|
||||
|
||||
@ -130,6 +130,7 @@ public enum StarsPurchasePurpose: Equatable {
|
||||
case subscription(peerId: EnginePeer.Id, requiredStars: Int64, renew: Bool)
|
||||
case gift(peerId: EnginePeer.Id)
|
||||
case unlockMedia(requiredStars: Int64)
|
||||
case starGift(peerId: EnginePeer.Id, requiredStars: Int64)
|
||||
}
|
||||
|
||||
public struct PremiumConfiguration {
|
||||
|
||||
@ -238,6 +238,7 @@ private final class ProfileGiftsContextImpl {
|
||||
private let peerId: PeerId
|
||||
|
||||
private let disposable = MetaDisposable()
|
||||
private let actionDisposable = MetaDisposable()
|
||||
|
||||
private var gifts: [ProfileGiftsContext.State.StarGift] = []
|
||||
private var count: Int32?
|
||||
@ -258,6 +259,7 @@ private final class ProfileGiftsContextImpl {
|
||||
|
||||
deinit {
|
||||
self.disposable.dispose()
|
||||
self.actionDisposable.dispose()
|
||||
}
|
||||
|
||||
func loadMore() {
|
||||
@ -315,6 +317,27 @@ private final class ProfileGiftsContextImpl {
|
||||
}
|
||||
}
|
||||
|
||||
func updateStarGiftAddedToProfile(messageId: EngineMessage.Id, added: Bool) {
|
||||
self.actionDisposable.set(
|
||||
_internal_updateStarGiftAddedToProfile(account: self.account, messageId: messageId, added: added).startStrict()
|
||||
)
|
||||
if let index = self.gifts.firstIndex(where: { $0.messageId == messageId }) {
|
||||
self.gifts[index] = self.gifts[index].withSavedToProfile(added)
|
||||
}
|
||||
self.pushState()
|
||||
}
|
||||
|
||||
func convertStarGift(messageId: EngineMessage.Id) {
|
||||
self.actionDisposable.set(
|
||||
_internal_convertStarGift(account: self.account, messageId: messageId).startStrict()
|
||||
)
|
||||
if let count = self.count {
|
||||
self.count = max(0, count - 1)
|
||||
}
|
||||
self.gifts.removeAll(where: { $0.messageId == messageId })
|
||||
self.pushState()
|
||||
}
|
||||
|
||||
private func pushState() {
|
||||
self.stateValue.set(.single(ProfileGiftsContext.State(gifts: self.gifts, count: self.count, dataState: self.dataState)))
|
||||
}
|
||||
@ -332,6 +355,20 @@ public final class ProfileGiftsContext {
|
||||
public let nameHidden: Bool
|
||||
public let savedToProfile: Bool
|
||||
public let convertStars: Int64?
|
||||
|
||||
public func withSavedToProfile(_ savedToProfile: Bool) -> StarGift {
|
||||
return StarGift(
|
||||
gift: self.gift,
|
||||
fromPeer: self.fromPeer,
|
||||
date: self.date,
|
||||
text: self.text,
|
||||
entities: self.entities,
|
||||
messageId: self.messageId,
|
||||
nameHidden: self.nameHidden,
|
||||
savedToProfile: savedToProfile,
|
||||
convertStars: self.convertStars
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public enum DataState: Equatable {
|
||||
@ -373,6 +410,18 @@ public final class ProfileGiftsContext {
|
||||
impl.loadMore()
|
||||
}
|
||||
}
|
||||
|
||||
public func updateStarGiftAddedToProfile(messageId: EngineMessage.Id, added: Bool) {
|
||||
self.impl.with { impl in
|
||||
impl.updateStarGiftAddedToProfile(messageId: messageId, added: added)
|
||||
}
|
||||
}
|
||||
|
||||
public func convertStarGift(messageId: EngineMessage.Id) {
|
||||
self.impl.with { impl in
|
||||
impl.convertStarGift(messageId: messageId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension ProfileGiftsContext.State.StarGift {
|
||||
|
||||
@ -459,6 +459,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/MinimizedContainer",
|
||||
"//submodules/TelegramUI/Components/SpaceWarpView",
|
||||
"//submodules/TelegramUI/Components/MiniAppListScreen",
|
||||
"//submodules/TelegramUI/Components/Stars/StarsIntroScreen",
|
||||
"//submodules/TelegramUI/Components/Gifts/GiftOptionsScreen",
|
||||
] + select({
|
||||
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
|
||||
|
||||
@ -31,13 +31,16 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
private let backgroundMaskNode: ASImageNode
|
||||
private var linkHighlightingNode: LinkHighlightingNode?
|
||||
|
||||
private let mediaBackgroundMaskNode: ASImageNode
|
||||
private var mediaBackgroundContent: WallpaperBubbleBackgroundNode?
|
||||
private let mediaBackgroundNode: NavigationBackgroundNode
|
||||
private let titleNode: TextNode
|
||||
private let subtitleNode: TextNode
|
||||
private let placeholderNode: StickerShimmerEffectNode
|
||||
private let animationNode: AnimatedStickerNode
|
||||
|
||||
private let ribbonBackgroundNode: ASImageNode
|
||||
private let ribbonTextNode: TextNode
|
||||
|
||||
private var shimmerEffectNode: ShimmerEffectForegroundNode?
|
||||
private let buttonNode: HighlightTrackingButtonNode
|
||||
private let buttonStarsNode: PremiumStarsNode
|
||||
@ -79,9 +82,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
|
||||
self.backgroundMaskNode = ASImageNode()
|
||||
|
||||
self.mediaBackgroundNode = NavigationBackgroundNode(color: .clear)
|
||||
self.mediaBackgroundNode.clipsToBounds = true
|
||||
self.mediaBackgroundNode.cornerRadius = 24.0
|
||||
self.mediaBackgroundMaskNode = ASImageNode()
|
||||
|
||||
self.titleNode = TextNode()
|
||||
self.titleNode.isUserInteractionEnabled = false
|
||||
@ -107,19 +108,30 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
self.buttonTitleNode.isUserInteractionEnabled = false
|
||||
self.buttonTitleNode.displaysAsynchronously = false
|
||||
|
||||
self.ribbonBackgroundNode = ASImageNode()
|
||||
self.ribbonBackgroundNode.displaysAsynchronously = false
|
||||
|
||||
self.ribbonTextNode = TextNode()
|
||||
self.ribbonTextNode.isUserInteractionEnabled = false
|
||||
self.ribbonTextNode.displaysAsynchronously = false
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.labelNode)
|
||||
|
||||
self.addSubnode(self.mediaBackgroundNode)
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.subtitleNode)
|
||||
self.addSubnode(self.subtitleNode)
|
||||
self.addSubnode(self.placeholderNode)
|
||||
self.addSubnode(self.animationNode)
|
||||
|
||||
self.addSubnode(self.buttonNode)
|
||||
self.buttonNode.addSubnode(self.buttonStarsNode)
|
||||
self.addSubnode(self.buttonTitleNode)
|
||||
|
||||
self.addSubnode(self.ribbonBackgroundNode)
|
||||
self.addSubnode(self.ribbonTextNode)
|
||||
|
||||
self.buttonNode.highligthedChanged = { [weak self] highlighted in
|
||||
if let strongSelf = self {
|
||||
if highlighted {
|
||||
@ -226,7 +238,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode)
|
||||
let makeButtonTitleLayout = TextNode.asyncLayout(self.buttonTitleNode)
|
||||
|
||||
let makeRibbonTextLayout = TextNode.asyncLayout(self.ribbonTextNode)
|
||||
|
||||
let cachedMaskBackgroundImage = self.cachedMaskBackgroundImage
|
||||
|
||||
return { item, layoutConstants, _, _, _, _ in
|
||||
@ -247,6 +260,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
var title = item.presentationData.strings.Notification_PremiumGift_Title
|
||||
var text = ""
|
||||
var buttonTitle = item.presentationData.strings.Notification_PremiumGift_View
|
||||
var ribbonTitle = ""
|
||||
var hasServiceMessage = true
|
||||
var textSpacing: CGFloat = 0.0
|
||||
for media in item.message.media {
|
||||
@ -315,8 +329,9 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
buttonTitle = item.presentationData.strings.Notification_PremiumPrize_View
|
||||
hasServiceMessage = false
|
||||
}
|
||||
case let .starGift(gift, convertStars, giftText, entities, nameHidden, savedToProfile, converted)://(amount, giftId, nameHidden, limitNumber, limitTotal, giftText, _):
|
||||
case let .starGift(gift, convertStars, giftText, entities, nameHidden, savedToProfile, converted):
|
||||
let _ = nameHidden
|
||||
//TODO:localize
|
||||
let authorName = item.message.author.flatMap { EnginePeer($0) }?.compactDisplayTitle ?? ""
|
||||
title = "Gift from \(authorName)"
|
||||
if let giftText, !giftText.isEmpty {
|
||||
@ -344,6 +359,9 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
}
|
||||
animationFile = gift.file
|
||||
if let availability = gift.availability {
|
||||
ribbonTitle = "1 of \(availability.total)"
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -377,6 +395,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let (buttonTitleLayout, buttonTitleApply) = makeButtonTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: buttonTitle, font: Font.semibold(15.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let (ribbonTextLayout, ribbonTextApply) = makeRibbonTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: ribbonTitle, font: Font.semibold(11.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
giftSize.height = titleLayout.size.height + textSpacing + subtitleLayout.size.height + 212.0
|
||||
|
||||
@ -424,31 +444,10 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
return (backgroundSize.width, { boundingWidth in
|
||||
return (backgroundSize, { [weak self] animation, synchronousLoads, _ in
|
||||
if let strongSelf = self {
|
||||
if strongSelf.item == nil {
|
||||
strongSelf.animationNode.autoplay = true
|
||||
|
||||
if let file = animationFile {
|
||||
strongSelf.animationNode.setup(source: AnimatedStickerResourceSource(account: item.context.account, resource: file.resource), width: 384, height: 384, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
|
||||
if strongSelf.fetchDisposable == nil {
|
||||
strongSelf.fetchDisposable = freeMediaFileResourceInteractiveFetched(postbox: item.context.account.postbox, userLocation: .other, fileReference: .message(message: MessageReference(item.message), media: file), resource: file.resource).start()
|
||||
}
|
||||
} else if animationName.hasPrefix("Gift") {
|
||||
strongSelf.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: animationName), width: 384, height: 384, playbackMode: .still(.end), mode: .direct(cachePathPrefix: nil))
|
||||
}
|
||||
}
|
||||
strongSelf.item = item
|
||||
|
||||
strongSelf.updateVisibility()
|
||||
let overlayColor = item.presentationData.theme.theme.overallDarkAppearance ? UIColor(rgb: 0xffffff, alpha: 0.12) : UIColor(rgb: 0x000000, alpha: 0.12)
|
||||
|
||||
strongSelf.labelNode.isHidden = !hasServiceMessage
|
||||
|
||||
let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - giftSize.width) / 2.0), y: hasServiceMessage ? labelLayout.size.height + 16.0 : 0.0), size: giftSize)
|
||||
let mediaBackgroundFrame = imageFrame.insetBy(dx: -2.0, dy: -2.0)
|
||||
strongSelf.mediaBackgroundNode.frame = mediaBackgroundFrame
|
||||
|
||||
strongSelf.mediaBackgroundNode.updateColor(color: selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), enableBlur: item.controllerInteraction.enableFullTranslucency && dateFillNeedsBlur(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), transition: .immediate)
|
||||
strongSelf.mediaBackgroundNode.update(size: mediaBackgroundFrame.size, transition: .immediate)
|
||||
strongSelf.buttonNode.backgroundColor = item.presentationData.theme.theme.overallDarkAppearance ? UIColor(rgb: 0xffffff, alpha: 0.12) : UIColor(rgb: 0x000000, alpha: 0.12)
|
||||
|
||||
var iconSize = CGSize(width: 160.0, height: 160.0)
|
||||
var iconOffset: CGFloat = 0.0
|
||||
@ -456,13 +455,56 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
iconSize = CGSize(width: 120.0, height: 120.0)
|
||||
iconOffset = 32.0
|
||||
}
|
||||
strongSelf.animationNode.frame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - iconSize.width) / 2.0), y: mediaBackgroundFrame.minY - 16.0 + iconOffset), size: iconSize)
|
||||
let animationFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - iconSize.width) / 2.0), y: mediaBackgroundFrame.minY - 16.0 + iconOffset), size: iconSize)
|
||||
strongSelf.animationNode.frame = animationFrame
|
||||
|
||||
if strongSelf.item == nil {
|
||||
strongSelf.animationNode.started = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
let current = CACurrentMediaTime()
|
||||
if let setupTimestamp = strongSelf.setupTimestamp, current - setupTimestamp > 0.3 {
|
||||
if !strongSelf.placeholderNode.alpha.isZero {
|
||||
strongSelf.animationNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
strongSelf.removePlaceholder(animated: true)
|
||||
}
|
||||
} else {
|
||||
strongSelf.removePlaceholder(animated: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
strongSelf.animationNode.autoplay = true
|
||||
|
||||
if let file = animationFile {
|
||||
strongSelf.animationNode.setup(source: AnimatedStickerResourceSource(account: item.context.account, resource: file.resource), width: 384, height: 384, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
|
||||
if strongSelf.fetchDisposable == nil {
|
||||
strongSelf.fetchDisposable = freeMediaFileResourceInteractiveFetched(postbox: item.context.account.postbox, userLocation: .other, fileReference: .message(message: MessageReference(item.message), media: file), resource: file.resource).start()
|
||||
}
|
||||
|
||||
if let immediateThumbnailData = file.immediateThumbnailData {
|
||||
let shimmeringColor = bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.stickerPlaceholderShimmerColor, wallpaper: item.presentationData.theme.wallpaper)
|
||||
strongSelf.placeholderNode.update(backgroundColor: nil, foregroundColor: overlayColor, shimmeringColor: shimmeringColor, data: immediateThumbnailData, size: animationFrame.size, enableEffect: item.context.sharedContext.energyUsageSettings.fullTranslucency)
|
||||
}
|
||||
} else if animationName.hasPrefix("Gift") {
|
||||
strongSelf.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: animationName), width: 384, height: 384, playbackMode: .still(.end), mode: .direct(cachePathPrefix: nil))
|
||||
}
|
||||
}
|
||||
strongSelf.item = item
|
||||
|
||||
strongSelf.updateVisibility()
|
||||
|
||||
strongSelf.labelNode.isHidden = !hasServiceMessage
|
||||
|
||||
strongSelf.buttonNode.backgroundColor = overlayColor
|
||||
|
||||
strongSelf.animationNode.updateLayout(size: iconSize)
|
||||
strongSelf.placeholderNode.frame = animationFrame
|
||||
|
||||
let _ = labelApply()
|
||||
let _ = titleApply()
|
||||
let _ = subtitleApply()
|
||||
let _ = buttonTitleApply()
|
||||
let _ = ribbonTextApply()
|
||||
|
||||
let labelFrame = CGRect(origin: CGPoint(x: 8.0, y: 2.0), size: labelLayout.size)
|
||||
strongSelf.labelNode.frame = labelFrame
|
||||
@ -479,23 +521,58 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
let buttonSize = CGSize(width: buttonTitleLayout.size.width + 38.0, height: 34.0)
|
||||
strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonSize.width) / 2.0), y: subtitleFrame.maxY + 10.0), size: buttonSize)
|
||||
strongSelf.buttonStarsNode.frame = CGRect(origin: .zero, size: buttonSize)
|
||||
|
||||
if item.controllerInteraction.presentationContext.backgroundNode?.hasExtraBubbleBackground() == true {
|
||||
if strongSelf.mediaBackgroundContent == nil, let backgroundContent = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) {
|
||||
strongSelf.mediaBackgroundNode.isHidden = true
|
||||
backgroundContent.clipsToBounds = true
|
||||
backgroundContent.allowsGroupOpacity = true
|
||||
backgroundContent.cornerRadius = 24.0
|
||||
|
||||
strongSelf.mediaBackgroundContent = backgroundContent
|
||||
strongSelf.insertSubnode(backgroundContent, at: 0)
|
||||
|
||||
if ribbonTextLayout.size.width > 0.0 {
|
||||
if strongSelf.ribbonBackgroundNode.image == nil {
|
||||
let ribbonImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/GiftRibbon"), color: overlayColor)
|
||||
strongSelf.ribbonBackgroundNode.image = ribbonImage
|
||||
}
|
||||
if let ribbonImage = strongSelf.ribbonBackgroundNode.image {
|
||||
let ribbonFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.maxX - ribbonImage.size.width + 2.0, y: mediaBackgroundFrame.minY - 2.0), size: ribbonImage.size)
|
||||
strongSelf.ribbonBackgroundNode.frame = ribbonFrame
|
||||
|
||||
strongSelf.ribbonTextNode.transform = CATransform3DMakeRotation(.pi / 4.0, 0.0, 0.0, 1.0)
|
||||
strongSelf.ribbonTextNode.bounds = CGRect(origin: .zero, size: ribbonTextLayout.size)
|
||||
strongSelf.ribbonTextNode.position = ribbonFrame.center.offsetBy(dx: 7.0, dy: -6.0)
|
||||
}
|
||||
}
|
||||
|
||||
if strongSelf.mediaBackgroundContent == nil, let backgroundContent = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) {
|
||||
backgroundContent.clipsToBounds = true
|
||||
backgroundContent.cornerRadius = 24.0
|
||||
|
||||
strongSelf.mediaBackgroundContent?.frame = mediaBackgroundFrame
|
||||
} else {
|
||||
strongSelf.mediaBackgroundNode.isHidden = false
|
||||
strongSelf.mediaBackgroundContent?.removeFromSupernode()
|
||||
strongSelf.mediaBackgroundContent = nil
|
||||
strongSelf.mediaBackgroundContent = backgroundContent
|
||||
strongSelf.insertSubnode(backgroundContent, at: 0)
|
||||
}
|
||||
|
||||
if let backgroundContent = strongSelf.mediaBackgroundContent {
|
||||
if ribbonTextLayout.size.width > 0.0 {
|
||||
let backgroundMaskFrame = mediaBackgroundFrame.insetBy(dx: -2.0, dy: -2.0)
|
||||
backgroundContent.frame = backgroundMaskFrame
|
||||
backgroundContent.cornerRadius = 0.0
|
||||
|
||||
if strongSelf.mediaBackgroundMaskNode.image?.size != mediaBackgroundFrame.size {
|
||||
strongSelf.mediaBackgroundMaskNode.image = generateImage(backgroundMaskFrame.size, contextGenerator: { size, context in
|
||||
let bounds = CGRect(origin: .zero, size: size)
|
||||
context.clear(bounds)
|
||||
|
||||
context.setFillColor(UIColor.black.cgColor)
|
||||
context.addPath(UIBezierPath(roundedRect: bounds.insetBy(dx: 2.0, dy: 2.0), cornerRadius: 24.0).cgPath)
|
||||
context.fillPath()
|
||||
|
||||
if let ribbonImage = UIImage(bundleImageName: "Chat/Message/GiftRibbon"), let cgImage = ribbonImage.cgImage {
|
||||
context.draw(cgImage, in: CGRect(origin: CGPoint(x: bounds.width - ribbonImage.size.width, y: bounds.height - ribbonImage.size.height), size: ribbonImage.size), byTiling: false)
|
||||
}
|
||||
})
|
||||
}
|
||||
backgroundContent.view.mask = strongSelf.mediaBackgroundMaskNode.view
|
||||
strongSelf.mediaBackgroundMaskNode.frame = CGRect(origin: .zero, size: backgroundMaskFrame.size)
|
||||
} else {
|
||||
backgroundContent.frame = mediaBackgroundFrame
|
||||
backgroundContent.clipsToBounds = true
|
||||
backgroundContent.cornerRadius = 24.0
|
||||
backgroundContent.view.mask = nil
|
||||
}
|
||||
}
|
||||
|
||||
let baseBackgroundFrame = labelFrame.offsetBy(dx: 0.0, dy: -11.0)
|
||||
@ -645,7 +722,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
return ChatMessageBubbleContentTapAction(content: .ignore)
|
||||
} else if let backgroundNode = self.backgroundNode, backgroundNode.frame.contains(point) {
|
||||
return ChatMessageBubbleContentTapAction(content: .openMessage)
|
||||
} else if self.mediaBackgroundNode.frame.contains(point) {
|
||||
} else if self.mediaBackgroundContent?.frame.contains(point) == true {
|
||||
return ChatMessageBubbleContentTapAction(content: .openMessage)
|
||||
} else {
|
||||
return ChatMessageBubbleContentTapAction(content: .none)
|
||||
|
||||
@ -20,35 +20,62 @@ public final class GiftItemComponent: Component {
|
||||
}
|
||||
|
||||
public struct Ribbon: Equatable {
|
||||
public enum Color {
|
||||
case red
|
||||
case blue
|
||||
|
||||
var colors: [UIColor] {
|
||||
switch self {
|
||||
case .red:
|
||||
return [
|
||||
UIColor(rgb: 0xed1c26),
|
||||
UIColor(rgb: 0xff5c55)
|
||||
|
||||
]
|
||||
case .blue:
|
||||
return [
|
||||
UIColor(rgb: 0x34a4fc),
|
||||
UIColor(rgb: 0x6fd3ff)
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
public let text: String
|
||||
public let color: UIColor
|
||||
public let color: Color
|
||||
|
||||
public init(text: String, color: UIColor) {
|
||||
public init(text: String, color: Color) {
|
||||
self.text = text
|
||||
self.color = color
|
||||
}
|
||||
}
|
||||
|
||||
public enum Peer: Equatable {
|
||||
case peer(EnginePeer)
|
||||
case anonymous
|
||||
}
|
||||
|
||||
let context: AccountContext
|
||||
let theme: PresentationTheme
|
||||
let peer: EnginePeer?
|
||||
let subject: Subject
|
||||
let peer: GiftItemComponent.Peer?
|
||||
let subject: GiftItemComponent.Subject
|
||||
let title: String?
|
||||
let subtitle: String?
|
||||
let price: String
|
||||
let ribbon: Ribbon?
|
||||
let isLoading: Bool
|
||||
let isHidden: Bool
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
theme: PresentationTheme,
|
||||
peer: EnginePeer?,
|
||||
subject: Subject,
|
||||
peer: GiftItemComponent.Peer?,
|
||||
subject: GiftItemComponent.Subject,
|
||||
title: String? = nil,
|
||||
subtitle: String? = nil,
|
||||
price: String,
|
||||
ribbon: Ribbon? = nil,
|
||||
isLoading: Bool = false
|
||||
isLoading: Bool = false,
|
||||
isHidden: Bool = false
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
@ -59,6 +86,7 @@ public final class GiftItemComponent: Component {
|
||||
self.price = price
|
||||
self.ribbon = ribbon
|
||||
self.isLoading = isLoading
|
||||
self.isHidden = isHidden
|
||||
}
|
||||
|
||||
public static func ==(lhs: GiftItemComponent, rhs: GiftItemComponent) -> Bool {
|
||||
@ -89,6 +117,9 @@ public final class GiftItemComponent: Component {
|
||||
if lhs.isLoading != rhs.isLoading {
|
||||
return false
|
||||
}
|
||||
if lhs.isHidden != rhs.isHidden {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -108,6 +139,9 @@ public final class GiftItemComponent: Component {
|
||||
|
||||
private var animationLayer: InlineStickerItemLayer?
|
||||
|
||||
private var hiddenIconBackground: UIVisualEffectView?
|
||||
private var hiddenIcon: UIImageView?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
@ -125,6 +159,8 @@ public final class GiftItemComponent: Component {
|
||||
}
|
||||
|
||||
func update(component: GiftItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
let isFirstTime = self.component == nil
|
||||
|
||||
self.component = component
|
||||
self.componentState = state
|
||||
|
||||
@ -201,8 +237,9 @@ public final class GiftItemComponent: Component {
|
||||
self.layer.addSublayer(animationLayer)
|
||||
}
|
||||
|
||||
let animationFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - iconSize.width) / 2.0), y: animationOffset), size: iconSize)
|
||||
if let animationLayer = self.animationLayer {
|
||||
transition.setFrame(layer: animationLayer, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - iconSize.width) / 2.0), y: animationOffset), size: iconSize))
|
||||
transition.setFrame(layer: animationLayer, frame: animationFrame)
|
||||
}
|
||||
|
||||
if let title = component.title {
|
||||
@ -287,7 +324,7 @@ public final class GiftItemComponent: Component {
|
||||
ribbonTextView.bounds = CGRect(origin: .zero, size: ribbonTextSize)
|
||||
|
||||
if self.ribbon.image == nil {
|
||||
self.ribbon.image = generateGradientTintedImage(image: UIImage(bundleImageName: "Premium/GiftRibbon"), colors: [ribbon.color.withMultipliedBrightnessBy(1.1), ribbon.color.withMultipliedBrightnessBy(0.9)], direction: .diagonal)
|
||||
self.ribbon.image = generateGradientTintedImage(image: UIImage(bundleImageName: "Premium/GiftRibbon"), colors: ribbon.color.colors, direction: .diagonal)
|
||||
}
|
||||
if let ribbonImage = self.ribbon.image {
|
||||
self.ribbon.frame = CGRect(origin: CGPoint(x: size.width - ribbonImage.size.width + 2.0, y: -2.0), size: ribbonImage.size)
|
||||
@ -312,13 +349,64 @@ public final class GiftItemComponent: Component {
|
||||
self.avatarNode = avatarNode
|
||||
}
|
||||
|
||||
avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, displayDimensions: CGSize(width: 20.0, height: 20.0))
|
||||
switch peer {
|
||||
case let .peer(peer):
|
||||
avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, displayDimensions: CGSize(width: 20.0, height: 20.0))
|
||||
case .anonymous:
|
||||
avatarNode.setPeer(context: component.context, theme: component.theme, peer: nil, overrideImage: .anonymousSavedMessagesIcon(isColored: true))
|
||||
}
|
||||
|
||||
avatarNode.frame = CGRect(origin: CGPoint(x: 2.0, y: 2.0), size: CGSize(width: 20.0, height: 20.0))
|
||||
}
|
||||
|
||||
self.backgroundLayer.backgroundColor = component.theme.list.itemBlocksBackgroundColor.cgColor
|
||||
transition.setFrame(layer: self.backgroundLayer, frame: CGRect(origin: .zero, size: size))
|
||||
|
||||
if component.isHidden {
|
||||
let hiddenIconBackground: UIVisualEffectView
|
||||
let hiddenIcon: UIImageView
|
||||
if let currentBackground = self.hiddenIconBackground, let currentIcon = self.hiddenIcon {
|
||||
hiddenIconBackground = currentBackground
|
||||
hiddenIcon = currentIcon
|
||||
} else {
|
||||
let blurEffect: UIBlurEffect
|
||||
if #available(iOS 13.0, *) {
|
||||
blurEffect = UIBlurEffect(style: .systemThinMaterialDark)
|
||||
} else {
|
||||
blurEffect = UIBlurEffect(style: .dark)
|
||||
}
|
||||
hiddenIconBackground = UIVisualEffectView(effect: blurEffect)
|
||||
hiddenIconBackground.clipsToBounds = true
|
||||
hiddenIconBackground.layer.cornerRadius = 15.0
|
||||
self.hiddenIconBackground = hiddenIconBackground
|
||||
|
||||
hiddenIcon = UIImageView(image: generateTintedImage(image: UIImage(bundleImageName: "Peer Info/HiddenIcon"), color: .white))
|
||||
self.hiddenIcon = hiddenIcon
|
||||
|
||||
self.addSubview(hiddenIconBackground)
|
||||
hiddenIconBackground.contentView.addSubview(hiddenIcon)
|
||||
|
||||
if !isFirstTime {
|
||||
hiddenIconBackground.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2)
|
||||
hiddenIconBackground.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
let iconSize = CGSize(width: 30.0, height: 30.0)
|
||||
hiddenIconBackground.frame = iconSize.centered(around: animationFrame.center)
|
||||
hiddenIcon.frame = CGRect(origin: .zero, size: iconSize)
|
||||
} else {
|
||||
if let hiddenIconBackground = self.hiddenIconBackground {
|
||||
self.hiddenIconBackground = nil
|
||||
self.hiddenIcon = nil
|
||||
|
||||
hiddenIconBackground.layer.animateAlpha(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false, completion: { _ in
|
||||
hiddenIconBackground.removeFromSuperview()
|
||||
})
|
||||
hiddenIconBackground.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
|
||||
}
|
||||
}
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,15 +30,18 @@ final class GiftOptionsScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
let context: AccountContext
|
||||
let starsContext: StarsContext
|
||||
let peerId: EnginePeer.Id
|
||||
let premiumOptions: [CachedPremiumGiftOption]
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
starsContext: StarsContext,
|
||||
peerId: EnginePeer.Id,
|
||||
premiumOptions: [CachedPremiumGiftOption]
|
||||
) {
|
||||
self.context = context
|
||||
self.starsContext = starsContext
|
||||
self.peerId = peerId
|
||||
self.premiumOptions = premiumOptions
|
||||
}
|
||||
@ -100,10 +103,15 @@ final class GiftOptionsScreenComponent: Component {
|
||||
|
||||
private let header = ComponentView<Empty>()
|
||||
|
||||
private let balanceTitle = ComponentView<Empty>()
|
||||
private let balanceValue = ComponentView<Empty>()
|
||||
private let balanceIcon = ComponentView<Empty>()
|
||||
|
||||
private let premiumTitle = ComponentView<Empty>()
|
||||
private let premiumDescription = ComponentView<Empty>()
|
||||
private var premiumItems: [AnyHashable: ComponentView<Empty>] = [:]
|
||||
private var selectedPremiumGift: String?
|
||||
private var inProgressPremiumGift: String?
|
||||
private let purchaseDisposable = MetaDisposable()
|
||||
|
||||
private let starsTitle = ComponentView<Empty>()
|
||||
private let starsDescription = ComponentView<Empty>()
|
||||
@ -113,6 +121,9 @@ final class GiftOptionsScreenComponent: Component {
|
||||
|
||||
private var isUpdating: Bool = false
|
||||
|
||||
private var starsStateDisposable: Disposable?
|
||||
private var starsState: StarsContext.State?
|
||||
|
||||
private var component: GiftOptionsScreenComponent?
|
||||
private(set) weak var state: State?
|
||||
private var environment: EnvironmentType?
|
||||
@ -147,6 +158,8 @@ final class GiftOptionsScreenComponent: Component {
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.starsStateDisposable?.dispose()
|
||||
self.purchaseDisposable.dispose()
|
||||
}
|
||||
|
||||
func scrollToTop() {
|
||||
@ -205,7 +218,6 @@ final class GiftOptionsScreenComponent: Component {
|
||||
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)
|
||||
@ -273,7 +285,7 @@ final class GiftOptionsScreenComponent: Component {
|
||||
ribbon: gift.availability != nil ?
|
||||
GiftItemComponent.Ribbon(
|
||||
text: "Limited",
|
||||
color: UIColor(rgb: 0x58c1fe)
|
||||
color: .blue
|
||||
)
|
||||
: nil
|
||||
)
|
||||
@ -330,6 +342,88 @@ final class GiftOptionsScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
private func buyPremium(_ product: PremiumGiftProduct) {
|
||||
guard let component = self.component, let inAppPurchaseManager = self.component?.context.inAppPurchaseManager, self.inProgressPremiumGift == nil else {
|
||||
return
|
||||
}
|
||||
|
||||
self.inProgressPremiumGift = product.id
|
||||
self.state?.updated()
|
||||
|
||||
let (currency, amount) = product.storeProduct.priceCurrencyAndAmount
|
||||
|
||||
addAppLogEvent(postbox: component.context.account.postbox, type: "premium_gift.promo_screen_accept")
|
||||
|
||||
let purpose: AppStoreTransactionPurpose = .giftCode(peerIds: [component.peerId], boostPeer: nil, currency: currency, amount: amount)
|
||||
let quantity: Int32 = 1
|
||||
|
||||
let _ = (component.context.engine.payments.canPurchasePremium(purpose: purpose)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] available in
|
||||
if let strongSelf = self {
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||
if available {
|
||||
strongSelf.purchaseDisposable.set((inAppPurchaseManager.buyProduct(product.storeProduct, quantity: quantity, purpose: purpose)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] status in
|
||||
guard let self, case .purchased = status, let controller = self.environment?.controller(), let navigationController = controller.navigationController as? NavigationController else {
|
||||
return
|
||||
}
|
||||
|
||||
var controllers = navigationController.viewControllers
|
||||
controllers = controllers.filter { !($0 is GiftOptionsScreen) }
|
||||
var foundController = false
|
||||
for controller in controllers.reversed() {
|
||||
if let chatController = controller as? ChatController, case .peer(id: component.peerId) = chatController.chatLocation {
|
||||
chatController.hintPlayNextOutgoingGift()
|
||||
foundController = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundController {
|
||||
let chatController = component.context.sharedContext.makeChatController(context: component.context, chatLocation: .peer(id: component.peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil)
|
||||
chatController.hintPlayNextOutgoingGift()
|
||||
controllers.append(chatController)
|
||||
}
|
||||
navigationController.setViewControllers(controllers, animated: true)
|
||||
}, error: { [weak self] error in
|
||||
guard let self, let controller = self.environment?.controller() else {
|
||||
return
|
||||
}
|
||||
self.inProgressPremiumGift = nil
|
||||
self.state?.updated(transition: .immediate)
|
||||
|
||||
var errorText: String?
|
||||
switch error {
|
||||
case .generic:
|
||||
errorText = presentationData.strings.Premium_Purchase_ErrorUnknown
|
||||
case .network:
|
||||
errorText = presentationData.strings.Premium_Purchase_ErrorNetwork
|
||||
case .notAllowed:
|
||||
errorText = presentationData.strings.Premium_Purchase_ErrorNotAllowed
|
||||
case .cantMakePayments:
|
||||
errorText = presentationData.strings.Premium_Purchase_ErrorCantMakePayments
|
||||
case .assignFailed:
|
||||
errorText = presentationData.strings.Premium_Purchase_ErrorUnknown
|
||||
case .tryLater:
|
||||
errorText = presentationData.strings.Premium_Purchase_ErrorUnknown
|
||||
case .cancelled:
|
||||
break
|
||||
}
|
||||
|
||||
if let errorText {
|
||||
addAppLogEvent(postbox: component.context.account.postbox, type: "premium_gift.promo_screen_fail")
|
||||
|
||||
let alertController = textAlertController(context: component.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])
|
||||
controller.present(alertController, in: .window(.root))
|
||||
}
|
||||
}))
|
||||
} else {
|
||||
self?.inProgressPremiumGift = nil
|
||||
self?.state?.updated(transition: .immediate)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func update(component: GiftOptionsScreenComponent, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
|
||||
self.isUpdating = true
|
||||
defer {
|
||||
@ -340,13 +434,21 @@ final class GiftOptionsScreenComponent: Component {
|
||||
let controller = environment.controller
|
||||
let themeUpdated = self.environment?.theme !== environment.theme
|
||||
self.environment = environment
|
||||
self.state = state
|
||||
|
||||
if self.component == nil {
|
||||
|
||||
self.starsStateDisposable = (component.starsContext.state
|
||||
|> deliverOnMainQueue).start(next: { [weak self] state in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.starsState = state
|
||||
if !self.isUpdating {
|
||||
self.state?.updated()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
if themeUpdated {
|
||||
self.backgroundColor = environment.theme.list.blocksBackgroundColor
|
||||
@ -451,6 +553,55 @@ final class GiftOptionsScreenComponent: Component {
|
||||
transition.setFrame(view: cancelButtonView, frame: cancelButtonFrame)
|
||||
}
|
||||
|
||||
let balanceTitleSize = self.balanceTitle.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: strings.Stars_Purchase_Balance,
|
||||
font: Font.regular(14.0),
|
||||
textColor: environment.theme.actionSheet.primaryTextColor
|
||||
)),
|
||||
maximumNumberOfLines: 1
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
let balanceValueSize = self.balanceValue.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: presentationStringsFormattedNumber(Int32(self.starsState?.balance ?? 0), environment.dateTimeFormat.groupingSeparator),
|
||||
font: Font.semibold(14.0),
|
||||
textColor: environment.theme.actionSheet.primaryTextColor
|
||||
)),
|
||||
maximumNumberOfLines: 1
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
let balanceIconSize = self.balanceIcon.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(BundleIconComponent(name: "Premium/Stars/StarSmall", tintColor: nil)),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
|
||||
if let balanceTitleView = self.balanceTitle.view, let balanceValueView = self.balanceValue.view, let balanceIconView = self.balanceIcon.view {
|
||||
if balanceTitleView.superview == nil {
|
||||
self.addSubview(balanceTitleView)
|
||||
self.addSubview(balanceValueView)
|
||||
self.addSubview(balanceIconView)
|
||||
}
|
||||
let navigationHeight = environment.navigationHeight - environment.statusBarHeight
|
||||
let topBalanceOriginY = environment.statusBarHeight + (navigationHeight - balanceTitleSize.height - balanceValueSize.height) / 2.0
|
||||
balanceTitleView.center = CGPoint(x: availableSize.width - 16.0 - environment.safeInsets.right - balanceTitleSize.width / 2.0, y: topBalanceOriginY + balanceTitleSize.height / 2.0)
|
||||
balanceTitleView.bounds = CGRect(origin: .zero, size: balanceTitleSize)
|
||||
balanceValueView.center = CGPoint(x: availableSize.width - 16.0 - environment.safeInsets.right - balanceValueSize.width / 2.0, y: topBalanceOriginY + balanceTitleSize.height + balanceValueSize.height / 2.0)
|
||||
balanceValueView.bounds = CGRect(origin: .zero, size: balanceValueSize)
|
||||
balanceIconView.center = CGPoint(x: availableSize.width - 16.0 - environment.safeInsets.right - balanceValueSize.width - balanceIconSize.width / 2.0 - 2.0, y: topBalanceOriginY + balanceTitleSize.height + balanceValueSize.height / 2.0 - UIScreenPixel)
|
||||
balanceIconView.bounds = CGRect(origin: .zero, size: balanceIconSize)
|
||||
}
|
||||
|
||||
let premiumTitleSize = self.premiumTitle.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
@ -494,8 +645,13 @@ final class GiftOptionsScreenComponent: Component {
|
||||
return nil
|
||||
}
|
||||
},
|
||||
tapAction: { _, _ in
|
||||
|
||||
tapAction: { [weak self] _, _ in
|
||||
guard let self, let component = self.component, let environment = self.environment else {
|
||||
return
|
||||
}
|
||||
let introController = component.context.sharedContext.makePremiumIntroController(context: component.context, source: .settings, forceDark: false, dismissed: nil)
|
||||
introController.navigationPresentation = .modal
|
||||
environment.controller()?.push(introController)
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
@ -561,21 +717,15 @@ final class GiftOptionsScreenComponent: Component {
|
||||
ribbon: product.discount.flatMap {
|
||||
GiftItemComponent.Ribbon(
|
||||
text: "-\($0)%",
|
||||
color: UIColor(rgb: 0xfa4846)
|
||||
color: .red
|
||||
)
|
||||
},
|
||||
isLoading: self.selectedPremiumGift == product.id
|
||||
isLoading: self.inProgressPremiumGift == 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()
|
||||
})
|
||||
self?.buyPremium(product)
|
||||
},
|
||||
animateAlpha: false
|
||||
)
|
||||
@ -658,8 +808,13 @@ final class GiftOptionsScreenComponent: Component {
|
||||
return nil
|
||||
}
|
||||
},
|
||||
tapAction: { _, _ in
|
||||
|
||||
tapAction: { [weak self] _, _ in
|
||||
guard let self, let component = self.component, let environment = self.environment else {
|
||||
return
|
||||
}
|
||||
let introController = component.context.sharedContext.makeStarsIntroScreen(context: component.context)
|
||||
introController.navigationPresentation = .modal
|
||||
environment.controller()?.push(introController)
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
@ -859,11 +1014,17 @@ final class GiftOptionsScreenComponent: Component {
|
||||
public final class GiftOptionsScreen: ViewControllerComponentContainer, GiftOptionsScreenProtocol {
|
||||
private let context: AccountContext
|
||||
|
||||
public init(context: AccountContext, peerId: EnginePeer.Id, premiumOptions: [CachedPremiumGiftOption]) {
|
||||
public init(
|
||||
context: AccountContext,
|
||||
starsContext: StarsContext,
|
||||
peerId: EnginePeer.Id,
|
||||
premiumOptions: [CachedPremiumGiftOption]
|
||||
) {
|
||||
self.context = context
|
||||
|
||||
super.init(context: context, component: GiftOptionsScreenComponent(
|
||||
context: context,
|
||||
starsContext: starsContext,
|
||||
peerId: peerId,
|
||||
premiumOptions: premiumOptions
|
||||
), navigationBarAppearance: .none, theme: .default, updatedPresentationData: nil)
|
||||
|
||||
@ -148,6 +148,8 @@ final class ChatGiftPreviewItemNode: ListViewItemNode {
|
||||
|
||||
private let disposable = MetaDisposable()
|
||||
|
||||
private var initialBubbleHeight: CGFloat?
|
||||
|
||||
init() {
|
||||
self.topStripeNode = ASDisplayNode()
|
||||
self.topStripeNode.isLayerBacked = true
|
||||
@ -235,14 +237,13 @@ final class ChatGiftPreviewItemNode: ListViewItemNode {
|
||||
})
|
||||
itemNode!.isUserInteractionEnabled = false
|
||||
messageNodes.append(itemNode!)
|
||||
|
||||
self.initialBubbleHeight = itemNode?.frame.height
|
||||
}
|
||||
nodes = messageNodes
|
||||
}
|
||||
|
||||
var contentSize = CGSize(width: params.width, height: 4.0 + 4.0)
|
||||
// for node in nodes {
|
||||
// contentSize.height += node.frame.size.height
|
||||
// }
|
||||
contentSize.height = 346.0
|
||||
insets = itemListNeighborsGroupedInsets(neighbors, params)
|
||||
if params.width <= 320.0 {
|
||||
@ -269,7 +270,13 @@ final class ChatGiftPreviewItemNode: ListViewItemNode {
|
||||
if node.supernode == nil {
|
||||
strongSelf.containerNode.addSubnode(node)
|
||||
}
|
||||
node.updateFrame(CGRect(origin: CGPoint(x: 0.0, y: floor((contentSize.height - node.frame.size.height) / 2.0)), size: node.frame.size), within: layoutSize)
|
||||
let bubbleHeight: CGFloat
|
||||
if let initialBubbleHeight = strongSelf.initialBubbleHeight {
|
||||
bubbleHeight = max(node.frame.height, initialBubbleHeight)
|
||||
} else {
|
||||
bubbleHeight = node.frame.height
|
||||
}
|
||||
node.updateFrame(CGRect(origin: CGPoint(x: 0.0, y: floor((contentSize.height - bubbleHeight) / 2.0)), size: node.frame.size), within: layoutSize)
|
||||
//topOffset += node.frame.size.height
|
||||
}
|
||||
|
||||
|
||||
@ -90,6 +90,14 @@ final class GiftSetupScreenComponent: Component {
|
||||
|
||||
private var starImage: (UIImage, PresentationTheme)?
|
||||
|
||||
private var optionsDisposable: Disposable?
|
||||
private(set) var options: [StarsTopUpOption] = [] {
|
||||
didSet {
|
||||
self.optionsPromise.set(self.options)
|
||||
}
|
||||
}
|
||||
private let optionsPromise = ValuePromise<[StarsTopUpOption]?>(nil)
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.scrollView = ScrollView()
|
||||
self.scrollView.showsVerticalScrollIndicator = true
|
||||
@ -159,45 +167,77 @@ final class GiftSetupScreenComponent: Component {
|
||||
}
|
||||
|
||||
func proceed() {
|
||||
guard let component = self.component else {
|
||||
guard let component = self.component, let starsContext = component.context.starsContext, let starsState = starsContext.currentState else {
|
||||
return
|
||||
}
|
||||
let source: BotPaymentInvoiceSource = .starGift(hideName: self.hideName, peerId: component.peerId, giftId: component.gift.id, text: self.textInputState.text.string, entities: [])
|
||||
let inputData = BotCheckoutController.InputData.fetch(context: component.context, source: source)
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<BotCheckoutController.InputData?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|
||||
let _ = (inputData
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak self] inputData in
|
||||
guard let inputData else {
|
||||
let proceed = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let _ = (component.context.engine.payments.sendStarsPaymentForm(formId: inputData.form.id, source: source)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
guard let self, let controller = self.environment?.controller(), let navigationController = controller.navigationController as? NavigationController else {
|
||||
let source: BotPaymentInvoiceSource = .starGift(hideName: self.hideName, peerId: component.peerId, giftId: component.gift.id, text: self.textInputState.text.string, entities: [])
|
||||
let inputData = BotCheckoutController.InputData.fetch(context: component.context, source: source)
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<BotCheckoutController.InputData?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|
||||
let _ = (inputData
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak self] inputData in
|
||||
guard let inputData else {
|
||||
return
|
||||
}
|
||||
|
||||
var controllers = navigationController.viewControllers
|
||||
controllers = controllers.filter { !($0 is GiftSetupScreen) && !($0 is GiftOptionsScreenProtocol) }
|
||||
var foundController = false
|
||||
for controller in controllers.reversed() {
|
||||
if let chatController = controller as? ChatController, case .peer(id: component.peerId) = chatController.chatLocation {
|
||||
chatController.hintPlayNextOutgoingGift()
|
||||
foundController = true
|
||||
break
|
||||
let _ = (component.context.engine.payments.sendStarsPaymentForm(formId: inputData.form.id, source: source)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
guard let self, let controller = self.environment?.controller(), let navigationController = controller.navigationController as? NavigationController else {
|
||||
return
|
||||
}
|
||||
}
|
||||
if !foundController {
|
||||
let chatController = component.context.sharedContext.makeChatController(context: component.context, chatLocation: .peer(id: component.peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil)
|
||||
chatController.hintPlayNextOutgoingGift()
|
||||
controllers.append(chatController)
|
||||
}
|
||||
navigationController.setViewControllers(controllers, animated: true)
|
||||
|
||||
var controllers = navigationController.viewControllers
|
||||
controllers = controllers.filter { !($0 is GiftSetupScreen) && !($0 is GiftOptionsScreenProtocol) }
|
||||
var foundController = false
|
||||
for controller in controllers.reversed() {
|
||||
if let chatController = controller as? ChatController, case .peer(id: component.peerId) = chatController.chatLocation {
|
||||
chatController.hintPlayNextOutgoingGift()
|
||||
foundController = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundController {
|
||||
let chatController = component.context.sharedContext.makeChatController(context: component.context, chatLocation: .peer(id: component.peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil)
|
||||
chatController.hintPlayNextOutgoingGift()
|
||||
controllers.append(chatController)
|
||||
}
|
||||
navigationController.setViewControllers(controllers, animated: true)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if starsState.balance < component.gift.price {
|
||||
let _ = (self.optionsPromise.get()
|
||||
|> filter { $0 != nil }
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak self] options in
|
||||
guard let self, let component = self.component, let controller = self.environment?.controller() else {
|
||||
return
|
||||
}
|
||||
let purchaseController = component.context.sharedContext.makeStarsPurchaseScreen(
|
||||
context: component.context,
|
||||
starsContext: starsContext,
|
||||
options: options ?? [],
|
||||
purpose: .starGift(peerId: component.peerId, requiredStars: component.gift.price),
|
||||
completion: { [weak starsContext] stars in
|
||||
starsContext?.add(balance: stars)
|
||||
Queue.mainQueue().after(0.1) {
|
||||
proceed()
|
||||
}
|
||||
}
|
||||
)
|
||||
controller.push(purchaseController)
|
||||
})
|
||||
} else {
|
||||
proceed()
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: GiftSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
|
||||
|
||||
@ -25,6 +25,7 @@ swift_library(
|
||||
"//submodules/Components/ViewControllerComponent",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/Components/MultilineTextWithEntitiesComponent",
|
||||
"//submodules/Components/BalancedTextComponent",
|
||||
"//submodules/TelegramUI/Components/ListSectionComponent",
|
||||
"//submodules/TelegramUI/Components/ListActionItemComponent",
|
||||
|
||||
@ -12,6 +12,7 @@ import ComponentFlow
|
||||
import ViewControllerComponent
|
||||
import SheetComponent
|
||||
import MultilineTextComponent
|
||||
import MultilineTextWithEntitiesComponent
|
||||
import BundleIconComponent
|
||||
import SolidRoundedButtonComponent
|
||||
import Markdown
|
||||
@ -32,6 +33,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
let openPeer: (EnginePeer) -> Void
|
||||
let updateSavedToProfile: (Bool) -> Void
|
||||
let convertToStars: () -> Void
|
||||
let openStarsIntro: () -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
@ -39,7 +41,8 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
cancel: @escaping (Bool) -> Void,
|
||||
openPeer: @escaping (EnginePeer) -> Void,
|
||||
updateSavedToProfile: @escaping (Bool) -> Void,
|
||||
convertToStars: @escaping () -> Void
|
||||
convertToStars: @escaping () -> Void,
|
||||
openStarsIntro: @escaping () -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.subject = subject
|
||||
@ -47,6 +50,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
self.openPeer = openPeer
|
||||
self.updateSavedToProfile = updateSavedToProfile
|
||||
self.convertToStars = convertToStars
|
||||
self.openStarsIntro = openStarsIntro
|
||||
}
|
||||
|
||||
static func ==(lhs: GiftViewSheetContent, rhs: GiftViewSheetContent) -> Bool {
|
||||
@ -176,7 +180,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
limitNumber = arguments.gift.availability?.remains
|
||||
limitTotal = arguments.gift.availability?.total
|
||||
convertStars = arguments.convertStars
|
||||
incoming = arguments.incoming
|
||||
incoming = arguments.incoming || arguments.peerId == component.context.account.peerId
|
||||
savedToProfile = arguments.savedToProfile
|
||||
converted = arguments.converted
|
||||
} else {
|
||||
@ -259,7 +263,13 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
)
|
||||
|
||||
let tableFont = Font.regular(15.0)
|
||||
let tableBoldFont = Font.semibold(15.0)
|
||||
let tableItalicFont = Font.italic(15.0)
|
||||
let tableBoldItalicFont = Font.semiboldItalic(15.0)
|
||||
let tableMonospaceFont = Font.monospace(15.0)
|
||||
|
||||
let tableTextColor = theme.list.itemPrimaryTextColor
|
||||
let tableLinkColor = theme.list.itemAccentColor
|
||||
var tableItems: [TableComponent.Item] = []
|
||||
|
||||
if let peerId = component.subject.arguments?.peerId, let peer = state.peerMap[peerId] {
|
||||
@ -291,6 +301,18 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
)
|
||||
)
|
||||
))
|
||||
} else {
|
||||
tableItems.append(.init(
|
||||
id: "from",
|
||||
title: strings.Stars_Transaction_From,
|
||||
component: AnyComponent(
|
||||
PeerCellComponent(
|
||||
context: component.context,
|
||||
theme: theme,
|
||||
peer: nil
|
||||
)
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
tableItems.append(.init(
|
||||
@ -312,11 +334,19 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
}
|
||||
|
||||
if let text {
|
||||
let attributedText = stringWithAppliedEntities(text, entities: entities ?? [], baseColor: tableTextColor, linkColor: tableLinkColor, baseFont: tableFont, linkFont: tableFont, boldFont: tableBoldFont, italicFont: tableItalicFont, boldItalicFont: tableBoldItalicFont, fixedFont: tableMonospaceFont, blockQuoteFont: tableFont, message: nil)
|
||||
|
||||
tableItems.append(.init(
|
||||
id: "text",
|
||||
title: nil,
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(text: .plain(NSAttributedString(string: text, font: tableFont, textColor: tableTextColor)))
|
||||
MultilineTextWithEntitiesComponent(
|
||||
context: component.context,
|
||||
animationCache: component.context.animationCache,
|
||||
animationRenderer: component.context.animationRenderer,
|
||||
placeholderColor: theme.list.mediaPlaceholderColor,
|
||||
text: .plain(attributedText)
|
||||
)
|
||||
)
|
||||
))
|
||||
}
|
||||
@ -331,40 +361,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
)
|
||||
|
||||
let textFont = Font.regular(15.0)
|
||||
// let boldTextFont = Font.semibold(15.0)
|
||||
// let textColor = theme.actionSheet.secondaryTextColor
|
||||
let linkColor = theme.actionSheet.controlAccentColor
|
||||
// let destructiveColor = theme.actionSheet.destructiveActionTextColor
|
||||
// let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in
|
||||
// return (TelegramTextAttributes.URL, contents)
|
||||
// })
|
||||
// let additional = additional.update(
|
||||
// component: BalancedTextComponent(
|
||||
// text: .markdown(text: additionalText, attributes: markdownAttributes),
|
||||
// horizontalAlignment: .center,
|
||||
// maximumNumberOfLines: 0,
|
||||
// lineSpacing: 0.2,
|
||||
// highlightColor: linkColor.withAlphaComponent(0.2),
|
||||
// highlightAction: { attributes in
|
||||
// if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
|
||||
// return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
|
||||
// } else {
|
||||
// return nil
|
||||
// }
|
||||
// },
|
||||
// tapAction: { attributes, _ in
|
||||
// if let controller = controller() as? GiftViewScreen, let navigationController = controller.navigationController as? NavigationController {
|
||||
// let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||
// component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: strings.Stars_Transaction_Terms_URL, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {})
|
||||
// component.cancel(true)
|
||||
// }
|
||||
// }
|
||||
// ),
|
||||
// availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height),
|
||||
// transition: .immediate
|
||||
// )
|
||||
|
||||
|
||||
|
||||
context.add(title
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: 177.0))
|
||||
@ -417,7 +414,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
}
|
||||
},
|
||||
tapAction: { _, _ in
|
||||
|
||||
component.openStarsIntro()
|
||||
}
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude),
|
||||
@ -467,31 +464,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY + table.size.height / 2.0))
|
||||
)
|
||||
originY += table.size.height + 23.0
|
||||
|
||||
// context.add(additional
|
||||
// .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + additional.size.height / 2.0))
|
||||
// )
|
||||
// originY += additional.size.height + 23.0
|
||||
|
||||
// if let statusText {
|
||||
// originY += 7.0
|
||||
// let status = status.update(
|
||||
// component: BalancedTextComponent(
|
||||
// text: .plain(NSAttributedString(string: statusText, font: textFont, textColor: statusIsDestructive ? destructiveColor : textColor)),
|
||||
// horizontalAlignment: .center,
|
||||
// maximumNumberOfLines: 0,
|
||||
// lineSpacing: 0.1
|
||||
// ),
|
||||
// availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height),
|
||||
// transition: .immediate
|
||||
// )
|
||||
// context.add(status
|
||||
// .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + status.size.height / 2.0))
|
||||
// )
|
||||
// originY += status.size.height + (statusIsDestructive ? 23.0 : 13.0)
|
||||
// }
|
||||
|
||||
|
||||
|
||||
if incoming && !converted {
|
||||
let button = button.update(
|
||||
component: SolidRoundedButtonComponent(
|
||||
@ -545,6 +518,33 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
.position(CGPoint(x: secondaryButtonFrame.midX, y: secondaryButtonFrame.midY))
|
||||
)
|
||||
originY += secondaryButton.size.height
|
||||
} else {
|
||||
let button = button.update(
|
||||
component: SolidRoundedButtonComponent(
|
||||
title: strings.Common_OK,
|
||||
theme: SolidRoundedButtonComponent.Theme(theme: theme),
|
||||
font: .bold,
|
||||
fontSize: 17.0,
|
||||
height: 50.0,
|
||||
cornerRadius: 10.0,
|
||||
gloss: false,
|
||||
iconName: nil,
|
||||
animationName: nil,
|
||||
iconPosition: .left,
|
||||
isLoading: state.inProgress,
|
||||
action: {
|
||||
component.cancel(true)
|
||||
}
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0),
|
||||
transition: context.transition
|
||||
)
|
||||
let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: originY), size: button.size)
|
||||
context.add(button
|
||||
.position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY))
|
||||
)
|
||||
originY += button.size.height
|
||||
originY += 7.0
|
||||
}
|
||||
|
||||
context.add(closeButton
|
||||
@ -566,19 +566,22 @@ private final class GiftViewSheetComponent: CombinedComponent {
|
||||
let openPeer: (EnginePeer) -> Void
|
||||
let updateSavedToProfile: (Bool) -> Void
|
||||
let convertToStars: () -> Void
|
||||
let openStarsIntro: () -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
subject: GiftViewScreen.Subject,
|
||||
openPeer: @escaping (EnginePeer) -> Void,
|
||||
updateSavedToProfile: @escaping (Bool) -> Void,
|
||||
convertToStars: @escaping () -> Void
|
||||
convertToStars: @escaping () -> Void,
|
||||
openStarsIntro: @escaping () -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.subject = subject
|
||||
self.openPeer = openPeer
|
||||
self.updateSavedToProfile = updateSavedToProfile
|
||||
self.convertToStars = convertToStars
|
||||
self.openStarsIntro = openStarsIntro
|
||||
}
|
||||
|
||||
static func ==(lhs: GiftViewSheetComponent, rhs: GiftViewSheetComponent) -> Bool {
|
||||
@ -620,7 +623,8 @@ private final class GiftViewSheetComponent: CombinedComponent {
|
||||
},
|
||||
openPeer: context.component.openPeer,
|
||||
updateSavedToProfile: context.component.updateSavedToProfile,
|
||||
convertToStars: context.component.convertToStars
|
||||
convertToStars: context.component.convertToStars,
|
||||
openStarsIntro: context.component.openStarsIntro
|
||||
)),
|
||||
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
|
||||
followContentSizeChanges: true,
|
||||
@ -698,7 +702,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
return (message.id.peerId, message.id, message.flags.contains(.Incoming), gift, convertStars, text, entities, nameHidden, savedToProfile, converted)
|
||||
}
|
||||
case let .profileGift(peerId, gift):
|
||||
return (peerId, gift.messageId, false, gift.gift, gift.convertStars ?? 0, gift.text, gift.entities, gift.nameHidden, true, false)
|
||||
return (peerId, gift.messageId, false, gift.gift, gift.convertStars ?? 0, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, false)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -712,13 +716,17 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
public init(
|
||||
context: AccountContext,
|
||||
subject: GiftViewScreen.Subject,
|
||||
forceDark: Bool = false
|
||||
forceDark: Bool = false,
|
||||
updateSavedToProfile: ((Bool) -> Void)? = nil,
|
||||
convertToStars: (() -> Void)? = nil
|
||||
) {
|
||||
self.context = context
|
||||
|
||||
var openPeerImpl: ((EnginePeer) -> Void)?
|
||||
var updateSavedToProfileImpl: ((Bool) -> Void)?
|
||||
var convertToStarsImpl: (() -> Void)?
|
||||
var openStarsIntroImpl: (() -> Void)?
|
||||
|
||||
super.init(
|
||||
context: context,
|
||||
component: GiftViewSheetComponent(
|
||||
@ -732,6 +740,9 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
},
|
||||
convertToStars: {
|
||||
convertToStarsImpl?()
|
||||
},
|
||||
openStarsIntro: {
|
||||
openStarsIntroImpl?()
|
||||
}
|
||||
),
|
||||
navigationBarAppearance: .none,
|
||||
@ -764,8 +775,12 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
guard let self, let arguments = subject.arguments, let messageId = arguments.messageId else {
|
||||
return
|
||||
}
|
||||
let _ = (context.engine.payments.updateStarGiftAddedToProfile(messageId: messageId, added: added)
|
||||
|> deliverOnMainQueue).startStandalone()
|
||||
if let updateSavedToProfile {
|
||||
updateSavedToProfile(added)
|
||||
} else {
|
||||
let _ = (context.engine.payments.updateStarGiftAddedToProfile(messageId: messageId, added: added)
|
||||
|> deliverOnMainQueue).startStandalone()
|
||||
}
|
||||
|
||||
self.dismissAnimated()
|
||||
|
||||
@ -774,7 +789,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
if let lastController = navigationController.viewControllers.last as? ViewController {
|
||||
let resultController = UndoOverlayController(
|
||||
presentationData: presentationData,
|
||||
content: .sticker(context: context, file: arguments.gift.file, loop: false, title: "Gift Saved to Profile", text: "The gift is now displayed in your profile.", undoText: nil, customAction: nil),
|
||||
content: .sticker(context: context, file: arguments.gift.file, loop: false, title: added ? "Gift Saved to Profile" : "Gift Removed from Profile", text: added ? "The gift is now displayed in your profile." : "The gift is no longer displayed in your profile.", undoText: nil, customAction: nil),
|
||||
elevatedLayout: lastController is ChatController,
|
||||
action: { _ in return true}
|
||||
)
|
||||
@ -795,9 +810,12 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
actions: [
|
||||
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}),
|
||||
TextAlertAction(type: .defaultAction, title: "Convert", action: { [weak self, weak navigationController] in
|
||||
let _ = (context.engine.payments.convertStarGift(messageId: messageId)
|
||||
|> deliverOnMainQueue).startStandalone()
|
||||
|
||||
if let convertToStars {
|
||||
convertToStars()
|
||||
} else {
|
||||
let _ = (context.engine.payments.convertStarGift(messageId: messageId)
|
||||
|> deliverOnMainQueue).startStandalone()
|
||||
}
|
||||
self?.dismissAnimated()
|
||||
|
||||
if let navigationController {
|
||||
@ -827,6 +845,14 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
)
|
||||
self.present(controller, in: .window(.root))
|
||||
}
|
||||
openStarsIntroImpl = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let introController = context.sharedContext.makeStarsIntroScreen(context: context)
|
||||
introController.navigationPresentation = .modal
|
||||
self.push(introController)
|
||||
}
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
@ -1130,14 +1156,18 @@ private final class PeerCellComponent: Component {
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let avatar = ComponentView<Empty>()
|
||||
private let avatarNode: AvatarNode
|
||||
private let text = ComponentView<Empty>()
|
||||
|
||||
private var component: PeerCellComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 8.0))
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubnode(self.avatarNode)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@ -1152,29 +1182,25 @@ private final class PeerCellComponent: Component {
|
||||
let spacing: CGFloat = 6.0
|
||||
|
||||
let peerName: String
|
||||
let peer: StarsContext.State.Transaction.Peer
|
||||
let avatarOverride: AvatarNodeImageOverride?
|
||||
if let peerValue = component.peer {
|
||||
peerName = peerValue.compactDisplayTitle
|
||||
peer = .peer(peerValue)
|
||||
avatarOverride = nil
|
||||
} else {
|
||||
//TODO:localize
|
||||
peerName = "Hidden Name"
|
||||
peer = .fragment
|
||||
avatarOverride = .anonymousSavedMessagesIcon(isColored: true)
|
||||
}
|
||||
|
||||
let avatarNaturalSize = self.avatar.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
StarsAvatarComponent(context: component.context, theme: component.theme, peer: peer, photo: nil, media: [], backgroundColor: .clear)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 40.0, height: 40.0)
|
||||
)
|
||||
let avatarNaturalSize = CGSize(width: 40.0, height: 40.0)
|
||||
self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: component.peer, overrideImage: avatarOverride)
|
||||
self.avatarNode.bounds = CGRect(origin: .zero, size: avatarNaturalSize)
|
||||
|
||||
let textSize = self.text.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: peerName, font: Font.regular(15.0), textColor: component.theme.list.itemAccentColor, paragraphAlignment: .left))
|
||||
text: .plain(NSAttributedString(string: peerName, font: Font.regular(15.0), textColor: component.peer != nil ? component.theme.list.itemAccentColor : component.theme.list.itemPrimaryTextColor, paragraphAlignment: .left))
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
@ -1184,15 +1210,7 @@ private final class PeerCellComponent: Component {
|
||||
let size = CGSize(width: avatarSize.width + textSize.width + spacing, height: textSize.height)
|
||||
|
||||
let avatarFrame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((size.height - avatarSize.height) / 2.0)), size: avatarSize)
|
||||
|
||||
if let view = self.avatar.view {
|
||||
if view.superview == nil {
|
||||
self.addSubview(view)
|
||||
}
|
||||
let scale = avatarSize.width / avatarNaturalSize.width
|
||||
view.transform = CGAffineTransform(scaleX: scale, y: scale)
|
||||
view.frame = avatarFrame
|
||||
}
|
||||
self.avatarNode.frame = avatarFrame
|
||||
|
||||
if let view = self.text.view {
|
||||
if view.superview == nil {
|
||||
|
||||
@ -100,7 +100,6 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode
|
||||
private let listNode: ListView
|
||||
private var currentEntries: [RecommendedChannelsListEntry] = []
|
||||
private var currentState: (RecommendedChannels?, Bool)?
|
||||
private var canLoadMore: Bool = false
|
||||
private var enqueuedTransactions: [RecommendedChannelsListTransaction] = []
|
||||
|
||||
private var unlockBackground: UIImageView?
|
||||
|
||||
@ -51,6 +51,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/Gifts/GiftItemComponent",
|
||||
"//submodules/TelegramUI/Components/Gifts/GiftViewScreen",
|
||||
"//submodules/TelegramUI/Components/ButtonComponent",
|
||||
"//submodules/Components/BalancedTextComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@ -15,12 +15,13 @@ import MergeLists
|
||||
import ItemListUI
|
||||
import ChatControllerInteraction
|
||||
import MultilineTextComponent
|
||||
import BalancedTextComponent
|
||||
import Markdown
|
||||
import PeerInfoPaneNode
|
||||
import GiftItemComponent
|
||||
import PlainButtonComponent
|
||||
import GiftViewScreen
|
||||
import ButtonComponent
|
||||
import SolidRoundedButtonNode
|
||||
|
||||
public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScrollViewDelegate {
|
||||
private let context: AccountContext
|
||||
@ -37,6 +38,10 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let scrollNode: ASScrollNode
|
||||
|
||||
private var unlockBackground: UIImageView?
|
||||
private var unlockText: ComponentView<Empty>?
|
||||
private var unlockButton: SolidRoundedButtonNode?
|
||||
|
||||
private var currentParams: (size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData)?
|
||||
|
||||
private var theme: PresentationTheme?
|
||||
@ -82,7 +87,8 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.statusPromise.set(.single(PeerInfoStatusData(text: "\(state.count ?? 0) gifts", isActivity: true, key: .gifts)))
|
||||
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||
self.statusPromise.set(.single(PeerInfoStatusData(text: presentationData.strings.SharedMedia_GiftCount(state.count ?? 0), isActivity: true, key: .gifts)))
|
||||
self.starsProducts = state.gifts
|
||||
|
||||
if !self.didSetReady {
|
||||
@ -149,6 +155,13 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
||||
}
|
||||
|
||||
if isVisible {
|
||||
let ribbonText: String?
|
||||
if let availability = product.gift.availability {
|
||||
//TODO:localize
|
||||
ribbonText = "1 of \(compactNumericCountString(Int(availability.total)))"
|
||||
} else {
|
||||
ribbonText = nil
|
||||
}
|
||||
let _ = visibleItem.update(
|
||||
transition: itemTransition,
|
||||
component: AnyComponent(
|
||||
@ -157,26 +170,36 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
||||
GiftItemComponent(
|
||||
context: self.context,
|
||||
theme: params.presentationData.theme,
|
||||
peer: product.fromPeer,
|
||||
peer: product.fromPeer.flatMap { .peer($0) } ?? .anonymous,
|
||||
subject: .starGift(product.gift.id, product.gift.file),
|
||||
price: "⭐️ \(product.gift.price)",
|
||||
ribbon: product.gift.availability != nil ?
|
||||
GiftItemComponent.Ribbon(
|
||||
text: "1 of 1K",
|
||||
color: UIColor(rgb: 0x58c1fe)
|
||||
)
|
||||
: nil
|
||||
ribbon: ribbonText.flatMap { GiftItemComponent.Ribbon(text: $0, color: .blue) },
|
||||
isHidden: !product.savedToProfile
|
||||
)
|
||||
),
|
||||
effectAlignment: .center,
|
||||
action: { [weak self] in
|
||||
if let self {
|
||||
let controller = GiftViewScreen(
|
||||
context: self.context,
|
||||
subject: .profileGift(self.peerId, product)
|
||||
)
|
||||
self.parentController?.push(controller)
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let controller = GiftViewScreen(
|
||||
context: self.context,
|
||||
subject: .profileGift(self.peerId, product),
|
||||
updateSavedToProfile: { [weak self] added in
|
||||
guard let self, let messageId = product.messageId else {
|
||||
return
|
||||
}
|
||||
self.profileGifts.updateStarGiftAddedToProfile(messageId: messageId, added: added)
|
||||
},
|
||||
convertToStars: { [weak self] in
|
||||
guard let self, let messageId = product.messageId else {
|
||||
return
|
||||
}
|
||||
self.profileGifts.convertStarGift(messageId: messageId)
|
||||
}
|
||||
)
|
||||
self.parentController?.push(controller)
|
||||
|
||||
},
|
||||
animateAlpha: false
|
||||
)
|
||||
@ -198,46 +221,124 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
||||
}
|
||||
}
|
||||
|
||||
let contentHeight = ceil(CGFloat(starsProducts.count) / 3.0) * starsOptionSize.height + 60.0 + params.bottomInset + 16.0
|
||||
var contentHeight = ceil(CGFloat(starsProducts.count) / 3.0) * starsOptionSize.height + 60.0 + 16.0
|
||||
|
||||
// //TODO:localize
|
||||
// let buttonSize = self.button.update(
|
||||
// transition: .immediate,
|
||||
// component: AnyComponent(ButtonComponent(
|
||||
// background: ButtonComponent.Background(
|
||||
// color: params.presentationData.theme.list.itemCheckColors.fillColor,
|
||||
// foreground: params.presentationData.theme.list.itemCheckColors.foregroundColor,
|
||||
// pressedColor: params.presentationData.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9),
|
||||
// cornerRadius: 10.0
|
||||
// ),
|
||||
// content: AnyComponentWithIdentity(
|
||||
// id: AnyHashable(0),
|
||||
// component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: "Send Gifts to Friends", font: Font.semibold(17.0), textColor: )params.presentationData.theme.list.itemCheckColors.foregroundColor)))
|
||||
// ),
|
||||
// isEnabled: true,
|
||||
// displaysProgress: false,
|
||||
// action: {
|
||||
//
|
||||
// }
|
||||
// )),
|
||||
// environment: {},
|
||||
// containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50)
|
||||
// )
|
||||
// if let buttonView = self.button.view {
|
||||
// if buttonView.superview == nil {
|
||||
// self.addSubview(buttonView)
|
||||
// }
|
||||
// buttonView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - buttonSize.width) / 2.0), y: availableSize.height - environment.safeInsets.bottom - buttonSize.height), size: buttonSize)
|
||||
// }
|
||||
|
||||
// contentHeight += 100.0
|
||||
if self.peerId == self.context.account.peerId {
|
||||
let transition = ComponentTransition.immediate
|
||||
|
||||
let size = params.size
|
||||
let sideInset = params.sideInset
|
||||
let bottomInset = params.bottomInset
|
||||
let presentationData = params.presentationData
|
||||
|
||||
let themeUpdated = self.theme !== presentationData.theme
|
||||
self.theme = presentationData.theme
|
||||
|
||||
let unlockText: ComponentView<Empty>
|
||||
let unlockBackground: UIImageView
|
||||
let unlockButton: SolidRoundedButtonNode
|
||||
if let current = self.unlockText {
|
||||
unlockText = current
|
||||
} else {
|
||||
unlockText = ComponentView<Empty>()
|
||||
self.unlockText = unlockText
|
||||
}
|
||||
|
||||
if let current = self.unlockBackground {
|
||||
unlockBackground = current
|
||||
} else {
|
||||
unlockBackground = UIImageView()
|
||||
unlockBackground.contentMode = .scaleToFill
|
||||
self.view.addSubview(unlockBackground)
|
||||
self.unlockBackground = unlockBackground
|
||||
}
|
||||
|
||||
if let current = self.unlockButton {
|
||||
unlockButton = current
|
||||
} else {
|
||||
unlockButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: presentationData.theme), height: 50.0, cornerRadius: 10.0)
|
||||
self.view.addSubview(unlockButton.view)
|
||||
self.unlockButton = unlockButton
|
||||
|
||||
//TODO:localize
|
||||
unlockButton.title = "Send Gifts to Friends"
|
||||
|
||||
unlockButton.pressed = { [weak self] in
|
||||
self?.buttonPressed()
|
||||
}
|
||||
}
|
||||
|
||||
if themeUpdated {
|
||||
let topColor = presentationData.theme.list.plainBackgroundColor.withAlphaComponent(0.0)
|
||||
let bottomColor = presentationData.theme.list.plainBackgroundColor
|
||||
unlockBackground.image = generateGradientImage(size: CGSize(width: 1.0, height: 170.0), colors: [topColor, bottomColor, bottomColor], locations: [0.0, 0.3, 1.0])
|
||||
unlockButton.updateTheme(SolidRoundedButtonTheme(theme: presentationData.theme))
|
||||
}
|
||||
|
||||
let textFont = Font.regular(13.0)
|
||||
let boldTextFont = Font.semibold(13.0)
|
||||
let textColor = presentationData.theme.list.itemSecondaryTextColor
|
||||
let linkColor = presentationData.theme.list.itemAccentColor
|
||||
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: boldTextFont, textColor: linkColor), linkAttribute: { _ in
|
||||
return nil
|
||||
})
|
||||
|
||||
let scrollOffset: CGFloat = min(0.0, self.scrollNode.view.contentOffset.y + bottomInset + 80.0)
|
||||
|
||||
transition.setFrame(view: unlockBackground, frame: CGRect(x: 0.0, y: size.height - bottomInset - 170.0 + scrollOffset, width: size.width, height: bottomInset + 170.0))
|
||||
|
||||
let buttonSideInset = sideInset + 16.0
|
||||
let buttonSize = CGSize(width: size.width - buttonSideInset * 2.0, height: 50.0)
|
||||
transition.setFrame(view: unlockButton.view, frame: CGRect(origin: CGPoint(x: buttonSideInset, y: size.height - bottomInset - buttonSize.height - 26.0), size: buttonSize))
|
||||
let _ = unlockButton.updateLayout(width: buttonSize.width, transition: .immediate)
|
||||
|
||||
let unlockSize = unlockText.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
BalancedTextComponent(
|
||||
text: .markdown(text: "These gifts were sent to you by other users. Tap on a gift to exchange it for Stars or change its privacy settings.", attributes: markdownAttributes),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 0,
|
||||
lineSpacing: 0.2
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: size.width - 32.0, height: 200.0)
|
||||
)
|
||||
if let view = unlockText.view {
|
||||
if view.superview == nil {
|
||||
view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.buttonPressed)))
|
||||
self.scrollNode.view.addSubview(view)
|
||||
}
|
||||
transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: floor((size.width - unlockSize.width) / 2.0), y: contentHeight), size: unlockSize))
|
||||
}
|
||||
contentHeight += unlockSize.height
|
||||
}
|
||||
contentHeight += params.bottomInset
|
||||
|
||||
let contentSize = CGSize(width: params.size.width, height: contentHeight)
|
||||
if self.scrollNode.view.contentSize != contentSize {
|
||||
self.scrollNode.view.contentSize = contentSize
|
||||
}
|
||||
}
|
||||
|
||||
let bottomOffset = max(0.0, self.scrollNode.view.contentSize.height - self.scrollNode.view.contentOffset.y - self.scrollNode.view.frame.height)
|
||||
if bottomOffset < 100.0 {
|
||||
self.profileGifts.loadMore()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func buttonPressed() {
|
||||
let _ = (self.context.account.stateManager.contactBirthdays
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] birthdays in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let controller = self.context.sharedContext.makePremiumGiftController(context: self.context, source: .settings(birthdays), completion: nil)
|
||||
controller.navigationPresentation = .modal
|
||||
self.chatControllerInteraction.navigationController()?.pushViewController(controller)
|
||||
})
|
||||
}
|
||||
|
||||
public func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
|
||||
|
||||
@ -0,0 +1,44 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "StarsIntroScreen",
|
||||
module_name = "StarsIntroScreen",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/Postbox",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/Components/ViewControllerComponent",
|
||||
"//submodules/Components/ComponentDisplayAdapters",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/Components/BalancedTextComponent",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/AppBundle",
|
||||
"//submodules/ItemListUI",
|
||||
"//submodules/TelegramStringFormatting",
|
||||
"//submodules/PresentationDataUtils",
|
||||
"//submodules/Components/SheetComponent",
|
||||
"//submodules/UndoUI",
|
||||
"//submodules/TextFormat",
|
||||
"//submodules/TelegramUI/Components/ListSectionComponent",
|
||||
"//submodules/TelegramUI/Components/ListActionItemComponent",
|
||||
"//submodules/TelegramUI/Components/ScrollComponent",
|
||||
"//submodules/TelegramUI/Components/Premium/PremiumStarComponent",
|
||||
"//submodules/TelegramUI/Components/ButtonComponent",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
"//submodules/Components/SolidRoundedButtonComponent",
|
||||
"//submodules/Components/BlurredBackgroundComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
@ -0,0 +1,573 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import Markdown
|
||||
import TextFormat
|
||||
import TelegramPresentationData
|
||||
import ViewControllerComponent
|
||||
import ScrollComponent
|
||||
import BundleIconComponent
|
||||
import BalancedTextComponent
|
||||
import MultilineTextComponent
|
||||
import SolidRoundedButtonComponent
|
||||
import AccountContext
|
||||
import ScrollComponent
|
||||
import BlurredBackgroundComponent
|
||||
import PremiumStarComponent
|
||||
|
||||
private final class ScrollContent: CombinedComponent {
|
||||
typealias EnvironmentType = (ViewControllerComponentContainer.Environment, ScrollChildEnvironment)
|
||||
|
||||
let context: AccountContext
|
||||
let openExamples: () -> Void
|
||||
let dismiss: () -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
openExamples: @escaping () -> Void,
|
||||
dismiss: @escaping () -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.openExamples = openExamples
|
||||
self.dismiss = dismiss
|
||||
}
|
||||
|
||||
static func ==(lhs: ScrollContent, rhs: ScrollContent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
static var body: Body {
|
||||
let star = Child(PremiumStarComponent.self)
|
||||
|
||||
let title = Child(BalancedTextComponent.self)
|
||||
let text = Child(BalancedTextComponent.self)
|
||||
let list = Child(List<Empty>.self)
|
||||
|
||||
return { context in
|
||||
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
|
||||
let component = context.component
|
||||
|
||||
let theme = environment.theme
|
||||
//let strings = environment.strings
|
||||
|
||||
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
|
||||
let textSideInset: CGFloat = 30.0 + environment.safeInsets.left
|
||||
|
||||
let titleFont = Font.semibold(20.0)
|
||||
let textFont = Font.regular(15.0)
|
||||
|
||||
let textColor = theme.actionSheet.primaryTextColor
|
||||
let secondaryTextColor = theme.actionSheet.secondaryTextColor
|
||||
let linkColor = theme.actionSheet.controlAccentColor
|
||||
|
||||
let spacing: CGFloat = 16.0
|
||||
var contentSize = CGSize(width: context.availableSize.width, height: 152.0)
|
||||
|
||||
let star = star.update(
|
||||
component: PremiumStarComponent(
|
||||
theme: environment.theme,
|
||||
isIntro: true,
|
||||
isVisible: true,
|
||||
hasIdleAnimations: true,
|
||||
colors: [
|
||||
UIColor(rgb: 0xe57d02),
|
||||
UIColor(rgb: 0xf09903),
|
||||
UIColor(rgb: 0xf9b004),
|
||||
UIColor(rgb: 0xfdd219)
|
||||
],
|
||||
particleColor: UIColor(rgb: 0xf9b004),
|
||||
backgroundColor: environment.theme.list.plainBackgroundColor
|
||||
),
|
||||
availableSize: CGSize(width: min(414.0, context.availableSize.width), height: 220.0),
|
||||
transition: context.transition
|
||||
)
|
||||
context.add(star
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: environment.navigationHeight + 24.0))
|
||||
)
|
||||
|
||||
let title = title.update(
|
||||
component: BalancedTextComponent(
|
||||
text: .plain(NSAttributedString(string: "What are Stars?", font: titleFont, textColor: textColor)),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 0,
|
||||
lineSpacing: 0.1
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(title
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height / 2.0))
|
||||
)
|
||||
contentSize.height += title.size.height
|
||||
contentSize.height += spacing - 8.0
|
||||
|
||||
let text = text.update(
|
||||
component: BalancedTextComponent(
|
||||
text: .plain(NSAttributedString(string: "Buy packages of Stars on Telegram that let you do following:", font: textFont, textColor: secondaryTextColor)),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 0,
|
||||
lineSpacing: 0.2
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(text
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + text.size.height / 2.0))
|
||||
)
|
||||
contentSize.height += text.size.height
|
||||
contentSize.height += spacing
|
||||
|
||||
var items: [AnyComponentWithIdentity<Empty>] = []
|
||||
items.append(
|
||||
AnyComponentWithIdentity(
|
||||
id: "gift",
|
||||
component: AnyComponent(ParagraphComponent(
|
||||
title: "Send Gifts to Friends",
|
||||
titleColor: textColor,
|
||||
text: "Give your friends gifts that can be kept on their profiles or converted to Stars.",
|
||||
textColor: secondaryTextColor,
|
||||
accentColor: linkColor,
|
||||
iconName: "Premium/StarsPerk/Gift",
|
||||
iconColor: linkColor
|
||||
))
|
||||
)
|
||||
)
|
||||
items.append(
|
||||
AnyComponentWithIdentity(
|
||||
id: "miniapp",
|
||||
component: AnyComponent(ParagraphComponent(
|
||||
title: "Use Stars in Miniapps",
|
||||
titleColor: textColor,
|
||||
text: "Buy additional content and services in Telegram miniapps. [See Examples >]()",
|
||||
textColor: secondaryTextColor,
|
||||
accentColor: linkColor,
|
||||
iconName: "Premium/StarsPerk/Miniapp",
|
||||
iconColor: linkColor,
|
||||
action: {
|
||||
component.openExamples()
|
||||
}
|
||||
))
|
||||
)
|
||||
)
|
||||
items.append(
|
||||
AnyComponentWithIdentity(
|
||||
id: "media",
|
||||
component: AnyComponent(ParagraphComponent(
|
||||
title: "Unlock Content in Channels",
|
||||
titleColor: textColor,
|
||||
text: "Get access to paid content and services in Telegram channels.",
|
||||
textColor: secondaryTextColor,
|
||||
accentColor: linkColor,
|
||||
iconName: "Premium/StarsPerk/Media",
|
||||
iconColor: linkColor
|
||||
))
|
||||
)
|
||||
)
|
||||
items.append(
|
||||
AnyComponentWithIdentity(
|
||||
id: "reaction",
|
||||
component: AnyComponent(ParagraphComponent(
|
||||
title: "Send Star Reactions",
|
||||
titleColor: textColor,
|
||||
text: "Support your favorite channels by sending Star reactions to their posts.",
|
||||
textColor: secondaryTextColor,
|
||||
accentColor: linkColor,
|
||||
iconName: "Premium/StarsPerk/Reaction",
|
||||
iconColor: linkColor
|
||||
))
|
||||
)
|
||||
)
|
||||
|
||||
let list = list.update(
|
||||
component: List(items),
|
||||
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 10000.0),
|
||||
transition: context.transition
|
||||
)
|
||||
context.add(list
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + list.size.height / 2.0))
|
||||
)
|
||||
contentSize.height += list.size.height
|
||||
contentSize.height += spacing - 9.0
|
||||
|
||||
contentSize.height += 12.0 + 50.0
|
||||
if environment.safeInsets.bottom > 0 {
|
||||
contentSize.height += environment.safeInsets.bottom + 5.0
|
||||
} else {
|
||||
contentSize.height += 12.0
|
||||
}
|
||||
|
||||
return contentSize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class ContainerComponent: CombinedComponent {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
let context: AccountContext
|
||||
let openExamples: () -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
openExamples: @escaping () -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.openExamples = openExamples
|
||||
}
|
||||
|
||||
static func ==(lhs: ContainerComponent, rhs: ContainerComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class State: ComponentState {
|
||||
var topContentOffset: CGFloat?
|
||||
var bottomContentOffset: CGFloat?
|
||||
}
|
||||
|
||||
func makeState() -> State {
|
||||
return State()
|
||||
}
|
||||
|
||||
static var body: Body {
|
||||
let background = Child(Rectangle.self)
|
||||
let scroll = Child(ScrollComponent<ViewControllerComponentContainer.Environment>.self)
|
||||
let bottomPanel = Child(BlurredBackgroundComponent.self)
|
||||
let bottomSeparator = Child(Rectangle.self)
|
||||
let actionButton = Child(SolidRoundedButtonComponent.self)
|
||||
let scrollExternalState = ScrollComponent<EnvironmentType>.ExternalState()
|
||||
|
||||
return { context in
|
||||
let environment = context.environment[EnvironmentType.self]
|
||||
let theme = environment.theme
|
||||
//let strings = environment.strings
|
||||
let state = context.state
|
||||
|
||||
let controller = environment.controller
|
||||
|
||||
let background = background.update(
|
||||
component: Rectangle(color: environment.theme.list.plainBackgroundColor),
|
||||
environment: {},
|
||||
availableSize: context.availableSize,
|
||||
transition: context.transition
|
||||
)
|
||||
context.add(background
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
|
||||
)
|
||||
|
||||
let scroll = scroll.update(
|
||||
component: ScrollComponent<EnvironmentType>(
|
||||
content: AnyComponent(ScrollContent(
|
||||
context: context.component.context,
|
||||
openExamples: context.component.openExamples,
|
||||
dismiss: {
|
||||
controller()?.dismiss()
|
||||
}
|
||||
)),
|
||||
externalState: scrollExternalState,
|
||||
contentInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 1.0, right: 0.0),
|
||||
contentOffsetUpdated: { [weak state] topContentOffset, bottomContentOffset in
|
||||
state?.topContentOffset = topContentOffset
|
||||
state?.bottomContentOffset = bottomContentOffset
|
||||
Queue.mainQueue().justDispatch {
|
||||
state?.updated(transition: .immediate)
|
||||
}
|
||||
},
|
||||
contentOffsetWillCommit: { targetContentOffset in
|
||||
}
|
||||
),
|
||||
environment: { environment },
|
||||
availableSize: context.availableSize,
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
context.add(scroll
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
|
||||
)
|
||||
|
||||
let buttonHeight: CGFloat = 50.0
|
||||
let bottomPanelPadding: CGFloat = 12.0
|
||||
let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding
|
||||
let bottomPanelHeight = bottomPanelPadding + buttonHeight + bottomInset
|
||||
|
||||
let bottomPanelAlpha: CGFloat
|
||||
if scrollExternalState.contentHeight > context.availableSize.height {
|
||||
if let bottomContentOffset = state.bottomContentOffset {
|
||||
bottomPanelAlpha = min(16.0, bottomContentOffset) / 16.0
|
||||
} else {
|
||||
bottomPanelAlpha = 1.0
|
||||
}
|
||||
} else {
|
||||
bottomPanelAlpha = 0.0
|
||||
}
|
||||
|
||||
let bottomPanel = bottomPanel.update(
|
||||
component: BlurredBackgroundComponent(
|
||||
color: theme.rootController.tabBar.backgroundColor
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width, height: bottomPanelHeight),
|
||||
transition: context.transition
|
||||
)
|
||||
let bottomSeparator = bottomSeparator.update(
|
||||
component: Rectangle(
|
||||
color: theme.rootController.tabBar.separatorColor
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width, height: UIScreenPixel),
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
context.add(bottomPanel
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height / 2.0))
|
||||
.opacity(bottomPanelAlpha)
|
||||
)
|
||||
context.add(bottomSeparator
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height))
|
||||
.opacity(bottomPanelAlpha)
|
||||
)
|
||||
|
||||
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
|
||||
let actionButton = actionButton.update(
|
||||
component: SolidRoundedButtonComponent(
|
||||
title: "Got It",
|
||||
theme: SolidRoundedButtonComponent.Theme(
|
||||
backgroundColor: theme.list.itemCheckColors.fillColor,
|
||||
backgroundColors: [],
|
||||
foregroundColor: theme.list.itemCheckColors.foregroundColor
|
||||
),
|
||||
font: .bold,
|
||||
fontSize: 17.0,
|
||||
height: buttonHeight,
|
||||
cornerRadius: 10.0,
|
||||
gloss: false,
|
||||
iconName: nil,
|
||||
animationName: nil,
|
||||
iconPosition: .left,
|
||||
action: {
|
||||
controller()?.dismiss()
|
||||
}
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0),
|
||||
transition: context.transition
|
||||
)
|
||||
context.add(actionButton
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanelHeight + bottomPanelPadding + actionButton.size.height / 2.0))
|
||||
)
|
||||
|
||||
return context.availableSize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class StarsIntroScreen: ViewControllerComponentContainer {
|
||||
private let context: AccountContext
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
forceDark: Bool = false
|
||||
) {
|
||||
self.context = context
|
||||
|
||||
var openExamplesImpl: (() -> Void)?
|
||||
super.init(
|
||||
context: context,
|
||||
component: ContainerComponent(
|
||||
context: context,
|
||||
openExamples: {
|
||||
openExamplesImpl?()
|
||||
}
|
||||
),
|
||||
navigationBarAppearance: .none,
|
||||
statusBarStyle: .ignore,
|
||||
theme: forceDark ? .dark : .default
|
||||
)
|
||||
|
||||
self.navigationPresentation = .modal
|
||||
|
||||
openExamplesImpl = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let _ = (context.sharedContext.makeMiniAppListScreenInitialData(context: context)
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak self] initialData in
|
||||
guard let self, let navigationController = self.navigationController as? NavigationController else {
|
||||
return
|
||||
}
|
||||
navigationController.pushViewController(context.sharedContext.makeMiniAppListScreen(context: context, initialData: initialData))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
private final class ParagraphComponent: CombinedComponent {
|
||||
let title: String
|
||||
let titleColor: UIColor
|
||||
let text: String
|
||||
let textColor: UIColor
|
||||
let accentColor: UIColor
|
||||
let iconName: String
|
||||
let iconColor: UIColor
|
||||
let action: () -> Void
|
||||
|
||||
public init(
|
||||
title: String,
|
||||
titleColor: UIColor,
|
||||
text: String,
|
||||
textColor: UIColor,
|
||||
accentColor: UIColor,
|
||||
iconName: String,
|
||||
iconColor: UIColor,
|
||||
action: @escaping () -> Void = {}
|
||||
) {
|
||||
self.title = title
|
||||
self.titleColor = titleColor
|
||||
self.text = text
|
||||
self.textColor = textColor
|
||||
self.accentColor = accentColor
|
||||
self.iconName = iconName
|
||||
self.iconColor = iconColor
|
||||
self.action = action
|
||||
}
|
||||
|
||||
static func ==(lhs: ParagraphComponent, rhs: ParagraphComponent) -> Bool {
|
||||
if lhs.title != rhs.title {
|
||||
return false
|
||||
}
|
||||
if lhs.titleColor != rhs.titleColor {
|
||||
return false
|
||||
}
|
||||
if lhs.text != rhs.text {
|
||||
return false
|
||||
}
|
||||
if lhs.textColor != rhs.textColor {
|
||||
return false
|
||||
}
|
||||
if lhs.accentColor != rhs.accentColor {
|
||||
return false
|
||||
}
|
||||
if lhs.iconName != rhs.iconName {
|
||||
return false
|
||||
}
|
||||
if lhs.iconColor != rhs.iconColor {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class State: ComponentState {
|
||||
var cachedChevronImage: (UIImage, UIColor)?
|
||||
}
|
||||
|
||||
func makeState() -> State {
|
||||
return State()
|
||||
}
|
||||
|
||||
static var body: Body {
|
||||
let title = Child(MultilineTextComponent.self)
|
||||
let text = Child(MultilineTextComponent.self)
|
||||
let icon = Child(BundleIconComponent.self)
|
||||
|
||||
return { context in
|
||||
let component = context.component
|
||||
let state = context.state
|
||||
|
||||
let leftInset: CGFloat = 32.0
|
||||
let rightInset: CGFloat = 24.0
|
||||
let textSideInset: CGFloat = leftInset + 8.0
|
||||
let spacing: CGFloat = 5.0
|
||||
|
||||
let textTopInset: CGFloat = 9.0
|
||||
|
||||
let title = title.update(
|
||||
component: MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: component.title,
|
||||
font: Font.semibold(15.0),
|
||||
textColor: component.titleColor,
|
||||
paragraphAlignment: .natural
|
||||
)),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 1
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude),
|
||||
transition: .immediate
|
||||
)
|
||||
|
||||
let textFont = Font.regular(15.0)
|
||||
let boldTextFont = Font.semibold(15.0)
|
||||
let textColor = component.textColor
|
||||
let accentColor = component.accentColor
|
||||
let markdownAttributes = MarkdownAttributes(
|
||||
body: MarkdownAttributeSet(font: textFont, textColor: textColor),
|
||||
bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor),
|
||||
link: MarkdownAttributeSet(font: textFont, textColor: accentColor),
|
||||
linkAttribute: { contents in
|
||||
return (TelegramTextAttributes.URL, contents)
|
||||
}
|
||||
)
|
||||
|
||||
if state.cachedChevronImage == nil || state.cachedChevronImage?.1 != accentColor {
|
||||
state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: accentColor)!, accentColor)
|
||||
}
|
||||
let textAttributedString = parseMarkdownIntoAttributedString(component.text, attributes: markdownAttributes).mutableCopy() as! NSMutableAttributedString
|
||||
if let range = textAttributedString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 {
|
||||
textAttributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: textAttributedString.string))
|
||||
}
|
||||
|
||||
let text = text.update(
|
||||
component: MultilineTextComponent(
|
||||
text: .plain(textAttributedString),
|
||||
horizontalAlignment: .natural,
|
||||
maximumNumberOfLines: 0,
|
||||
lineSpacing: 0.2,
|
||||
highlightAction: { attributes in
|
||||
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
|
||||
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
},
|
||||
tapAction: { _, _ in
|
||||
component.action()
|
||||
}
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: context.availableSize.height),
|
||||
transition: .immediate
|
||||
)
|
||||
|
||||
let icon = icon.update(
|
||||
component: BundleIconComponent(
|
||||
name: component.iconName,
|
||||
tintColor: component.iconColor
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height),
|
||||
transition: .immediate
|
||||
)
|
||||
|
||||
context.add(title
|
||||
.position(CGPoint(x: textSideInset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0))
|
||||
)
|
||||
|
||||
context.add(text
|
||||
.position(CGPoint(x: textSideInset + text.size.width / 2.0, y: textTopInset + title.size.height + spacing + text.size.height / 2.0))
|
||||
)
|
||||
|
||||
context.add(icon
|
||||
.position(CGPoint(x: 15.0, y: textTopInset + 18.0))
|
||||
)
|
||||
|
||||
return CGSize(width: context.availableSize.width, height: textTopInset + title.size.height + text.size.height + 20.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -237,6 +237,8 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent {
|
||||
textString = renew ? strings.Stars_Purchase_SubscriptionRenewInfo(component.peers.first?.value.compactDisplayTitle ?? "").string : strings.Stars_Purchase_SubscriptionInfo(component.peers.first?.value.compactDisplayTitle ?? "").string
|
||||
case .unlockMedia:
|
||||
textString = strings.Stars_Purchase_StarsNeededUnlockInfo
|
||||
case .starGift:
|
||||
textString = strings.Stars_Purchase_StarGiftInfo(component.peers.first?.value.compactDisplayTitle ?? "").string
|
||||
}
|
||||
|
||||
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: accentColor), linkAttribute: { contents in
|
||||
@ -815,11 +817,9 @@ private final class StarsPurchaseScreenComponent: CombinedComponent {
|
||||
switch context.component.purpose {
|
||||
case .generic:
|
||||
titleText = strings.Stars_Purchase_GetStars
|
||||
case let .topUp(requiredStars, _):
|
||||
titleText = strings.Stars_Purchase_StarsNeeded(Int32(requiredStars))
|
||||
case .gift:
|
||||
titleText = strings.Stars_Purchase_GiftStars
|
||||
case let .transfer(_, requiredStars), let .reactions(_, requiredStars), let .subscription(_, requiredStars, _), let .unlockMedia(requiredStars):
|
||||
case let .topUp(requiredStars, _), let .transfer(_, requiredStars), let .reactions(_, requiredStars), let .subscription(_, requiredStars, _), let .unlockMedia(requiredStars), let .starGift(_, requiredStars):
|
||||
titleText = strings.Stars_Purchase_StarsNeeded(Int32(requiredStars))
|
||||
}
|
||||
|
||||
@ -1239,6 +1239,8 @@ private extension StarsPurchasePurpose {
|
||||
return [peerId]
|
||||
case let .subscription(peerId, _, _):
|
||||
return [peerId]
|
||||
case let .starGift(peerId, _):
|
||||
return [peerId]
|
||||
default:
|
||||
return []
|
||||
}
|
||||
@ -1256,6 +1258,8 @@ private extension StarsPurchasePurpose {
|
||||
return requiredStars
|
||||
case let .unlockMedia(requiredStars):
|
||||
return requiredStars
|
||||
case let .starGift(_, requiredStars):
|
||||
return requiredStars
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
12
submodules/TelegramUI/Images.xcassets/Peer Info/HiddenIcon.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Peer Info/HiddenIcon.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "hidden_30.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Peer Info/HiddenIcon.imageset/hidden_30.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Peer Info/HiddenIcon.imageset/hidden_30.pdf
vendored
Normal file
Binary file not shown.
@ -0,0 +1,9 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
||||
12
submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Gift.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Gift.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "gift_30 (4).pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Gift.imageset/gift_30 (4).pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Gift.imageset/gift_30 (4).pdf
vendored
Normal file
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Media.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Media.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "unlock_30.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Media.imageset/unlock_30.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Media.imageset/unlock_30.pdf
vendored
Normal file
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Miniapp.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Miniapp.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "bot_30.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Miniapp.imageset/bot_30.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Miniapp.imageset/bot_30.pdf
vendored
Normal file
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Reaction.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Reaction.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "cash_30.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
62
submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Reaction.imageset/cash_30.pdf
vendored
Normal file
62
submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Reaction.imageset/cash_30.pdf
vendored
Normal file
@ -0,0 +1,62 @@
|
||||
%PDF-1.7
|
||||
|
||||
1 0 obj
|
||||
<< >>
|
||||
endobj
|
||||
|
||||
2 0 obj
|
||||
<< /Filter /FlateDecode
|
||||
/Length 3 0 R
|
||||
>>
|
||||
stream
|
||||
xe•KŽT1EçYE<59>‘‰óõ<14>KxjRwK¨%ÖÏÉç%ÔÈïĹvl'õáóÓŸ_×Ó·/Ÿ¾›ûëz3¿<33>·®ÿî6Þ/kEBÔì·q½˜åö¿ñv½"6*Îæœ\©<>—&[Åå”É&§Qä!c;~sn›]flIØ›NϱÚ6tð»Ì¦=`r~ni‚#fŒx-t™¾¡e¸Ø0ðêK]ë&S§Ëü4ó.Ãþb¢-%Ö¸²óÙ¦”¼¦
¼®]ô®Ñ:Ñ<>&6–J¾ýqZˆê
]_6œÑ<C593><0C>&¶ÑéA:TÈnZèÍU<çoÖ«À—D/”)[õ}³°i±R%Ôò€9ÉA9t±©:ïÇq’N¿É8<C389>³¢¾¦“ªUýGp£ù2&¬\Š<›jcФß`õ¥’;*IK$…Øûìã<C3AC>Ī0p\sÂ{*ÏôÖ‡Ø\mЬ1´#Q¥æ§¶èðZˆ³y›|<7C>J=}6ˆûš7…éžJú5Tªhw/ÇCpÂb#íb†ˆ¬¡ºàÙæ(™ÍÓ,„ÞêÉ„©éÑ’Œ^¾aËZmˆ>ĺY¥]ýÈ…rDñu
|
||||
$>äá`¹Öš!*ŽHÕjM@%ŸIZ/R*‘é^l5b)dÅ[,Û’Äa1fï¸ö #0ëÔxÎMb_Ô¤ì[,ÚØæ«â`.–Šh+͘ր|o
|
||||
h–p!êºæAÁšßV;ÐÚ·NHv¼Ò9’ -æÄ©`òx™e—f—xþŠp×âÌc:Ð;àœ#ô$}¯!mà”*rë[
|
||||
¡{bݼX«Õ=ì¶byîFÜWïAµP¶ÖŒ+vÌp^Æv<C386>o,å¡oÌ´xž¯ÈpƒHËYZÚ“¶g^=Ù”Cs<43><1D><©p }-zPؼQ×ìTY=™Ë1óÚiS±™|Ûë’E:íaê,(¹'c7Ó‘yÂ9ä¢dq·f+n†öн)™S7áâ3(‰vbñüi)ÊsNCÆDñx:§™R²û¤óI}æ‘ÿñμš¯æ/²ô¤r
|
||||
endstream
|
||||
endobj
|
||||
|
||||
3 0 obj
|
||||
797
|
||||
endobj
|
||||
|
||||
4 0 obj
|
||||
<< /Annots []
|
||||
/Type /Page
|
||||
/MediaBox [ 0.000000 0.000000 30.000000 30.000000 ]
|
||||
/Resources 1 0 R
|
||||
/Contents 2 0 R
|
||||
/Parent 5 0 R
|
||||
>>
|
||||
endobj
|
||||
|
||||
5 0 obj
|
||||
<< /Kids [ 4 0 R ]
|
||||
/Count 1
|
||||
/Type /Pages
|
||||
>>
|
||||
endobj
|
||||
|
||||
6 0 obj
|
||||
<< /Pages 5 0 R
|
||||
/Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
|
||||
xref
|
||||
0 7
|
||||
0000000000 65535 f
|
||||
0000000010 00000 n
|
||||
0000000034 00000 n
|
||||
0000000915 00000 n
|
||||
0000000937 00000 n
|
||||
0000001110 00000 n
|
||||
0000001184 00000 n
|
||||
trailer
|
||||
<< /ID [ (some) (id) ]
|
||||
/Root 6 0 R
|
||||
/Size 7
|
||||
>>
|
||||
startxref
|
||||
1243
|
||||
%%EOF
|
||||
@ -72,6 +72,7 @@ import StarsWithdrawalScreen
|
||||
import MiniAppListScreen
|
||||
import GiftOptionsScreen
|
||||
import GiftViewScreen
|
||||
import StarsIntroScreen
|
||||
|
||||
private final class AccountUserInterfaceInUseContext {
|
||||
let subscribers = Bag<(Bool) -> Void>()
|
||||
@ -2205,8 +2206,6 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
public func makePremiumGiftController(context: AccountContext, source: PremiumGiftSource, completion: (([EnginePeer.Id]) -> Void)?) -> ViewController {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
// let limit: Int32 = 10
|
||||
// var reachedLimitImpl: ((Int32) -> Void)?
|
||||
var presentBirthdayPickerImpl: (() -> Void)?
|
||||
var starsMode: ContactSelectionControllerMode = .generic
|
||||
var currentBirthdays: [EnginePeer.Id: TelegramBirthday]?
|
||||
@ -2268,9 +2267,9 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
))
|
||||
let _ = combineLatest(queue: Queue.mainQueue(), contactsController.result, options.get())
|
||||
.startStandalone(next: { [weak contactsController] result, options in
|
||||
if let (peers, _, _, _, _, _) = result, let contactPeer = peers.first, case let .peer(peer, _, _) = contactPeer {
|
||||
if let (peers, _, _, _, _, _) = result, let contactPeer = peers.first, case let .peer(peer, _, _) = contactPeer, let starsContext = context.starsContext {
|
||||
let premiumOptions = options.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) }
|
||||
let giftController = GiftOptionsScreen(context: context, peerId: peer.id, premiumOptions: premiumOptions)
|
||||
let giftController = GiftOptionsScreen(context: context, starsContext: starsContext, peerId: peer.id, premiumOptions: premiumOptions)
|
||||
giftController.navigationPresentation = .modal
|
||||
contactsController?.push(giftController)
|
||||
|
||||
@ -2812,6 +2811,10 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
return StarsTransactionScreen(context: context, subject: .boost(peerId, boost))
|
||||
}
|
||||
|
||||
public func makeStarsIntroScreen(context: AccountContext) -> ViewController {
|
||||
return StarsIntroScreen(context: context)
|
||||
}
|
||||
|
||||
public func makeGiftViewScreen(context: AccountContext, message: EngineMessage) -> ViewController {
|
||||
return GiftViewScreen(context: context, subject: .message(message))
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user