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.";
"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 makeStarsGiftScreen(context: AccountContext, message: EngineMessage) -> ViewController
func makeStarsGiveawayBoostScreen(context: AccountContext, peerId: EnginePeer.Id, boost: ChannelBoostersContext.State.Boost) -> ViewController
func makeStarsIntroScreen(context: AccountContext) -> ViewController
func makeGiftViewScreen(context: AccountContext, message: EngineMessage) -> ViewController
func makeMiniAppListScreenInitialData(context: AccountContext) -> Signal<MiniAppListScreenInitialData, NoError>

View File

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

View File

@ -238,6 +238,7 @@ private final class ProfileGiftsContextImpl {
private let peerId: PeerId
private let disposable = MetaDisposable()
private let actionDisposable = MetaDisposable()
private var gifts: [ProfileGiftsContext.State.StarGift] = []
private var count: Int32?
@ -258,6 +259,7 @@ private final class ProfileGiftsContextImpl {
deinit {
self.disposable.dispose()
self.actionDisposable.dispose()
}
func loadMore() {
@ -315,6 +317,27 @@ private final class ProfileGiftsContextImpl {
}
}
func updateStarGiftAddedToProfile(messageId: EngineMessage.Id, added: Bool) {
self.actionDisposable.set(
_internal_updateStarGiftAddedToProfile(account: self.account, messageId: messageId, added: added).startStrict()
)
if let index = self.gifts.firstIndex(where: { $0.messageId == messageId }) {
self.gifts[index] = self.gifts[index].withSavedToProfile(added)
}
self.pushState()
}
func convertStarGift(messageId: EngineMessage.Id) {
self.actionDisposable.set(
_internal_convertStarGift(account: self.account, messageId: messageId).startStrict()
)
if let count = self.count {
self.count = max(0, count - 1)
}
self.gifts.removeAll(where: { $0.messageId == messageId })
self.pushState()
}
private func pushState() {
self.stateValue.set(.single(ProfileGiftsContext.State(gifts: self.gifts, count: self.count, dataState: self.dataState)))
}
@ -332,6 +355,20 @@ public final class ProfileGiftsContext {
public let nameHidden: Bool
public let savedToProfile: Bool
public let convertStars: Int64?
public func withSavedToProfile(_ savedToProfile: Bool) -> StarGift {
return StarGift(
gift: self.gift,
fromPeer: self.fromPeer,
date: self.date,
text: self.text,
entities: self.entities,
messageId: self.messageId,
nameHidden: self.nameHidden,
savedToProfile: savedToProfile,
convertStars: self.convertStars
)
}
}
public enum DataState: Equatable {
@ -373,6 +410,18 @@ public final class ProfileGiftsContext {
impl.loadMore()
}
}
public func updateStarGiftAddedToProfile(messageId: EngineMessage.Id, added: Bool) {
self.impl.with { impl in
impl.updateStarGiftAddedToProfile(messageId: messageId, added: added)
}
}
public func convertStarGift(messageId: EngineMessage.Id) {
self.impl.with { impl in
impl.convertStarGift(messageId: messageId)
}
}
}
private extension ProfileGiftsContext.State.StarGift {

View File

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

View File

@ -31,13 +31,16 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
private let backgroundMaskNode: ASImageNode
private var linkHighlightingNode: LinkHighlightingNode?
private let mediaBackgroundMaskNode: ASImageNode
private var mediaBackgroundContent: WallpaperBubbleBackgroundNode?
private let mediaBackgroundNode: NavigationBackgroundNode
private let titleNode: TextNode
private let subtitleNode: TextNode
private let placeholderNode: StickerShimmerEffectNode
private let animationNode: AnimatedStickerNode
private let ribbonBackgroundNode: ASImageNode
private let ribbonTextNode: TextNode
private var shimmerEffectNode: ShimmerEffectForegroundNode?
private let buttonNode: HighlightTrackingButtonNode
private let buttonStarsNode: PremiumStarsNode
@ -79,9 +82,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
self.backgroundMaskNode = ASImageNode()
self.mediaBackgroundNode = NavigationBackgroundNode(color: .clear)
self.mediaBackgroundNode.clipsToBounds = true
self.mediaBackgroundNode.cornerRadius = 24.0
self.mediaBackgroundMaskNode = ASImageNode()
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
@ -107,19 +108,30 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
self.buttonTitleNode.isUserInteractionEnabled = false
self.buttonTitleNode.displaysAsynchronously = false
self.ribbonBackgroundNode = ASImageNode()
self.ribbonBackgroundNode.displaysAsynchronously = false
self.ribbonTextNode = TextNode()
self.ribbonTextNode.isUserInteractionEnabled = false
self.ribbonTextNode.displaysAsynchronously = false
super.init()
self.addSubnode(self.labelNode)
self.addSubnode(self.mediaBackgroundNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.subtitleNode)
self.addSubnode(self.subtitleNode)
self.addSubnode(self.placeholderNode)
self.addSubnode(self.animationNode)
self.addSubnode(self.buttonNode)
self.buttonNode.addSubnode(self.buttonStarsNode)
self.addSubnode(self.buttonTitleNode)
self.addSubnode(self.ribbonBackgroundNode)
self.addSubnode(self.ribbonTextNode)
self.buttonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
@ -226,6 +238,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode)
let makeButtonTitleLayout = TextNode.asyncLayout(self.buttonTitleNode)
let makeRibbonTextLayout = TextNode.asyncLayout(self.ribbonTextNode)
let cachedMaskBackgroundImage = self.cachedMaskBackgroundImage
@ -247,6 +260,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
var title = item.presentationData.strings.Notification_PremiumGift_Title
var text = ""
var buttonTitle = item.presentationData.strings.Notification_PremiumGift_View
var ribbonTitle = ""
var hasServiceMessage = true
var textSpacing: CGFloat = 0.0
for media in item.message.media {
@ -315,8 +329,9 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
buttonTitle = item.presentationData.strings.Notification_PremiumPrize_View
hasServiceMessage = false
}
case let .starGift(gift, convertStars, giftText, entities, nameHidden, savedToProfile, converted)://(amount, giftId, nameHidden, limitNumber, limitTotal, giftText, _):
case let .starGift(gift, convertStars, giftText, entities, nameHidden, savedToProfile, converted):
let _ = nameHidden
//TODO:localize
let authorName = item.message.author.flatMap { EnginePeer($0) }?.compactDisplayTitle ?? ""
title = "Gift from \(authorName)"
if let giftText, !giftText.isEmpty {
@ -344,6 +359,9 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
}
}
animationFile = gift.file
if let availability = gift.availability {
ribbonTitle = "1 of \(availability.total)"
}
default:
break
}
@ -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 (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
var labelRects = labelLayout.linesRects()
@ -424,7 +444,35 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
return (backgroundSize.width, { boundingWidth in
return (backgroundSize, { [weak self] animation, synchronousLoads, _ in
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 {
strongSelf.animationNode.started = { [weak self] in
if let strongSelf = self {
let current = CACurrentMediaTime()
if let setupTimestamp = strongSelf.setupTimestamp, current - setupTimestamp > 0.3 {
if !strongSelf.placeholderNode.alpha.isZero {
strongSelf.animationNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
strongSelf.removePlaceholder(animated: true)
}
} else {
strongSelf.removePlaceholder(animated: false)
}
}
}
strongSelf.animationNode.autoplay = true
if let file = animationFile {
@ -432,6 +480,11 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
if strongSelf.fetchDisposable == nil {
strongSelf.fetchDisposable = freeMediaFileResourceInteractiveFetched(postbox: item.context.account.postbox, userLocation: .other, fileReference: .message(message: MessageReference(item.message), media: file), resource: file.resource).start()
}
if let immediateThumbnailData = file.immediateThumbnailData {
let shimmeringColor = bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.stickerPlaceholderShimmerColor, wallpaper: item.presentationData.theme.wallpaper)
strongSelf.placeholderNode.update(backgroundColor: nil, foregroundColor: overlayColor, shimmeringColor: shimmeringColor, data: immediateThumbnailData, size: animationFrame.size, enableEffect: item.context.sharedContext.energyUsageSettings.fullTranslucency)
}
} else if animationName.hasPrefix("Gift") {
strongSelf.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: animationName), width: 384, height: 384, playbackMode: .still(.end), mode: .direct(cachePathPrefix: nil))
}
@ -442,27 +495,16 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
strongSelf.labelNode.isHidden = !hasServiceMessage
let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - giftSize.width) / 2.0), y: hasServiceMessage ? labelLayout.size.height + 16.0 : 0.0), size: giftSize)
let mediaBackgroundFrame = imageFrame.insetBy(dx: -2.0, dy: -2.0)
strongSelf.mediaBackgroundNode.frame = mediaBackgroundFrame
strongSelf.buttonNode.backgroundColor = overlayColor
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.placeholderNode.frame = animationFrame
let _ = labelApply()
let _ = titleApply()
let _ = subtitleApply()
let _ = buttonTitleApply()
let _ = ribbonTextApply()
let labelFrame = CGRect(origin: CGPoint(x: 8.0, y: 2.0), size: labelLayout.size)
strongSelf.labelNode.frame = labelFrame
@ -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.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) {
strongSelf.mediaBackgroundNode.isHidden = true
backgroundContent.clipsToBounds = true
backgroundContent.allowsGroupOpacity = true
backgroundContent.cornerRadius = 24.0
strongSelf.mediaBackgroundContent = backgroundContent
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 {
strongSelf.mediaBackgroundNode.isHidden = false
strongSelf.mediaBackgroundContent?.removeFromSupernode()
strongSelf.mediaBackgroundContent = nil
backgroundContent.frame = mediaBackgroundFrame
backgroundContent.clipsToBounds = true
backgroundContent.cornerRadius = 24.0
backgroundContent.view.mask = nil
}
}
let baseBackgroundFrame = labelFrame.offsetBy(dx: 0.0, dy: -11.0)
@ -645,7 +722,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
return ChatMessageBubbleContentTapAction(content: .ignore)
} else if let backgroundNode = self.backgroundNode, backgroundNode.frame.contains(point) {
return ChatMessageBubbleContentTapAction(content: .openMessage)
} else if self.mediaBackgroundNode.frame.contains(point) {
} else if self.mediaBackgroundContent?.frame.contains(point) == true {
return ChatMessageBubbleContentTapAction(content: .openMessage)
} else {
return ChatMessageBubbleContentTapAction(content: .none)

View File

@ -20,35 +20,62 @@ public final class GiftItemComponent: Component {
}
public struct Ribbon: Equatable {
public let text: String
public let color: UIColor
public enum Color {
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.color = color
}
}
public enum Peer: Equatable {
case peer(EnginePeer)
case anonymous
}
let context: AccountContext
let theme: PresentationTheme
let peer: EnginePeer?
let subject: Subject
let peer: GiftItemComponent.Peer?
let subject: GiftItemComponent.Subject
let title: String?
let subtitle: String?
let price: String
let ribbon: Ribbon?
let isLoading: Bool
let isHidden: Bool
public init(
context: AccountContext,
theme: PresentationTheme,
peer: EnginePeer?,
subject: Subject,
peer: GiftItemComponent.Peer?,
subject: GiftItemComponent.Subject,
title: String? = nil,
subtitle: String? = nil,
price: String,
ribbon: Ribbon? = nil,
isLoading: Bool = false
isLoading: Bool = false,
isHidden: Bool = false
) {
self.context = context
self.theme = theme
@ -59,6 +86,7 @@ public final class GiftItemComponent: Component {
self.price = price
self.ribbon = ribbon
self.isLoading = isLoading
self.isHidden = isHidden
}
public static func ==(lhs: GiftItemComponent, rhs: GiftItemComponent) -> Bool {
@ -89,6 +117,9 @@ public final class GiftItemComponent: Component {
if lhs.isLoading != rhs.isLoading {
return false
}
if lhs.isHidden != rhs.isHidden {
return false
}
return true
}
@ -108,6 +139,9 @@ public final class GiftItemComponent: Component {
private var animationLayer: InlineStickerItemLayer?
private var hiddenIconBackground: UIVisualEffectView?
private var hiddenIcon: UIImageView?
override init(frame: CGRect) {
super.init(frame: frame)
@ -125,6 +159,8 @@ public final class GiftItemComponent: Component {
}
func update(component: GiftItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let isFirstTime = self.component == nil
self.component = component
self.componentState = state
@ -201,8 +237,9 @@ public final class GiftItemComponent: Component {
self.layer.addSublayer(animationLayer)
}
let animationFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - iconSize.width) / 2.0), y: animationOffset), size: iconSize)
if let animationLayer = self.animationLayer {
transition.setFrame(layer: animationLayer, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - iconSize.width) / 2.0), y: animationOffset), size: iconSize))
transition.setFrame(layer: animationLayer, frame: animationFrame)
}
if let title = component.title {
@ -287,7 +324,7 @@ public final class GiftItemComponent: Component {
ribbonTextView.bounds = CGRect(origin: .zero, size: ribbonTextSize)
if self.ribbon.image == nil {
self.ribbon.image = generateGradientTintedImage(image: UIImage(bundleImageName: "Premium/GiftRibbon"), colors: [ribbon.color.withMultipliedBrightnessBy(1.1), ribbon.color.withMultipliedBrightnessBy(0.9)], direction: .diagonal)
self.ribbon.image = generateGradientTintedImage(image: UIImage(bundleImageName: "Premium/GiftRibbon"), colors: ribbon.color.colors, direction: .diagonal)
}
if let ribbonImage = self.ribbon.image {
self.ribbon.frame = CGRect(origin: CGPoint(x: size.width - ribbonImage.size.width + 2.0, y: -2.0), size: ribbonImage.size)
@ -312,13 +349,64 @@ public final class GiftItemComponent: Component {
self.avatarNode = avatarNode
}
switch peer {
case let .peer(peer):
avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, displayDimensions: CGSize(width: 20.0, height: 20.0))
case .anonymous:
avatarNode.setPeer(context: component.context, theme: component.theme, peer: nil, overrideImage: .anonymousSavedMessagesIcon(isColored: true))
}
avatarNode.frame = CGRect(origin: CGPoint(x: 2.0, y: 2.0), size: CGSize(width: 20.0, height: 20.0))
}
self.backgroundLayer.backgroundColor = component.theme.list.itemBlocksBackgroundColor.cgColor
transition.setFrame(layer: self.backgroundLayer, frame: CGRect(origin: .zero, size: size))
if component.isHidden {
let hiddenIconBackground: UIVisualEffectView
let hiddenIcon: UIImageView
if let currentBackground = self.hiddenIconBackground, let currentIcon = self.hiddenIcon {
hiddenIconBackground = currentBackground
hiddenIcon = currentIcon
} else {
let blurEffect: UIBlurEffect
if #available(iOS 13.0, *) {
blurEffect = UIBlurEffect(style: .systemThinMaterialDark)
} else {
blurEffect = UIBlurEffect(style: .dark)
}
hiddenIconBackground = UIVisualEffectView(effect: blurEffect)
hiddenIconBackground.clipsToBounds = true
hiddenIconBackground.layer.cornerRadius = 15.0
self.hiddenIconBackground = hiddenIconBackground
hiddenIcon = UIImageView(image: generateTintedImage(image: UIImage(bundleImageName: "Peer Info/HiddenIcon"), color: .white))
self.hiddenIcon = hiddenIcon
self.addSubview(hiddenIconBackground)
hiddenIconBackground.contentView.addSubview(hiddenIcon)
if !isFirstTime {
hiddenIconBackground.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2)
hiddenIconBackground.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
let iconSize = CGSize(width: 30.0, height: 30.0)
hiddenIconBackground.frame = iconSize.centered(around: animationFrame.center)
hiddenIcon.frame = CGRect(origin: .zero, size: iconSize)
} else {
if let hiddenIconBackground = self.hiddenIconBackground {
self.hiddenIconBackground = nil
self.hiddenIcon = nil
hiddenIconBackground.layer.animateAlpha(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false, completion: { _ in
hiddenIconBackground.removeFromSuperview()
})
hiddenIconBackground.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
}
}
return size
}
}

View File

@ -30,15 +30,18 @@ final class GiftOptionsScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let starsContext: StarsContext
let peerId: EnginePeer.Id
let premiumOptions: [CachedPremiumGiftOption]
init(
context: AccountContext,
starsContext: StarsContext,
peerId: EnginePeer.Id,
premiumOptions: [CachedPremiumGiftOption]
) {
self.context = context
self.starsContext = starsContext
self.peerId = peerId
self.premiumOptions = premiumOptions
}
@ -100,10 +103,15 @@ final class GiftOptionsScreenComponent: Component {
private let header = ComponentView<Empty>()
private let balanceTitle = ComponentView<Empty>()
private let balanceValue = ComponentView<Empty>()
private let balanceIcon = ComponentView<Empty>()
private let premiumTitle = ComponentView<Empty>()
private let premiumDescription = ComponentView<Empty>()
private var premiumItems: [AnyHashable: ComponentView<Empty>] = [:]
private var selectedPremiumGift: String?
private var inProgressPremiumGift: String?
private let purchaseDisposable = MetaDisposable()
private let starsTitle = ComponentView<Empty>()
private let starsDescription = ComponentView<Empty>()
@ -113,6 +121,9 @@ final class GiftOptionsScreenComponent: Component {
private var isUpdating: Bool = false
private var starsStateDisposable: Disposable?
private var starsState: StarsContext.State?
private var component: GiftOptionsScreenComponent?
private(set) weak var state: State?
private var environment: EnvironmentType?
@ -147,6 +158,8 @@ final class GiftOptionsScreenComponent: Component {
}
deinit {
self.starsStateDisposable?.dispose()
self.purchaseDisposable.dispose()
}
func scrollToTop() {
@ -205,7 +218,6 @@ final class GiftOptionsScreenComponent: Component {
transition.setScale(view: premiumTitleView, scale: premiumTitleScale)
}
if let headerView = self.header.view {
transition.setPosition(view: headerView, position: CGPoint(x: availableWidth / 2.0, y: topInset + headerView.bounds.height / 2.0 - 30.0 - premiumTitleOffset * premiumTitleScale))
transition.setScale(view: headerView, scale: premiumTitleScale)
@ -273,7 +285,7 @@ final class GiftOptionsScreenComponent: Component {
ribbon: gift.availability != nil ?
GiftItemComponent.Ribbon(
text: "Limited",
color: UIColor(rgb: 0x58c1fe)
color: .blue
)
: nil
)
@ -330,6 +342,88 @@ final class GiftOptionsScreenComponent: Component {
}
}
private func buyPremium(_ product: PremiumGiftProduct) {
guard let component = self.component, let inAppPurchaseManager = self.component?.context.inAppPurchaseManager, self.inProgressPremiumGift == nil else {
return
}
self.inProgressPremiumGift = product.id
self.state?.updated()
let (currency, amount) = product.storeProduct.priceCurrencyAndAmount
addAppLogEvent(postbox: component.context.account.postbox, type: "premium_gift.promo_screen_accept")
let purpose: AppStoreTransactionPurpose = .giftCode(peerIds: [component.peerId], boostPeer: nil, currency: currency, amount: amount)
let quantity: Int32 = 1
let _ = (component.context.engine.payments.canPurchasePremium(purpose: purpose)
|> deliverOnMainQueue).start(next: { [weak self] available in
if let strongSelf = self {
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
if available {
strongSelf.purchaseDisposable.set((inAppPurchaseManager.buyProduct(product.storeProduct, quantity: quantity, purpose: purpose)
|> deliverOnMainQueue).start(next: { [weak self] status in
guard let self, case .purchased = status, let controller = self.environment?.controller(), let navigationController = controller.navigationController as? NavigationController else {
return
}
var controllers = navigationController.viewControllers
controllers = controllers.filter { !($0 is GiftOptionsScreen) }
var foundController = false
for controller in controllers.reversed() {
if let chatController = controller as? ChatController, case .peer(id: component.peerId) = chatController.chatLocation {
chatController.hintPlayNextOutgoingGift()
foundController = true
break
}
}
if !foundController {
let chatController = component.context.sharedContext.makeChatController(context: component.context, chatLocation: .peer(id: component.peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil)
chatController.hintPlayNextOutgoingGift()
controllers.append(chatController)
}
navigationController.setViewControllers(controllers, animated: true)
}, error: { [weak self] error in
guard let self, let controller = self.environment?.controller() else {
return
}
self.inProgressPremiumGift = nil
self.state?.updated(transition: .immediate)
var errorText: String?
switch error {
case .generic:
errorText = presentationData.strings.Premium_Purchase_ErrorUnknown
case .network:
errorText = presentationData.strings.Premium_Purchase_ErrorNetwork
case .notAllowed:
errorText = presentationData.strings.Premium_Purchase_ErrorNotAllowed
case .cantMakePayments:
errorText = presentationData.strings.Premium_Purchase_ErrorCantMakePayments
case .assignFailed:
errorText = presentationData.strings.Premium_Purchase_ErrorUnknown
case .tryLater:
errorText = presentationData.strings.Premium_Purchase_ErrorUnknown
case .cancelled:
break
}
if let errorText {
addAppLogEvent(postbox: component.context.account.postbox, type: "premium_gift.promo_screen_fail")
let alertController = textAlertController(context: component.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])
controller.present(alertController, in: .window(.root))
}
}))
} else {
self?.inProgressPremiumGift = nil
self?.state?.updated(transition: .immediate)
}
}
})
}
func update(component: GiftOptionsScreenComponent, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
@ -340,13 +434,21 @@ final class GiftOptionsScreenComponent: Component {
let controller = environment.controller
let themeUpdated = self.environment?.theme !== environment.theme
self.environment = environment
self.state = state
if self.component == nil {
self.starsStateDisposable = (component.starsContext.state
|> deliverOnMainQueue).start(next: { [weak self] state in
guard let self else {
return
}
self.starsState = state
if !self.isUpdating {
self.state?.updated()
}
})
}
self.component = component
self.state = state
if themeUpdated {
self.backgroundColor = environment.theme.list.blocksBackgroundColor
@ -451,6 +553,55 @@ final class GiftOptionsScreenComponent: Component {
transition.setFrame(view: cancelButtonView, frame: cancelButtonFrame)
}
let balanceTitleSize = self.balanceTitle.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: strings.Stars_Purchase_Balance,
font: Font.regular(14.0),
textColor: environment.theme.actionSheet.primaryTextColor
)),
maximumNumberOfLines: 1
)),
environment: {},
containerSize: availableSize
)
let balanceValueSize = self.balanceValue.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: presentationStringsFormattedNumber(Int32(self.starsState?.balance ?? 0), environment.dateTimeFormat.groupingSeparator),
font: Font.semibold(14.0),
textColor: environment.theme.actionSheet.primaryTextColor
)),
maximumNumberOfLines: 1
)),
environment: {},
containerSize: availableSize
)
let balanceIconSize = self.balanceIcon.update(
transition: .immediate,
component: AnyComponent(BundleIconComponent(name: "Premium/Stars/StarSmall", tintColor: nil)),
environment: {},
containerSize: availableSize
)
if let balanceTitleView = self.balanceTitle.view, let balanceValueView = self.balanceValue.view, let balanceIconView = self.balanceIcon.view {
if balanceTitleView.superview == nil {
self.addSubview(balanceTitleView)
self.addSubview(balanceValueView)
self.addSubview(balanceIconView)
}
let navigationHeight = environment.navigationHeight - environment.statusBarHeight
let topBalanceOriginY = environment.statusBarHeight + (navigationHeight - balanceTitleSize.height - balanceValueSize.height) / 2.0
balanceTitleView.center = CGPoint(x: availableSize.width - 16.0 - environment.safeInsets.right - balanceTitleSize.width / 2.0, y: topBalanceOriginY + balanceTitleSize.height / 2.0)
balanceTitleView.bounds = CGRect(origin: .zero, size: balanceTitleSize)
balanceValueView.center = CGPoint(x: availableSize.width - 16.0 - environment.safeInsets.right - balanceValueSize.width / 2.0, y: topBalanceOriginY + balanceTitleSize.height + balanceValueSize.height / 2.0)
balanceValueView.bounds = CGRect(origin: .zero, size: balanceValueSize)
balanceIconView.center = CGPoint(x: availableSize.width - 16.0 - environment.safeInsets.right - balanceValueSize.width - balanceIconSize.width / 2.0 - 2.0, y: topBalanceOriginY + balanceTitleSize.height + balanceValueSize.height / 2.0 - UIScreenPixel)
balanceIconView.bounds = CGRect(origin: .zero, size: balanceIconSize)
}
let premiumTitleSize = self.premiumTitle.update(
transition: transition,
component: AnyComponent(MultilineTextComponent(
@ -494,8 +645,13 @@ final class GiftOptionsScreenComponent: Component {
return nil
}
},
tapAction: { _, _ in
tapAction: { [weak self] _, _ in
guard let self, let component = self.component, let environment = self.environment else {
return
}
let introController = component.context.sharedContext.makePremiumIntroController(context: component.context, source: .settings, forceDark: false, dismissed: nil)
introController.navigationPresentation = .modal
environment.controller()?.push(introController)
}
)),
environment: {},
@ -561,21 +717,15 @@ final class GiftOptionsScreenComponent: Component {
ribbon: product.discount.flatMap {
GiftItemComponent.Ribbon(
text: "-\($0)%",
color: UIColor(rgb: 0xfa4846)
color: .red
)
},
isLoading: self.selectedPremiumGift == product.id
isLoading: self.inProgressPremiumGift == product.id
)
),
effectAlignment: .center,
action: { [weak self] in
self?.selectedPremiumGift = product.id
self?.state?.updated()
Queue.mainQueue().after(4.0, {
self?.selectedPremiumGift = nil
self?.state?.updated()
})
self?.buyPremium(product)
},
animateAlpha: false
)
@ -658,8 +808,13 @@ final class GiftOptionsScreenComponent: Component {
return nil
}
},
tapAction: { _, _ in
tapAction: { [weak self] _, _ in
guard let self, let component = self.component, let environment = self.environment else {
return
}
let introController = component.context.sharedContext.makeStarsIntroScreen(context: component.context)
introController.navigationPresentation = .modal
environment.controller()?.push(introController)
}
)),
environment: {},
@ -859,11 +1014,17 @@ final class GiftOptionsScreenComponent: Component {
public final class GiftOptionsScreen: ViewControllerComponentContainer, GiftOptionsScreenProtocol {
private let context: AccountContext
public init(context: AccountContext, peerId: EnginePeer.Id, premiumOptions: [CachedPremiumGiftOption]) {
public init(
context: AccountContext,
starsContext: StarsContext,
peerId: EnginePeer.Id,
premiumOptions: [CachedPremiumGiftOption]
) {
self.context = context
super.init(context: context, component: GiftOptionsScreenComponent(
context: context,
starsContext: starsContext,
peerId: peerId,
premiumOptions: premiumOptions
), navigationBarAppearance: .none, theme: .default, updatedPresentationData: nil)

View File

@ -148,6 +148,8 @@ final class ChatGiftPreviewItemNode: ListViewItemNode {
private let disposable = MetaDisposable()
private var initialBubbleHeight: CGFloat?
init() {
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
@ -235,14 +237,13 @@ final class ChatGiftPreviewItemNode: ListViewItemNode {
})
itemNode!.isUserInteractionEnabled = false
messageNodes.append(itemNode!)
self.initialBubbleHeight = itemNode?.frame.height
}
nodes = messageNodes
}
var contentSize = CGSize(width: params.width, height: 4.0 + 4.0)
// for node in nodes {
// contentSize.height += node.frame.size.height
// }
contentSize.height = 346.0
insets = itemListNeighborsGroupedInsets(neighbors, params)
if params.width <= 320.0 {
@ -269,7 +270,13 @@ final class ChatGiftPreviewItemNode: ListViewItemNode {
if node.supernode == nil {
strongSelf.containerNode.addSubnode(node)
}
node.updateFrame(CGRect(origin: CGPoint(x: 0.0, y: floor((contentSize.height - node.frame.size.height) / 2.0)), size: node.frame.size), within: layoutSize)
let bubbleHeight: CGFloat
if let initialBubbleHeight = strongSelf.initialBubbleHeight {
bubbleHeight = max(node.frame.height, initialBubbleHeight)
} else {
bubbleHeight = node.frame.height
}
node.updateFrame(CGRect(origin: CGPoint(x: 0.0, y: floor((contentSize.height - bubbleHeight) / 2.0)), size: node.frame.size), within: layoutSize)
//topOffset += node.frame.size.height
}

View File

@ -90,6 +90,14 @@ final class GiftSetupScreenComponent: Component {
private var starImage: (UIImage, PresentationTheme)?
private var optionsDisposable: Disposable?
private(set) var options: [StarsTopUpOption] = [] {
didSet {
self.optionsPromise.set(self.options)
}
}
private let optionsPromise = ValuePromise<[StarsTopUpOption]?>(nil)
override init(frame: CGRect) {
self.scrollView = ScrollView()
self.scrollView.showsVerticalScrollIndicator = true
@ -159,7 +167,12 @@ final class GiftSetupScreenComponent: Component {
}
func proceed() {
guard let component = self.component else {
guard let component = self.component, let starsContext = component.context.starsContext, let starsState = starsContext.currentState else {
return
}
let proceed = { [weak self] in
guard let self else {
return
}
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 {
self.isUpdating = true
defer {

View File

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

View File

@ -12,6 +12,7 @@ import ComponentFlow
import ViewControllerComponent
import SheetComponent
import MultilineTextComponent
import MultilineTextWithEntitiesComponent
import BundleIconComponent
import SolidRoundedButtonComponent
import Markdown
@ -32,6 +33,7 @@ private final class GiftViewSheetContent: CombinedComponent {
let openPeer: (EnginePeer) -> Void
let updateSavedToProfile: (Bool) -> Void
let convertToStars: () -> Void
let openStarsIntro: () -> Void
init(
context: AccountContext,
@ -39,7 +41,8 @@ private final class GiftViewSheetContent: CombinedComponent {
cancel: @escaping (Bool) -> Void,
openPeer: @escaping (EnginePeer) -> Void,
updateSavedToProfile: @escaping (Bool) -> Void,
convertToStars: @escaping () -> Void
convertToStars: @escaping () -> Void,
openStarsIntro: @escaping () -> Void
) {
self.context = context
self.subject = subject
@ -47,6 +50,7 @@ private final class GiftViewSheetContent: CombinedComponent {
self.openPeer = openPeer
self.updateSavedToProfile = updateSavedToProfile
self.convertToStars = convertToStars
self.openStarsIntro = openStarsIntro
}
static func ==(lhs: GiftViewSheetContent, rhs: GiftViewSheetContent) -> Bool {
@ -176,7 +180,7 @@ private final class GiftViewSheetContent: CombinedComponent {
limitNumber = arguments.gift.availability?.remains
limitTotal = arguments.gift.availability?.total
convertStars = arguments.convertStars
incoming = arguments.incoming
incoming = arguments.incoming || arguments.peerId == component.context.account.peerId
savedToProfile = arguments.savedToProfile
converted = arguments.converted
} else {
@ -259,7 +263,13 @@ private final class GiftViewSheetContent: CombinedComponent {
)
let tableFont = Font.regular(15.0)
let tableBoldFont = Font.semibold(15.0)
let tableItalicFont = Font.italic(15.0)
let tableBoldItalicFont = Font.semiboldItalic(15.0)
let tableMonospaceFont = Font.monospace(15.0)
let tableTextColor = theme.list.itemPrimaryTextColor
let tableLinkColor = theme.list.itemAccentColor
var tableItems: [TableComponent.Item] = []
if let peerId = component.subject.arguments?.peerId, let peer = state.peerMap[peerId] {
@ -291,6 +301,18 @@ private final class GiftViewSheetContent: CombinedComponent {
)
)
))
} else {
tableItems.append(.init(
id: "from",
title: strings.Stars_Transaction_From,
component: AnyComponent(
PeerCellComponent(
context: component.context,
theme: theme,
peer: nil
)
)
))
}
tableItems.append(.init(
@ -312,11 +334,19 @@ private final class GiftViewSheetContent: CombinedComponent {
}
if let text {
let attributedText = stringWithAppliedEntities(text, entities: entities ?? [], baseColor: tableTextColor, linkColor: tableLinkColor, baseFont: tableFont, linkFont: tableFont, boldFont: tableBoldFont, italicFont: tableItalicFont, boldItalicFont: tableBoldItalicFont, fixedFont: tableMonospaceFont, blockQuoteFont: tableFont, message: nil)
tableItems.append(.init(
id: "text",
title: nil,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: text, font: tableFont, textColor: tableTextColor)))
MultilineTextWithEntitiesComponent(
context: component.context,
animationCache: component.context.animationCache,
animationRenderer: component.context.animationRenderer,
placeholderColor: theme.list.mediaPlaceholderColor,
text: .plain(attributedText)
)
)
))
}
@ -331,40 +361,7 @@ private final class GiftViewSheetContent: CombinedComponent {
)
let textFont = Font.regular(15.0)
// let boldTextFont = Font.semibold(15.0)
// let textColor = theme.actionSheet.secondaryTextColor
let linkColor = theme.actionSheet.controlAccentColor
// let destructiveColor = theme.actionSheet.destructiveActionTextColor
// let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in
// return (TelegramTextAttributes.URL, contents)
// })
// let additional = additional.update(
// component: BalancedTextComponent(
// text: .markdown(text: additionalText, attributes: markdownAttributes),
// horizontalAlignment: .center,
// maximumNumberOfLines: 0,
// lineSpacing: 0.2,
// highlightColor: linkColor.withAlphaComponent(0.2),
// highlightAction: { attributes in
// if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
// return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
// } else {
// return nil
// }
// },
// tapAction: { attributes, _ in
// if let controller = controller() as? GiftViewScreen, let navigationController = controller.navigationController as? NavigationController {
// let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
// component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: strings.Stars_Transaction_Terms_URL, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {})
// component.cancel(true)
// }
// }
// ),
// availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height),
// transition: .immediate
// )
context.add(title
.position(CGPoint(x: context.availableSize.width / 2.0, y: 177.0))
@ -417,7 +414,7 @@ private final class GiftViewSheetContent: CombinedComponent {
}
},
tapAction: { _, _ in
component.openStarsIntro()
}
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude),
@ -468,30 +465,6 @@ private final class GiftViewSheetContent: CombinedComponent {
)
originY += table.size.height + 23.0
// context.add(additional
// .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + additional.size.height / 2.0))
// )
// originY += additional.size.height + 23.0
// if let statusText {
// originY += 7.0
// let status = status.update(
// component: BalancedTextComponent(
// text: .plain(NSAttributedString(string: statusText, font: textFont, textColor: statusIsDestructive ? destructiveColor : textColor)),
// horizontalAlignment: .center,
// maximumNumberOfLines: 0,
// lineSpacing: 0.1
// ),
// availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height),
// transition: .immediate
// )
// context.add(status
// .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + status.size.height / 2.0))
// )
// originY += status.size.height + (statusIsDestructive ? 23.0 : 13.0)
// }
if incoming && !converted {
let button = button.update(
component: SolidRoundedButtonComponent(
@ -545,6 +518,33 @@ private final class GiftViewSheetContent: CombinedComponent {
.position(CGPoint(x: secondaryButtonFrame.midX, y: secondaryButtonFrame.midY))
)
originY += secondaryButton.size.height
} else {
let button = button.update(
component: SolidRoundedButtonComponent(
title: strings.Common_OK,
theme: SolidRoundedButtonComponent.Theme(theme: theme),
font: .bold,
fontSize: 17.0,
height: 50.0,
cornerRadius: 10.0,
gloss: false,
iconName: nil,
animationName: nil,
iconPosition: .left,
isLoading: state.inProgress,
action: {
component.cancel(true)
}
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0),
transition: context.transition
)
let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: originY), size: button.size)
context.add(button
.position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY))
)
originY += button.size.height
originY += 7.0
}
context.add(closeButton
@ -566,19 +566,22 @@ private final class GiftViewSheetComponent: CombinedComponent {
let openPeer: (EnginePeer) -> Void
let updateSavedToProfile: (Bool) -> Void
let convertToStars: () -> Void
let openStarsIntro: () -> Void
init(
context: AccountContext,
subject: GiftViewScreen.Subject,
openPeer: @escaping (EnginePeer) -> Void,
updateSavedToProfile: @escaping (Bool) -> Void,
convertToStars: @escaping () -> Void
convertToStars: @escaping () -> Void,
openStarsIntro: @escaping () -> Void
) {
self.context = context
self.subject = subject
self.openPeer = openPeer
self.updateSavedToProfile = updateSavedToProfile
self.convertToStars = convertToStars
self.openStarsIntro = openStarsIntro
}
static func ==(lhs: GiftViewSheetComponent, rhs: GiftViewSheetComponent) -> Bool {
@ -620,7 +623,8 @@ private final class GiftViewSheetComponent: CombinedComponent {
},
openPeer: context.component.openPeer,
updateSavedToProfile: context.component.updateSavedToProfile,
convertToStars: context.component.convertToStars
convertToStars: context.component.convertToStars,
openStarsIntro: context.component.openStarsIntro
)),
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
followContentSizeChanges: true,
@ -698,7 +702,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
return (message.id.peerId, message.id, message.flags.contains(.Incoming), gift, convertStars, text, entities, nameHidden, savedToProfile, converted)
}
case let .profileGift(peerId, gift):
return (peerId, gift.messageId, false, gift.gift, gift.convertStars ?? 0, gift.text, gift.entities, gift.nameHidden, true, false)
return (peerId, gift.messageId, false, gift.gift, gift.convertStars ?? 0, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, false)
}
return nil
}
@ -712,13 +716,17 @@ public class GiftViewScreen: ViewControllerComponentContainer {
public init(
context: AccountContext,
subject: GiftViewScreen.Subject,
forceDark: Bool = false
forceDark: Bool = false,
updateSavedToProfile: ((Bool) -> Void)? = nil,
convertToStars: (() -> Void)? = nil
) {
self.context = context
var openPeerImpl: ((EnginePeer) -> Void)?
var updateSavedToProfileImpl: ((Bool) -> Void)?
var convertToStarsImpl: (() -> Void)?
var openStarsIntroImpl: (() -> Void)?
super.init(
context: context,
component: GiftViewSheetComponent(
@ -732,6 +740,9 @@ public class GiftViewScreen: ViewControllerComponentContainer {
},
convertToStars: {
convertToStarsImpl?()
},
openStarsIntro: {
openStarsIntroImpl?()
}
),
navigationBarAppearance: .none,
@ -764,8 +775,12 @@ public class GiftViewScreen: ViewControllerComponentContainer {
guard let self, let arguments = subject.arguments, let messageId = arguments.messageId else {
return
}
if let updateSavedToProfile {
updateSavedToProfile(added)
} else {
let _ = (context.engine.payments.updateStarGiftAddedToProfile(messageId: messageId, added: added)
|> deliverOnMainQueue).startStandalone()
}
self.dismissAnimated()
@ -774,7 +789,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
if let lastController = navigationController.viewControllers.last as? ViewController {
let resultController = UndoOverlayController(
presentationData: presentationData,
content: .sticker(context: context, file: arguments.gift.file, loop: false, title: "Gift Saved to Profile", text: "The gift is now displayed in your profile.", undoText: nil, customAction: nil),
content: .sticker(context: context, file: arguments.gift.file, loop: false, title: added ? "Gift Saved to Profile" : "Gift Removed from Profile", text: added ? "The gift is now displayed in your profile." : "The gift is no longer displayed in your profile.", undoText: nil, customAction: nil),
elevatedLayout: lastController is ChatController,
action: { _ in return true}
)
@ -795,9 +810,12 @@ public class GiftViewScreen: ViewControllerComponentContainer {
actions: [
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}),
TextAlertAction(type: .defaultAction, title: "Convert", action: { [weak self, weak navigationController] in
if let convertToStars {
convertToStars()
} else {
let _ = (context.engine.payments.convertStarGift(messageId: messageId)
|> deliverOnMainQueue).startStandalone()
}
self?.dismissAnimated()
if let navigationController {
@ -827,6 +845,14 @@ public class GiftViewScreen: ViewControllerComponentContainer {
)
self.present(controller, in: .window(.root))
}
openStarsIntroImpl = { [weak self] in
guard let self else {
return
}
let introController = context.sharedContext.makeStarsIntroScreen(context: context)
introController.navigationPresentation = .modal
self.push(introController)
}
}
required public init(coder aDecoder: NSCoder) {
@ -1130,14 +1156,18 @@ private final class PeerCellComponent: Component {
}
final class View: UIView {
private let avatar = ComponentView<Empty>()
private let avatarNode: AvatarNode
private let text = ComponentView<Empty>()
private var component: PeerCellComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 8.0))
super.init(frame: frame)
self.addSubnode(self.avatarNode)
}
required init?(coder: NSCoder) {
@ -1152,29 +1182,25 @@ private final class PeerCellComponent: Component {
let spacing: CGFloat = 6.0
let peerName: String
let peer: StarsContext.State.Transaction.Peer
let avatarOverride: AvatarNodeImageOverride?
if let peerValue = component.peer {
peerName = peerValue.compactDisplayTitle
peer = .peer(peerValue)
avatarOverride = nil
} else {
//TODO:localize
peerName = "Hidden Name"
peer = .fragment
avatarOverride = .anonymousSavedMessagesIcon(isColored: true)
}
let avatarNaturalSize = self.avatar.update(
transition: .immediate,
component: AnyComponent(
StarsAvatarComponent(context: component.context, theme: component.theme, peer: peer, photo: nil, media: [], backgroundColor: .clear)
),
environment: {},
containerSize: CGSize(width: 40.0, height: 40.0)
)
let avatarNaturalSize = CGSize(width: 40.0, height: 40.0)
self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: component.peer, overrideImage: avatarOverride)
self.avatarNode.bounds = CGRect(origin: .zero, size: avatarNaturalSize)
let textSize = self.text.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: peerName, font: Font.regular(15.0), textColor: component.theme.list.itemAccentColor, paragraphAlignment: .left))
text: .plain(NSAttributedString(string: peerName, font: Font.regular(15.0), textColor: component.peer != nil ? component.theme.list.itemAccentColor : component.theme.list.itemPrimaryTextColor, paragraphAlignment: .left))
)
),
environment: {},
@ -1184,15 +1210,7 @@ private final class PeerCellComponent: Component {
let size = CGSize(width: avatarSize.width + textSize.width + spacing, height: textSize.height)
let avatarFrame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((size.height - avatarSize.height) / 2.0)), size: avatarSize)
if let view = self.avatar.view {
if view.superview == nil {
self.addSubview(view)
}
let scale = avatarSize.width / avatarNaturalSize.width
view.transform = CGAffineTransform(scaleX: scale, y: scale)
view.frame = avatarFrame
}
self.avatarNode.frame = avatarFrame
if let view = self.text.view {
if view.superview == nil {

View File

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

View File

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

View File

@ -15,12 +15,13 @@ import MergeLists
import ItemListUI
import ChatControllerInteraction
import MultilineTextComponent
import BalancedTextComponent
import Markdown
import PeerInfoPaneNode
import GiftItemComponent
import PlainButtonComponent
import GiftViewScreen
import ButtonComponent
import SolidRoundedButtonNode
public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScrollViewDelegate {
private let context: AccountContext
@ -37,6 +38,10 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
private let backgroundNode: ASDisplayNode
private let scrollNode: ASScrollNode
private var unlockBackground: UIImageView?
private var unlockText: ComponentView<Empty>?
private var unlockButton: SolidRoundedButtonNode?
private var currentParams: (size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData)?
private var theme: PresentationTheme?
@ -82,7 +87,8 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
guard let self else {
return
}
self.statusPromise.set(.single(PeerInfoStatusData(text: "\(state.count ?? 0) gifts", isActivity: true, key: .gifts)))
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
self.statusPromise.set(.single(PeerInfoStatusData(text: presentationData.strings.SharedMedia_GiftCount(state.count ?? 0), isActivity: true, key: .gifts)))
self.starsProducts = state.gifts
if !self.didSetReady {
@ -149,6 +155,13 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
}
if isVisible {
let ribbonText: String?
if let availability = product.gift.availability {
//TODO:localize
ribbonText = "1 of \(compactNumericCountString(Int(availability.total)))"
} else {
ribbonText = nil
}
let _ = visibleItem.update(
transition: itemTransition,
component: AnyComponent(
@ -157,26 +170,36 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
GiftItemComponent(
context: self.context,
theme: params.presentationData.theme,
peer: product.fromPeer,
peer: product.fromPeer.flatMap { .peer($0) } ?? .anonymous,
subject: .starGift(product.gift.id, product.gift.file),
price: "⭐️ \(product.gift.price)",
ribbon: product.gift.availability != nil ?
GiftItemComponent.Ribbon(
text: "1 of 1K",
color: UIColor(rgb: 0x58c1fe)
)
: nil
ribbon: ribbonText.flatMap { GiftItemComponent.Ribbon(text: $0, color: .blue) },
isHidden: !product.savedToProfile
)
),
effectAlignment: .center,
action: { [weak self] in
if let self {
guard let self else {
return
}
let controller = GiftViewScreen(
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)
}
},
animateAlpha: false
)
@ -198,46 +221,124 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
}
}
let contentHeight = ceil(CGFloat(starsProducts.count) / 3.0) * starsOptionSize.height + 60.0 + params.bottomInset + 16.0
var contentHeight = ceil(CGFloat(starsProducts.count) / 3.0) * starsOptionSize.height + 60.0 + 16.0
// //TODO:localize
// let buttonSize = self.button.update(
// transition: .immediate,
// component: AnyComponent(ButtonComponent(
// background: ButtonComponent.Background(
// color: params.presentationData.theme.list.itemCheckColors.fillColor,
// foreground: params.presentationData.theme.list.itemCheckColors.foregroundColor,
// pressedColor: params.presentationData.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9),
// cornerRadius: 10.0
// ),
// content: AnyComponentWithIdentity(
// id: AnyHashable(0),
// component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: "Send Gifts to Friends", font: Font.semibold(17.0), textColor: )params.presentationData.theme.list.itemCheckColors.foregroundColor)))
// ),
// isEnabled: true,
// displaysProgress: false,
// action: {
//
// }
// )),
// environment: {},
// containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50)
// )
// if let buttonView = self.button.view {
// if buttonView.superview == nil {
// self.addSubview(buttonView)
// }
// buttonView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - buttonSize.width) / 2.0), y: availableSize.height - environment.safeInsets.bottom - buttonSize.height), size: buttonSize)
// }
if self.peerId == self.context.account.peerId {
let transition = ComponentTransition.immediate
// 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)
if self.scrollNode.view.contentSize != contentSize {
self.scrollNode.view.contentSize = contentSize
}
}
let bottomOffset = max(0.0, self.scrollNode.view.contentSize.height - self.scrollNode.view.contentOffset.y - self.scrollNode.view.frame.height)
if bottomOffset < 100.0 {
self.profileGifts.loadMore()
}
}
@objc private func buttonPressed() {
let _ = (self.context.account.stateManager.contactBirthdays
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] birthdays in
guard let self else {
return
}
let controller = self.context.sharedContext.makePremiumGiftController(context: self.context, source: .settings(birthdays), completion: nil)
controller.navigationPresentation = .modal
self.chatControllerInteraction.navigationController()?.pushViewController(controller)
})
}
public func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {

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
case .unlockMedia:
textString = strings.Stars_Purchase_StarsNeededUnlockInfo
case .starGift:
textString = strings.Stars_Purchase_StarGiftInfo(component.peers.first?.value.compactDisplayTitle ?? "").string
}
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: accentColor), linkAttribute: { contents in
@ -815,11 +817,9 @@ private final class StarsPurchaseScreenComponent: CombinedComponent {
switch context.component.purpose {
case .generic:
titleText = strings.Stars_Purchase_GetStars
case let .topUp(requiredStars, _):
titleText = strings.Stars_Purchase_StarsNeeded(Int32(requiredStars))
case .gift:
titleText = strings.Stars_Purchase_GiftStars
case let .transfer(_, requiredStars), let .reactions(_, requiredStars), let .subscription(_, requiredStars, _), let .unlockMedia(requiredStars):
case let .topUp(requiredStars, _), let .transfer(_, requiredStars), let .reactions(_, requiredStars), let .subscription(_, requiredStars, _), let .unlockMedia(requiredStars), let .starGift(_, requiredStars):
titleText = strings.Stars_Purchase_StarsNeeded(Int32(requiredStars))
}
@ -1239,6 +1239,8 @@ private extension StarsPurchasePurpose {
return [peerId]
case let .subscription(peerId, _, _):
return [peerId]
case let .starGift(peerId, _):
return [peerId]
default:
return []
}
@ -1256,6 +1258,8 @@ private extension StarsPurchasePurpose {
return requiredStars
case let .unlockMedia(requiredStars):
return requiredStars
case let .starGift(_, requiredStars):
return requiredStars
default:
return nil
}

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 GiftOptionsScreen
import GiftViewScreen
import StarsIntroScreen
private final class AccountUserInterfaceInUseContext {
let subscribers = Bag<(Bool) -> Void>()
@ -2205,8 +2206,6 @@ public final class SharedAccountContextImpl: SharedAccountContext {
public func makePremiumGiftController(context: AccountContext, source: PremiumGiftSource, completion: (([EnginePeer.Id]) -> Void)?) -> ViewController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
// let limit: Int32 = 10
// var reachedLimitImpl: ((Int32) -> Void)?
var presentBirthdayPickerImpl: (() -> Void)?
var starsMode: ContactSelectionControllerMode = .generic
var currentBirthdays: [EnginePeer.Id: TelegramBirthday]?
@ -2268,9 +2267,9 @@ public final class SharedAccountContextImpl: SharedAccountContext {
))
let _ = combineLatest(queue: Queue.mainQueue(), contactsController.result, options.get())
.startStandalone(next: { [weak contactsController] result, options in
if let (peers, _, _, _, _, _) = result, let contactPeer = peers.first, case let .peer(peer, _, _) = contactPeer {
if let (peers, _, _, _, _, _) = result, let contactPeer = peers.first, case let .peer(peer, _, _) = contactPeer, let starsContext = context.starsContext {
let premiumOptions = options.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) }
let giftController = GiftOptionsScreen(context: context, peerId: peer.id, premiumOptions: premiumOptions)
let giftController = GiftOptionsScreen(context: context, starsContext: starsContext, peerId: peer.id, premiumOptions: premiumOptions)
giftController.navigationPresentation = .modal
contactsController?.push(giftController)
@ -2812,6 +2811,10 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return StarsTransactionScreen(context: context, subject: .boost(peerId, boost))
}
public func makeStarsIntroScreen(context: AccountContext) -> ViewController {
return StarsIntroScreen(context: context)
}
public func makeGiftViewScreen(context: AccountContext, message: EngineMessage) -> ViewController {
return GiftViewScreen(context: context, subject: .message(message))
}