Gifts improvements

This commit is contained in:
Ilya Laktyushin 2024-09-23 22:44:29 +04:00
parent 04d7a791c9
commit 2c56786809
30 changed files with 1564 additions and 259 deletions

View File

@ -12941,3 +12941,8 @@ Sorry for the inconvenience.";
"VerificationCodes.DescriptionText" = "This chat is used to receive verification codes from third-party services."; "VerificationCodes.DescriptionText" = "This chat is used to receive verification codes from third-party services.";
"Conversation.CodeCopied" = "Code copied to clipboard"; "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";

View File

@ -1017,6 +1017,7 @@ public protocol SharedAccountContext: AnyObject {
func makeStarsWithdrawalScreen(context: AccountContext, stats: StarsRevenueStats, completion: @escaping (Int64) -> Void) -> ViewController func makeStarsWithdrawalScreen(context: AccountContext, stats: StarsRevenueStats, completion: @escaping (Int64) -> Void) -> ViewController
func makeStarsGiftScreen(context: AccountContext, message: EngineMessage) -> ViewController func makeStarsGiftScreen(context: AccountContext, message: EngineMessage) -> ViewController
func makeStarsGiveawayBoostScreen(context: AccountContext, peerId: EnginePeer.Id, boost: ChannelBoostersContext.State.Boost) -> 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 makeGiftViewScreen(context: AccountContext, message: EngineMessage) -> ViewController
func makeMiniAppListScreenInitialData(context: AccountContext) -> Signal<MiniAppListScreenInitialData, NoError> func makeMiniAppListScreenInitialData(context: AccountContext) -> Signal<MiniAppListScreenInitialData, NoError>

View File

@ -130,6 +130,7 @@ public enum StarsPurchasePurpose: Equatable {
case subscription(peerId: EnginePeer.Id, requiredStars: Int64, renew: Bool) case subscription(peerId: EnginePeer.Id, requiredStars: Int64, renew: Bool)
case gift(peerId: EnginePeer.Id) case gift(peerId: EnginePeer.Id)
case unlockMedia(requiredStars: Int64) case unlockMedia(requiredStars: Int64)
case starGift(peerId: EnginePeer.Id, requiredStars: Int64)
} }
public struct PremiumConfiguration { public struct PremiumConfiguration {

View File

@ -238,6 +238,7 @@ private final class ProfileGiftsContextImpl {
private let peerId: PeerId private let peerId: PeerId
private let disposable = MetaDisposable() private let disposable = MetaDisposable()
private let actionDisposable = MetaDisposable()
private var gifts: [ProfileGiftsContext.State.StarGift] = [] private var gifts: [ProfileGiftsContext.State.StarGift] = []
private var count: Int32? private var count: Int32?
@ -258,6 +259,7 @@ private final class ProfileGiftsContextImpl {
deinit { deinit {
self.disposable.dispose() self.disposable.dispose()
self.actionDisposable.dispose()
} }
func loadMore() { 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() { private func pushState() {
self.stateValue.set(.single(ProfileGiftsContext.State(gifts: self.gifts, count: self.count, dataState: self.dataState))) 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 nameHidden: Bool
public let savedToProfile: Bool public let savedToProfile: Bool
public let convertStars: Int64? 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 { public enum DataState: Equatable {
@ -373,6 +410,18 @@ public final class ProfileGiftsContext {
impl.loadMore() 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 { private extension ProfileGiftsContext.State.StarGift {

View File

@ -459,6 +459,7 @@ swift_library(
"//submodules/TelegramUI/Components/MinimizedContainer", "//submodules/TelegramUI/Components/MinimizedContainer",
"//submodules/TelegramUI/Components/SpaceWarpView", "//submodules/TelegramUI/Components/SpaceWarpView",
"//submodules/TelegramUI/Components/MiniAppListScreen", "//submodules/TelegramUI/Components/MiniAppListScreen",
"//submodules/TelegramUI/Components/Stars/StarsIntroScreen",
"//submodules/TelegramUI/Components/Gifts/GiftOptionsScreen", "//submodules/TelegramUI/Components/Gifts/GiftOptionsScreen",
] + select({ ] + select({
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,

View File

@ -31,13 +31,16 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
private let backgroundMaskNode: ASImageNode private let backgroundMaskNode: ASImageNode
private var linkHighlightingNode: LinkHighlightingNode? private var linkHighlightingNode: LinkHighlightingNode?
private let mediaBackgroundMaskNode: ASImageNode
private var mediaBackgroundContent: WallpaperBubbleBackgroundNode? private var mediaBackgroundContent: WallpaperBubbleBackgroundNode?
private let mediaBackgroundNode: NavigationBackgroundNode
private let titleNode: TextNode private let titleNode: TextNode
private let subtitleNode: TextNode private let subtitleNode: TextNode
private let placeholderNode: StickerShimmerEffectNode private let placeholderNode: StickerShimmerEffectNode
private let animationNode: AnimatedStickerNode private let animationNode: AnimatedStickerNode
private let ribbonBackgroundNode: ASImageNode
private let ribbonTextNode: TextNode
private var shimmerEffectNode: ShimmerEffectForegroundNode? private var shimmerEffectNode: ShimmerEffectForegroundNode?
private let buttonNode: HighlightTrackingButtonNode private let buttonNode: HighlightTrackingButtonNode
private let buttonStarsNode: PremiumStarsNode private let buttonStarsNode: PremiumStarsNode
@ -79,9 +82,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
self.backgroundMaskNode = ASImageNode() self.backgroundMaskNode = ASImageNode()
self.mediaBackgroundNode = NavigationBackgroundNode(color: .clear) self.mediaBackgroundMaskNode = ASImageNode()
self.mediaBackgroundNode.clipsToBounds = true
self.mediaBackgroundNode.cornerRadius = 24.0
self.titleNode = TextNode() self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false self.titleNode.isUserInteractionEnabled = false
@ -107,19 +108,30 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
self.buttonTitleNode.isUserInteractionEnabled = false self.buttonTitleNode.isUserInteractionEnabled = false
self.buttonTitleNode.displaysAsynchronously = 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() super.init()
self.addSubnode(self.labelNode) self.addSubnode(self.labelNode)
self.addSubnode(self.mediaBackgroundNode)
self.addSubnode(self.titleNode) self.addSubnode(self.titleNode)
self.addSubnode(self.subtitleNode) self.addSubnode(self.subtitleNode)
self.addSubnode(self.subtitleNode)
self.addSubnode(self.placeholderNode)
self.addSubnode(self.animationNode) self.addSubnode(self.animationNode)
self.addSubnode(self.buttonNode) self.addSubnode(self.buttonNode)
self.buttonNode.addSubnode(self.buttonStarsNode) self.buttonNode.addSubnode(self.buttonStarsNode)
self.addSubnode(self.buttonTitleNode) self.addSubnode(self.buttonTitleNode)
self.addSubnode(self.ribbonBackgroundNode)
self.addSubnode(self.ribbonTextNode)
self.buttonNode.highligthedChanged = { [weak self] highlighted in self.buttonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self { if let strongSelf = self {
if highlighted { if highlighted {
@ -226,6 +238,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode) let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode)
let makeButtonTitleLayout = TextNode.asyncLayout(self.buttonTitleNode) let makeButtonTitleLayout = TextNode.asyncLayout(self.buttonTitleNode)
let makeRibbonTextLayout = TextNode.asyncLayout(self.ribbonTextNode)
let cachedMaskBackgroundImage = self.cachedMaskBackgroundImage let cachedMaskBackgroundImage = self.cachedMaskBackgroundImage
@ -247,6 +260,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
var title = item.presentationData.strings.Notification_PremiumGift_Title var title = item.presentationData.strings.Notification_PremiumGift_Title
var text = "" var text = ""
var buttonTitle = item.presentationData.strings.Notification_PremiumGift_View var buttonTitle = item.presentationData.strings.Notification_PremiumGift_View
var ribbonTitle = ""
var hasServiceMessage = true var hasServiceMessage = true
var textSpacing: CGFloat = 0.0 var textSpacing: CGFloat = 0.0
for media in item.message.media { for media in item.message.media {
@ -315,8 +329,9 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
buttonTitle = item.presentationData.strings.Notification_PremiumPrize_View buttonTitle = item.presentationData.strings.Notification_PremiumPrize_View
hasServiceMessage = false 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 let _ = nameHidden
//TODO:localize
let authorName = item.message.author.flatMap { EnginePeer($0) }?.compactDisplayTitle ?? "" let authorName = item.message.author.flatMap { EnginePeer($0) }?.compactDisplayTitle ?? ""
title = "Gift from \(authorName)" title = "Gift from \(authorName)"
if let giftText, !giftText.isEmpty { if let giftText, !giftText.isEmpty {
@ -344,6 +359,9 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
} }
} }
animationFile = gift.file animationFile = gift.file
if let availability = gift.availability {
ribbonTitle = "1 of \(availability.total)"
}
default: default:
break break
} }
@ -378,6 +396,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
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 (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 giftSize.height = titleLayout.size.height + textSpacing + subtitleLayout.size.height + 212.0
var labelRects = labelLayout.linesRects() var labelRects = labelLayout.linesRects()
@ -424,7 +444,35 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
return (backgroundSize.width, { boundingWidth in return (backgroundSize.width, { boundingWidth in
return (backgroundSize, { [weak self] animation, synchronousLoads, _ in return (backgroundSize, { [weak self] animation, synchronousLoads, _ in
if let strongSelf = self { if let strongSelf = self {
let overlayColor = item.presentationData.theme.theme.overallDarkAppearance ? UIColor(rgb: 0xffffff, alpha: 0.12) : UIColor(rgb: 0x000000, alpha: 0.12)
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)
var iconSize = CGSize(width: 160.0, height: 160.0)
var iconOffset: CGFloat = 0.0
if let _ = animationFile {
iconSize = CGSize(width: 120.0, height: 120.0)
iconOffset = 32.0
}
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 { 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 strongSelf.animationNode.autoplay = true
if let file = animationFile { if let file = animationFile {
@ -432,6 +480,11 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
if strongSelf.fetchDisposable == 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() 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") { } else if animationName.hasPrefix("Gift") {
strongSelf.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: animationName), width: 384, height: 384, playbackMode: .still(.end), mode: .direct(cachePathPrefix: nil)) strongSelf.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: animationName), width: 384, height: 384, playbackMode: .still(.end), mode: .direct(cachePathPrefix: nil))
} }
@ -442,27 +495,16 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
strongSelf.labelNode.isHidden = !hasServiceMessage 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) strongSelf.buttonNode.backgroundColor = overlayColor
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
if let _ = animationFile {
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)
strongSelf.animationNode.updateLayout(size: iconSize) strongSelf.animationNode.updateLayout(size: iconSize)
strongSelf.placeholderNode.frame = animationFrame
let _ = labelApply() let _ = labelApply()
let _ = titleApply() let _ = titleApply()
let _ = subtitleApply() let _ = subtitleApply()
let _ = buttonTitleApply() let _ = buttonTitleApply()
let _ = ribbonTextApply()
let labelFrame = CGRect(origin: CGPoint(x: 8.0, y: 2.0), size: labelLayout.size) let labelFrame = CGRect(origin: CGPoint(x: 8.0, y: 2.0), size: labelLayout.size)
strongSelf.labelNode.frame = labelFrame strongSelf.labelNode.frame = labelFrame
@ -480,22 +522,57 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonSize.width) / 2.0), y: subtitleFrame.maxY + 10.0), size: buttonSize) 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) strongSelf.buttonStarsNode.frame = CGRect(origin: .zero, size: buttonSize)
if item.controllerInteraction.presentationContext.backgroundNode?.hasExtraBubbleBackground() == true { 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) { if strongSelf.mediaBackgroundContent == nil, let backgroundContent = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) {
strongSelf.mediaBackgroundNode.isHidden = true
backgroundContent.clipsToBounds = true backgroundContent.clipsToBounds = true
backgroundContent.allowsGroupOpacity = true
backgroundContent.cornerRadius = 24.0 backgroundContent.cornerRadius = 24.0
strongSelf.mediaBackgroundContent = backgroundContent strongSelf.mediaBackgroundContent = backgroundContent
strongSelf.insertSubnode(backgroundContent, at: 0) strongSelf.insertSubnode(backgroundContent, at: 0)
} }
strongSelf.mediaBackgroundContent?.frame = mediaBackgroundFrame 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 { } else {
strongSelf.mediaBackgroundNode.isHidden = false backgroundContent.frame = mediaBackgroundFrame
strongSelf.mediaBackgroundContent?.removeFromSupernode() backgroundContent.clipsToBounds = true
strongSelf.mediaBackgroundContent = nil backgroundContent.cornerRadius = 24.0
backgroundContent.view.mask = nil
}
} }
let baseBackgroundFrame = labelFrame.offsetBy(dx: 0.0, dy: -11.0) let baseBackgroundFrame = labelFrame.offsetBy(dx: 0.0, dy: -11.0)
@ -645,7 +722,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
return ChatMessageBubbleContentTapAction(content: .ignore) return ChatMessageBubbleContentTapAction(content: .ignore)
} else if let backgroundNode = self.backgroundNode, backgroundNode.frame.contains(point) { } else if let backgroundNode = self.backgroundNode, backgroundNode.frame.contains(point) {
return ChatMessageBubbleContentTapAction(content: .openMessage) return ChatMessageBubbleContentTapAction(content: .openMessage)
} else if self.mediaBackgroundNode.frame.contains(point) { } else if self.mediaBackgroundContent?.frame.contains(point) == true {
return ChatMessageBubbleContentTapAction(content: .openMessage) return ChatMessageBubbleContentTapAction(content: .openMessage)
} else { } else {
return ChatMessageBubbleContentTapAction(content: .none) return ChatMessageBubbleContentTapAction(content: .none)

View File

@ -20,35 +20,62 @@ public final class GiftItemComponent: Component {
} }
public struct Ribbon: Equatable { public struct Ribbon: Equatable {
public let text: String public enum Color {
public let color: UIColor case red
case blue
public init(text: String, color: UIColor) { 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: Color
public init(text: String, color: Color) {
self.text = text self.text = text
self.color = color self.color = color
} }
} }
public enum Peer: Equatable {
case peer(EnginePeer)
case anonymous
}
let context: AccountContext let context: AccountContext
let theme: PresentationTheme let theme: PresentationTheme
let peer: EnginePeer? let peer: GiftItemComponent.Peer?
let subject: Subject let subject: GiftItemComponent.Subject
let title: String? let title: String?
let subtitle: String? let subtitle: String?
let price: String let price: String
let ribbon: Ribbon? let ribbon: Ribbon?
let isLoading: Bool let isLoading: Bool
let isHidden: Bool
public init( public init(
context: AccountContext, context: AccountContext,
theme: PresentationTheme, theme: PresentationTheme,
peer: EnginePeer?, peer: GiftItemComponent.Peer?,
subject: Subject, subject: GiftItemComponent.Subject,
title: String? = nil, title: String? = nil,
subtitle: String? = nil, subtitle: String? = nil,
price: String, price: String,
ribbon: Ribbon? = nil, ribbon: Ribbon? = nil,
isLoading: Bool = false isLoading: Bool = false,
isHidden: Bool = false
) { ) {
self.context = context self.context = context
self.theme = theme self.theme = theme
@ -59,6 +86,7 @@ public final class GiftItemComponent: Component {
self.price = price self.price = price
self.ribbon = ribbon self.ribbon = ribbon
self.isLoading = isLoading self.isLoading = isLoading
self.isHidden = isHidden
} }
public static func ==(lhs: GiftItemComponent, rhs: GiftItemComponent) -> Bool { public static func ==(lhs: GiftItemComponent, rhs: GiftItemComponent) -> Bool {
@ -89,6 +117,9 @@ public final class GiftItemComponent: Component {
if lhs.isLoading != rhs.isLoading { if lhs.isLoading != rhs.isLoading {
return false return false
} }
if lhs.isHidden != rhs.isHidden {
return false
}
return true return true
} }
@ -108,6 +139,9 @@ public final class GiftItemComponent: Component {
private var animationLayer: InlineStickerItemLayer? private var animationLayer: InlineStickerItemLayer?
private var hiddenIconBackground: UIVisualEffectView?
private var hiddenIcon: UIImageView?
override init(frame: CGRect) { override init(frame: CGRect) {
super.init(frame: frame) 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 { func update(component: GiftItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let isFirstTime = self.component == nil
self.component = component self.component = component
self.componentState = state self.componentState = state
@ -201,8 +237,9 @@ public final class GiftItemComponent: Component {
self.layer.addSublayer(animationLayer) 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 { 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 { if let title = component.title {
@ -287,7 +324,7 @@ public final class GiftItemComponent: Component {
ribbonTextView.bounds = CGRect(origin: .zero, size: ribbonTextSize) ribbonTextView.bounds = CGRect(origin: .zero, size: ribbonTextSize)
if self.ribbon.image == nil { 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 { 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) 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 self.avatarNode = avatarNode
} }
switch peer {
case let .peer(peer):
avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, displayDimensions: CGSize(width: 20.0, height: 20.0)) 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)) 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 self.backgroundLayer.backgroundColor = component.theme.list.itemBlocksBackgroundColor.cgColor
transition.setFrame(layer: self.backgroundLayer, frame: CGRect(origin: .zero, size: size)) 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 return size
} }
} }

View File

@ -30,15 +30,18 @@ final class GiftOptionsScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext let context: AccountContext
let starsContext: StarsContext
let peerId: EnginePeer.Id let peerId: EnginePeer.Id
let premiumOptions: [CachedPremiumGiftOption] let premiumOptions: [CachedPremiumGiftOption]
init( init(
context: AccountContext, context: AccountContext,
starsContext: StarsContext,
peerId: EnginePeer.Id, peerId: EnginePeer.Id,
premiumOptions: [CachedPremiumGiftOption] premiumOptions: [CachedPremiumGiftOption]
) { ) {
self.context = context self.context = context
self.starsContext = starsContext
self.peerId = peerId self.peerId = peerId
self.premiumOptions = premiumOptions self.premiumOptions = premiumOptions
} }
@ -100,10 +103,15 @@ final class GiftOptionsScreenComponent: Component {
private let header = ComponentView<Empty>() 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 premiumTitle = ComponentView<Empty>()
private let premiumDescription = ComponentView<Empty>() private let premiumDescription = ComponentView<Empty>()
private var premiumItems: [AnyHashable: 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 starsTitle = ComponentView<Empty>()
private let starsDescription = ComponentView<Empty>() private let starsDescription = ComponentView<Empty>()
@ -113,6 +121,9 @@ final class GiftOptionsScreenComponent: Component {
private var isUpdating: Bool = false private var isUpdating: Bool = false
private var starsStateDisposable: Disposable?
private var starsState: StarsContext.State?
private var component: GiftOptionsScreenComponent? private var component: GiftOptionsScreenComponent?
private(set) weak var state: State? private(set) weak var state: State?
private var environment: EnvironmentType? private var environment: EnvironmentType?
@ -147,6 +158,8 @@ final class GiftOptionsScreenComponent: Component {
} }
deinit { deinit {
self.starsStateDisposable?.dispose()
self.purchaseDisposable.dispose()
} }
func scrollToTop() { func scrollToTop() {
@ -205,7 +218,6 @@ final class GiftOptionsScreenComponent: Component {
transition.setScale(view: premiumTitleView, scale: premiumTitleScale) transition.setScale(view: premiumTitleView, scale: premiumTitleScale)
} }
if let headerView = self.header.view { 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.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) transition.setScale(view: headerView, scale: premiumTitleScale)
@ -273,7 +285,7 @@ final class GiftOptionsScreenComponent: Component {
ribbon: gift.availability != nil ? ribbon: gift.availability != nil ?
GiftItemComponent.Ribbon( GiftItemComponent.Ribbon(
text: "Limited", text: "Limited",
color: UIColor(rgb: 0x58c1fe) color: .blue
) )
: nil : 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 { func update(component: GiftOptionsScreenComponent, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true self.isUpdating = true
defer { defer {
@ -340,13 +434,21 @@ final class GiftOptionsScreenComponent: Component {
let controller = environment.controller let controller = environment.controller
let themeUpdated = self.environment?.theme !== environment.theme let themeUpdated = self.environment?.theme !== environment.theme
self.environment = environment self.environment = environment
self.state = state
if self.component == nil { 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.component = component
self.state = state
if themeUpdated { if themeUpdated {
self.backgroundColor = environment.theme.list.blocksBackgroundColor self.backgroundColor = environment.theme.list.blocksBackgroundColor
@ -451,6 +553,55 @@ final class GiftOptionsScreenComponent: Component {
transition.setFrame(view: cancelButtonView, frame: cancelButtonFrame) 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( let premiumTitleSize = self.premiumTitle.update(
transition: transition, transition: transition,
component: AnyComponent(MultilineTextComponent( component: AnyComponent(MultilineTextComponent(
@ -494,8 +645,13 @@ final class GiftOptionsScreenComponent: Component {
return nil 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: {}, environment: {},
@ -561,21 +717,15 @@ final class GiftOptionsScreenComponent: Component {
ribbon: product.discount.flatMap { ribbon: product.discount.flatMap {
GiftItemComponent.Ribbon( GiftItemComponent.Ribbon(
text: "-\($0)%", text: "-\($0)%",
color: UIColor(rgb: 0xfa4846) color: .red
) )
}, },
isLoading: self.selectedPremiumGift == product.id isLoading: self.inProgressPremiumGift == product.id
) )
), ),
effectAlignment: .center, effectAlignment: .center,
action: { [weak self] in action: { [weak self] in
self?.selectedPremiumGift = product.id self?.buyPremium(product)
self?.state?.updated()
Queue.mainQueue().after(4.0, {
self?.selectedPremiumGift = nil
self?.state?.updated()
})
}, },
animateAlpha: false animateAlpha: false
) )
@ -658,8 +808,13 @@ final class GiftOptionsScreenComponent: Component {
return nil 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: {}, environment: {},
@ -859,11 +1014,17 @@ final class GiftOptionsScreenComponent: Component {
public final class GiftOptionsScreen: ViewControllerComponentContainer, GiftOptionsScreenProtocol { public final class GiftOptionsScreen: ViewControllerComponentContainer, GiftOptionsScreenProtocol {
private let context: AccountContext 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 self.context = context
super.init(context: context, component: GiftOptionsScreenComponent( super.init(context: context, component: GiftOptionsScreenComponent(
context: context, context: context,
starsContext: starsContext,
peerId: peerId, peerId: peerId,
premiumOptions: premiumOptions premiumOptions: premiumOptions
), navigationBarAppearance: .none, theme: .default, updatedPresentationData: nil) ), navigationBarAppearance: .none, theme: .default, updatedPresentationData: nil)

View File

@ -148,6 +148,8 @@ final class ChatGiftPreviewItemNode: ListViewItemNode {
private let disposable = MetaDisposable() private let disposable = MetaDisposable()
private var initialBubbleHeight: CGFloat?
init() { init() {
self.topStripeNode = ASDisplayNode() self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true self.topStripeNode.isLayerBacked = true
@ -235,14 +237,13 @@ final class ChatGiftPreviewItemNode: ListViewItemNode {
}) })
itemNode!.isUserInteractionEnabled = false itemNode!.isUserInteractionEnabled = false
messageNodes.append(itemNode!) messageNodes.append(itemNode!)
self.initialBubbleHeight = itemNode?.frame.height
} }
nodes = messageNodes nodes = messageNodes
} }
var contentSize = CGSize(width: params.width, height: 4.0 + 4.0) 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 contentSize.height = 346.0
insets = itemListNeighborsGroupedInsets(neighbors, params) insets = itemListNeighborsGroupedInsets(neighbors, params)
if params.width <= 320.0 { if params.width <= 320.0 {
@ -269,7 +270,13 @@ final class ChatGiftPreviewItemNode: ListViewItemNode {
if node.supernode == nil { if node.supernode == nil {
strongSelf.containerNode.addSubnode(node) 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 //topOffset += node.frame.size.height
} }

View File

@ -90,6 +90,14 @@ final class GiftSetupScreenComponent: Component {
private var starImage: (UIImage, PresentationTheme)? 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) { override init(frame: CGRect) {
self.scrollView = ScrollView() self.scrollView = ScrollView()
self.scrollView.showsVerticalScrollIndicator = true self.scrollView.showsVerticalScrollIndicator = true
@ -159,7 +167,12 @@ final class GiftSetupScreenComponent: Component {
} }
func proceed() { 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 proceed = { [weak self] in
guard let self else {
return return
} }
let source: BotPaymentInvoiceSource = .starGift(hideName: self.hideName, peerId: component.peerId, giftId: component.gift.id, text: self.textInputState.text.string, entities: []) let source: BotPaymentInvoiceSource = .starGift(hideName: self.hideName, peerId: component.peerId, giftId: component.gift.id, text: self.textInputState.text.string, entities: [])
@ -200,6 +213,33 @@ final class GiftSetupScreenComponent: Component {
}) })
} }
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 { func update(component: GiftSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true self.isUpdating = true
defer { defer {

View File

@ -25,6 +25,7 @@ swift_library(
"//submodules/Components/ViewControllerComponent", "//submodules/Components/ViewControllerComponent",
"//submodules/Components/BundleIconComponent", "//submodules/Components/BundleIconComponent",
"//submodules/Components/MultilineTextComponent", "//submodules/Components/MultilineTextComponent",
"//submodules/Components/MultilineTextWithEntitiesComponent",
"//submodules/Components/BalancedTextComponent", "//submodules/Components/BalancedTextComponent",
"//submodules/TelegramUI/Components/ListSectionComponent", "//submodules/TelegramUI/Components/ListSectionComponent",
"//submodules/TelegramUI/Components/ListActionItemComponent", "//submodules/TelegramUI/Components/ListActionItemComponent",

View File

@ -12,6 +12,7 @@ import ComponentFlow
import ViewControllerComponent import ViewControllerComponent
import SheetComponent import SheetComponent
import MultilineTextComponent import MultilineTextComponent
import MultilineTextWithEntitiesComponent
import BundleIconComponent import BundleIconComponent
import SolidRoundedButtonComponent import SolidRoundedButtonComponent
import Markdown import Markdown
@ -32,6 +33,7 @@ private final class GiftViewSheetContent: CombinedComponent {
let openPeer: (EnginePeer) -> Void let openPeer: (EnginePeer) -> Void
let updateSavedToProfile: (Bool) -> Void let updateSavedToProfile: (Bool) -> Void
let convertToStars: () -> Void let convertToStars: () -> Void
let openStarsIntro: () -> Void
init( init(
context: AccountContext, context: AccountContext,
@ -39,7 +41,8 @@ private final class GiftViewSheetContent: CombinedComponent {
cancel: @escaping (Bool) -> Void, cancel: @escaping (Bool) -> Void,
openPeer: @escaping (EnginePeer) -> Void, openPeer: @escaping (EnginePeer) -> Void,
updateSavedToProfile: @escaping (Bool) -> Void, updateSavedToProfile: @escaping (Bool) -> Void,
convertToStars: @escaping () -> Void convertToStars: @escaping () -> Void,
openStarsIntro: @escaping () -> Void
) { ) {
self.context = context self.context = context
self.subject = subject self.subject = subject
@ -47,6 +50,7 @@ private final class GiftViewSheetContent: CombinedComponent {
self.openPeer = openPeer self.openPeer = openPeer
self.updateSavedToProfile = updateSavedToProfile self.updateSavedToProfile = updateSavedToProfile
self.convertToStars = convertToStars self.convertToStars = convertToStars
self.openStarsIntro = openStarsIntro
} }
static func ==(lhs: GiftViewSheetContent, rhs: GiftViewSheetContent) -> Bool { static func ==(lhs: GiftViewSheetContent, rhs: GiftViewSheetContent) -> Bool {
@ -176,7 +180,7 @@ private final class GiftViewSheetContent: CombinedComponent {
limitNumber = arguments.gift.availability?.remains limitNumber = arguments.gift.availability?.remains
limitTotal = arguments.gift.availability?.total limitTotal = arguments.gift.availability?.total
convertStars = arguments.convertStars convertStars = arguments.convertStars
incoming = arguments.incoming incoming = arguments.incoming || arguments.peerId == component.context.account.peerId
savedToProfile = arguments.savedToProfile savedToProfile = arguments.savedToProfile
converted = arguments.converted converted = arguments.converted
} else { } else {
@ -259,7 +263,13 @@ private final class GiftViewSheetContent: CombinedComponent {
) )
let tableFont = Font.regular(15.0) let tableFont = Font.regular(15.0)
let tableBoldFont = Font.semibold(15.0)
let tableItalicFont = Font.italic(15.0)
let tableBoldItalicFont = Font.semiboldItalic(15.0)
let tableMonospaceFont = Font.monospace(15.0)
let tableTextColor = theme.list.itemPrimaryTextColor let tableTextColor = theme.list.itemPrimaryTextColor
let tableLinkColor = theme.list.itemAccentColor
var tableItems: [TableComponent.Item] = [] var tableItems: [TableComponent.Item] = []
if let peerId = component.subject.arguments?.peerId, let peer = state.peerMap[peerId] { 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( tableItems.append(.init(
@ -312,11 +334,19 @@ private final class GiftViewSheetContent: CombinedComponent {
} }
if let text { 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( tableItems.append(.init(
id: "text", id: "text",
title: nil, title: nil,
component: AnyComponent( 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 textFont = Font.regular(15.0)
// let boldTextFont = Font.semibold(15.0)
// let textColor = theme.actionSheet.secondaryTextColor
let linkColor = theme.actionSheet.controlAccentColor 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 context.add(title
.position(CGPoint(x: context.availableSize.width / 2.0, y: 177.0)) .position(CGPoint(x: context.availableSize.width / 2.0, y: 177.0))
@ -417,7 +414,7 @@ private final class GiftViewSheetContent: CombinedComponent {
} }
}, },
tapAction: { _, _ in tapAction: { _, _ in
component.openStarsIntro()
} }
), ),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude),
@ -468,30 +465,6 @@ private final class GiftViewSheetContent: CombinedComponent {
) )
originY += table.size.height + 23.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 { if incoming && !converted {
let button = button.update( let button = button.update(
component: SolidRoundedButtonComponent( component: SolidRoundedButtonComponent(
@ -545,6 +518,33 @@ private final class GiftViewSheetContent: CombinedComponent {
.position(CGPoint(x: secondaryButtonFrame.midX, y: secondaryButtonFrame.midY)) .position(CGPoint(x: secondaryButtonFrame.midX, y: secondaryButtonFrame.midY))
) )
originY += secondaryButton.size.height 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 context.add(closeButton
@ -566,19 +566,22 @@ private final class GiftViewSheetComponent: CombinedComponent {
let openPeer: (EnginePeer) -> Void let openPeer: (EnginePeer) -> Void
let updateSavedToProfile: (Bool) -> Void let updateSavedToProfile: (Bool) -> Void
let convertToStars: () -> Void let convertToStars: () -> Void
let openStarsIntro: () -> Void
init( init(
context: AccountContext, context: AccountContext,
subject: GiftViewScreen.Subject, subject: GiftViewScreen.Subject,
openPeer: @escaping (EnginePeer) -> Void, openPeer: @escaping (EnginePeer) -> Void,
updateSavedToProfile: @escaping (Bool) -> Void, updateSavedToProfile: @escaping (Bool) -> Void,
convertToStars: @escaping () -> Void convertToStars: @escaping () -> Void,
openStarsIntro: @escaping () -> Void
) { ) {
self.context = context self.context = context
self.subject = subject self.subject = subject
self.openPeer = openPeer self.openPeer = openPeer
self.updateSavedToProfile = updateSavedToProfile self.updateSavedToProfile = updateSavedToProfile
self.convertToStars = convertToStars self.convertToStars = convertToStars
self.openStarsIntro = openStarsIntro
} }
static func ==(lhs: GiftViewSheetComponent, rhs: GiftViewSheetComponent) -> Bool { static func ==(lhs: GiftViewSheetComponent, rhs: GiftViewSheetComponent) -> Bool {
@ -620,7 +623,8 @@ private final class GiftViewSheetComponent: CombinedComponent {
}, },
openPeer: context.component.openPeer, openPeer: context.component.openPeer,
updateSavedToProfile: context.component.updateSavedToProfile, updateSavedToProfile: context.component.updateSavedToProfile,
convertToStars: context.component.convertToStars convertToStars: context.component.convertToStars,
openStarsIntro: context.component.openStarsIntro
)), )),
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
followContentSizeChanges: true, 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) return (message.id.peerId, message.id, message.flags.contains(.Incoming), gift, convertStars, text, entities, nameHidden, savedToProfile, converted)
} }
case let .profileGift(peerId, gift): 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 return nil
} }
@ -712,13 +716,17 @@ public class GiftViewScreen: ViewControllerComponentContainer {
public init( public init(
context: AccountContext, context: AccountContext,
subject: GiftViewScreen.Subject, subject: GiftViewScreen.Subject,
forceDark: Bool = false forceDark: Bool = false,
updateSavedToProfile: ((Bool) -> Void)? = nil,
convertToStars: (() -> Void)? = nil
) { ) {
self.context = context self.context = context
var openPeerImpl: ((EnginePeer) -> Void)? var openPeerImpl: ((EnginePeer) -> Void)?
var updateSavedToProfileImpl: ((Bool) -> Void)? var updateSavedToProfileImpl: ((Bool) -> Void)?
var convertToStarsImpl: (() -> Void)? var convertToStarsImpl: (() -> Void)?
var openStarsIntroImpl: (() -> Void)?
super.init( super.init(
context: context, context: context,
component: GiftViewSheetComponent( component: GiftViewSheetComponent(
@ -732,6 +740,9 @@ public class GiftViewScreen: ViewControllerComponentContainer {
}, },
convertToStars: { convertToStars: {
convertToStarsImpl?() convertToStarsImpl?()
},
openStarsIntro: {
openStarsIntroImpl?()
} }
), ),
navigationBarAppearance: .none, navigationBarAppearance: .none,
@ -764,8 +775,12 @@ public class GiftViewScreen: ViewControllerComponentContainer {
guard let self, let arguments = subject.arguments, let messageId = arguments.messageId else { guard let self, let arguments = subject.arguments, let messageId = arguments.messageId else {
return return
} }
if let updateSavedToProfile {
updateSavedToProfile(added)
} else {
let _ = (context.engine.payments.updateStarGiftAddedToProfile(messageId: messageId, added: added) let _ = (context.engine.payments.updateStarGiftAddedToProfile(messageId: messageId, added: added)
|> deliverOnMainQueue).startStandalone() |> deliverOnMainQueue).startStandalone()
}
self.dismissAnimated() self.dismissAnimated()
@ -774,7 +789,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
if let lastController = navigationController.viewControllers.last as? ViewController { if let lastController = navigationController.viewControllers.last as? ViewController {
let resultController = UndoOverlayController( let resultController = UndoOverlayController(
presentationData: presentationData, 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, elevatedLayout: lastController is ChatController,
action: { _ in return true} action: { _ in return true}
) )
@ -795,9 +810,12 @@ public class GiftViewScreen: ViewControllerComponentContainer {
actions: [ actions: [
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}),
TextAlertAction(type: .defaultAction, title: "Convert", action: { [weak self, weak navigationController] in TextAlertAction(type: .defaultAction, title: "Convert", action: { [weak self, weak navigationController] in
if let convertToStars {
convertToStars()
} else {
let _ = (context.engine.payments.convertStarGift(messageId: messageId) let _ = (context.engine.payments.convertStarGift(messageId: messageId)
|> deliverOnMainQueue).startStandalone() |> deliverOnMainQueue).startStandalone()
}
self?.dismissAnimated() self?.dismissAnimated()
if let navigationController { if let navigationController {
@ -827,6 +845,14 @@ public class GiftViewScreen: ViewControllerComponentContainer {
) )
self.present(controller, in: .window(.root)) 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) { required public init(coder aDecoder: NSCoder) {
@ -1130,14 +1156,18 @@ private final class PeerCellComponent: Component {
} }
final class View: UIView { final class View: UIView {
private let avatar = ComponentView<Empty>() private let avatarNode: AvatarNode
private let text = ComponentView<Empty>() private let text = ComponentView<Empty>()
private var component: PeerCellComponent? private var component: PeerCellComponent?
private weak var state: EmptyComponentState? private weak var state: EmptyComponentState?
override init(frame: CGRect) { override init(frame: CGRect) {
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 8.0))
super.init(frame: frame) super.init(frame: frame)
self.addSubnode(self.avatarNode)
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
@ -1152,29 +1182,25 @@ private final class PeerCellComponent: Component {
let spacing: CGFloat = 6.0 let spacing: CGFloat = 6.0
let peerName: String let peerName: String
let peer: StarsContext.State.Transaction.Peer let avatarOverride: AvatarNodeImageOverride?
if let peerValue = component.peer { if let peerValue = component.peer {
peerName = peerValue.compactDisplayTitle peerName = peerValue.compactDisplayTitle
peer = .peer(peerValue) avatarOverride = nil
} else { } else {
//TODO:localize
peerName = "Hidden Name" peerName = "Hidden Name"
peer = .fragment avatarOverride = .anonymousSavedMessagesIcon(isColored: true)
} }
let avatarNaturalSize = self.avatar.update( let avatarNaturalSize = CGSize(width: 40.0, height: 40.0)
transition: .immediate, self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: component.peer, overrideImage: avatarOverride)
component: AnyComponent( self.avatarNode.bounds = CGRect(origin: .zero, size: avatarNaturalSize)
StarsAvatarComponent(context: component.context, theme: component.theme, peer: peer, photo: nil, media: [], backgroundColor: .clear)
),
environment: {},
containerSize: CGSize(width: 40.0, height: 40.0)
)
let textSize = self.text.update( let textSize = self.text.update(
transition: .immediate, transition: .immediate,
component: AnyComponent( component: AnyComponent(
MultilineTextComponent( 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: {}, environment: {},
@ -1184,15 +1210,7 @@ private final class PeerCellComponent: Component {
let size = CGSize(width: avatarSize.width + textSize.width + spacing, height: textSize.height) let size = CGSize(width: avatarSize.width + textSize.width + spacing, height: textSize.height)
let avatarFrame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((size.height - avatarSize.height) / 2.0)), size: avatarSize) let avatarFrame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((size.height - avatarSize.height) / 2.0)), size: avatarSize)
self.avatarNode.frame = avatarFrame
if let view = self.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
}
if let view = self.text.view { if let view = self.text.view {
if view.superview == nil { if view.superview == nil {

View File

@ -100,7 +100,6 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode
private let listNode: ListView private let listNode: ListView
private var currentEntries: [RecommendedChannelsListEntry] = [] private var currentEntries: [RecommendedChannelsListEntry] = []
private var currentState: (RecommendedChannels?, Bool)? private var currentState: (RecommendedChannels?, Bool)?
private var canLoadMore: Bool = false
private var enqueuedTransactions: [RecommendedChannelsListTransaction] = [] private var enqueuedTransactions: [RecommendedChannelsListTransaction] = []
private var unlockBackground: UIImageView? private var unlockBackground: UIImageView?

View File

@ -51,6 +51,7 @@ swift_library(
"//submodules/TelegramUI/Components/Gifts/GiftItemComponent", "//submodules/TelegramUI/Components/Gifts/GiftItemComponent",
"//submodules/TelegramUI/Components/Gifts/GiftViewScreen", "//submodules/TelegramUI/Components/Gifts/GiftViewScreen",
"//submodules/TelegramUI/Components/ButtonComponent", "//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/Components/BalancedTextComponent",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -15,12 +15,13 @@ import MergeLists
import ItemListUI import ItemListUI
import ChatControllerInteraction import ChatControllerInteraction
import MultilineTextComponent import MultilineTextComponent
import BalancedTextComponent
import Markdown import Markdown
import PeerInfoPaneNode import PeerInfoPaneNode
import GiftItemComponent import GiftItemComponent
import PlainButtonComponent import PlainButtonComponent
import GiftViewScreen import GiftViewScreen
import ButtonComponent import SolidRoundedButtonNode
public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScrollViewDelegate { public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScrollViewDelegate {
private let context: AccountContext private let context: AccountContext
@ -37,6 +38,10 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
private let backgroundNode: ASDisplayNode private let backgroundNode: ASDisplayNode
private let scrollNode: ASScrollNode 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 currentParams: (size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData)?
private var theme: PresentationTheme? private var theme: PresentationTheme?
@ -82,7 +87,8 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
guard let self else { guard let self else {
return 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 self.starsProducts = state.gifts
if !self.didSetReady { if !self.didSetReady {
@ -149,6 +155,13 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
} }
if isVisible { 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( let _ = visibleItem.update(
transition: itemTransition, transition: itemTransition,
component: AnyComponent( component: AnyComponent(
@ -157,26 +170,36 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
GiftItemComponent( GiftItemComponent(
context: self.context, context: self.context,
theme: params.presentationData.theme, theme: params.presentationData.theme,
peer: product.fromPeer, peer: product.fromPeer.flatMap { .peer($0) } ?? .anonymous,
subject: .starGift(product.gift.id, product.gift.file), subject: .starGift(product.gift.id, product.gift.file),
price: "⭐️ \(product.gift.price)", price: "⭐️ \(product.gift.price)",
ribbon: product.gift.availability != nil ? ribbon: ribbonText.flatMap { GiftItemComponent.Ribbon(text: $0, color: .blue) },
GiftItemComponent.Ribbon( isHidden: !product.savedToProfile
text: "1 of 1K",
color: UIColor(rgb: 0x58c1fe)
)
: nil
) )
), ),
effectAlignment: .center, effectAlignment: .center,
action: { [weak self] in action: { [weak self] in
if let self { guard let self else {
return
}
let controller = GiftViewScreen( let controller = GiftViewScreen(
context: self.context, context: self.context,
subject: .profileGift(self.peerId, product) 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) self.parentController?.push(controller)
}
}, },
animateAlpha: false 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 if self.peerId == self.context.account.peerId {
// let buttonSize = self.button.update( let transition = ComponentTransition.immediate
// 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 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) let contentSize = CGSize(width: params.size.width, height: contentHeight)
if self.scrollNode.view.contentSize != contentSize { if self.scrollNode.view.contentSize != contentSize {
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) { 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) {

View File

@ -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",
],
)

View File

@ -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)
}
}
}

View File

@ -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 textString = renew ? strings.Stars_Purchase_SubscriptionRenewInfo(component.peers.first?.value.compactDisplayTitle ?? "").string : strings.Stars_Purchase_SubscriptionInfo(component.peers.first?.value.compactDisplayTitle ?? "").string
case .unlockMedia: case .unlockMedia:
textString = strings.Stars_Purchase_StarsNeededUnlockInfo 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 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 { switch context.component.purpose {
case .generic: case .generic:
titleText = strings.Stars_Purchase_GetStars titleText = strings.Stars_Purchase_GetStars
case let .topUp(requiredStars, _):
titleText = strings.Stars_Purchase_StarsNeeded(Int32(requiredStars))
case .gift: case .gift:
titleText = strings.Stars_Purchase_GiftStars 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)) titleText = strings.Stars_Purchase_StarsNeeded(Int32(requiredStars))
} }
@ -1239,6 +1239,8 @@ private extension StarsPurchasePurpose {
return [peerId] return [peerId]
case let .subscription(peerId, _, _): case let .subscription(peerId, _, _):
return [peerId] return [peerId]
case let .starGift(peerId, _):
return [peerId]
default: default:
return [] return []
} }
@ -1256,6 +1258,8 @@ private extension StarsPurchasePurpose {
return requiredStars return requiredStars
case let .unlockMedia(requiredStars): case let .unlockMedia(requiredStars):
return requiredStars return requiredStars
case let .starGift(_, requiredStars):
return requiredStars
default: default:
return nil return nil
} }

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "hidden_30.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,9 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"provides-namespace" : true
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "gift_30 (4).pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "unlock_30.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "bot_30.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "cash_30.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,62 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Filter /FlateDecode
/Length 3 0 R
>>
stream
xe•KŽT1 EçYE<59>‰óõ<14>KxjRwK¨%ÖÏÉç%ÔÈïĹvl'õáóÓŸ_×Ó·/Ÿ¾›ûëz3¿<33>·®ÿî6Þ/k­EBÔì·q½˜åö¿ñv½"6*Îæœ\©<>&[Åå”É&§Qä!c;~sn]flIØN ϱÚ6tð»Ì¦=`r~ni#­x-t™¾¡e¸Ø0ðêK]ë&S§Ëü4ó þb¢-%Ö¸²óÙ¦”¼¦ ¼®]ô®Ñ:Ñ<>&6J¾ýqZˆê ]_6œÑ<C593> <0C>&¶ÑéA:TÈnZèÍU<çoÖ«À—D/”)[õ}³°i±R%Ôò€9ÉA9t±©:ïÇqN¿É8<C389>³¢¾¦“ªUýGp£ù2&¬\Š<jcФß`õ¥’;*IK$…Øûìã<C3AC>Ī0p\{*ÏôÖ‡Ø\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
hp!êºæ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

View File

@ -72,6 +72,7 @@ import StarsWithdrawalScreen
import MiniAppListScreen import MiniAppListScreen
import GiftOptionsScreen import GiftOptionsScreen
import GiftViewScreen import GiftViewScreen
import StarsIntroScreen
private final class AccountUserInterfaceInUseContext { private final class AccountUserInterfaceInUseContext {
let subscribers = Bag<(Bool) -> Void>() 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 { public func makePremiumGiftController(context: AccountContext, source: PremiumGiftSource, completion: (([EnginePeer.Id]) -> Void)?) -> ViewController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 }
// let limit: Int32 = 10
// var reachedLimitImpl: ((Int32) -> Void)?
var presentBirthdayPickerImpl: (() -> Void)? var presentBirthdayPickerImpl: (() -> Void)?
var starsMode: ContactSelectionControllerMode = .generic var starsMode: ContactSelectionControllerMode = .generic
var currentBirthdays: [EnginePeer.Id: TelegramBirthday]? var currentBirthdays: [EnginePeer.Id: TelegramBirthday]?
@ -2268,9 +2267,9 @@ public final class SharedAccountContextImpl: SharedAccountContext {
)) ))
let _ = combineLatest(queue: Queue.mainQueue(), contactsController.result, options.get()) let _ = combineLatest(queue: Queue.mainQueue(), contactsController.result, options.get())
.startStandalone(next: { [weak contactsController] result, options in .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 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 giftController.navigationPresentation = .modal
contactsController?.push(giftController) contactsController?.push(giftController)
@ -2812,6 +2811,10 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return StarsTransactionScreen(context: context, subject: .boost(peerId, boost)) 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 { public func makeGiftViewScreen(context: AccountContext, message: EngineMessage) -> ViewController {
return GiftViewScreen(context: context, subject: .message(message)) return GiftViewScreen(context: context, subject: .message(message))
} }