Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
@@ -0,0 +1,30 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "BalanceNeededScreen",
module_name = "BalanceNeededScreen",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display",
"//submodules/ComponentFlow",
"//submodules/TelegramPresentationData",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/AccountContext",
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/BalancedTextComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/Components/ViewControllerComponent",
"//submodules/Components/SheetComponent",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TelegramCore",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,400 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import ViewControllerComponent
import AccountContext
import SheetComponent
import ButtonComponent
import LottieComponent
import MultilineTextComponent
import BalancedTextComponent
import Markdown
import TelegramStringFormatting
import BundleIconComponent
import TelegramCore
import TelegramPresentationData
private final class BalanceNeededSheetContentComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let amount: StarsAmount
let action: () -> Void
let dismiss: () -> Void
init(
context: AccountContext,
amount: StarsAmount,
action: @escaping () -> Void,
dismiss: @escaping () -> Void
) {
self.context = context
self.amount = amount
self.action = action
self.dismiss = dismiss
}
static func ==(lhs: BalanceNeededSheetContentComponent, rhs: BalanceNeededSheetContentComponent) -> Bool {
return true
}
final class View: UIView {
private let icon = ComponentView<Empty>()
private let title = ComponentView<Empty>()
private let text = ComponentView<Empty>()
private let button = ComponentView<Empty>()
private let closeButton = ComponentView<Empty>()
private var component: BalanceNeededSheetContentComponent?
private weak var state: EmptyComponentState?
private var cachedCloseImage: (UIImage, PresentationTheme)?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
func update(component: BalanceNeededSheetContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let environment = environment[EnvironmentType.self].value
let sideInset: CGFloat = 16.0
let closeImage: UIImage
if let (image, theme) = self.cachedCloseImage, theme === environment.theme {
closeImage = image
} else {
closeImage = generateCloseButtonImage(backgroundColor: UIColor(rgb: 0x808084, alpha: 0.1), foregroundColor: environment.theme.actionSheet.inputClearButtonColor)!
self.cachedCloseImage = (closeImage, environment.theme)
}
let closeButtonSize = self.closeButton.update(
transition: .immediate,
component: AnyComponent(Button(
content: AnyComponent(Image(image: closeImage)),
action: { [weak self] in
guard let self, let component = self.component else {
return
}
component.dismiss()
}
)),
environment: {},
containerSize: CGSize(width: 30.0, height: 30.0)
)
let closeButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - closeButtonSize.width - 16.0, y: 12.0), size: closeButtonSize)
if let closeButtonView = self.closeButton.view {
if closeButtonView.superview == nil {
self.addSubview(closeButtonView)
}
transition.setFrame(view: closeButtonView, frame: closeButtonFrame)
}
var contentHeight: CGFloat = 0.0
contentHeight += 32.0
let iconSize = CGSize(width: 120.0, height: 120.0)
let _ = self.icon.update(
transition: transition,
component: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(name: "TonLogo"),
color: nil,
startingPosition: .begin,
size: iconSize,
loop: true
)),
environment: {},
containerSize: iconSize
)
if let iconView = self.icon.view {
if iconView.superview == nil {
self.addSubview(iconView)
}
transition.setFrame(view: iconView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: 16.0), size: iconSize))
}
contentHeight += 110.0
let titleSize = self.title.update(
transition: transition,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: environment.strings.BalanceNeeded_FragmentTitle(formatTonAmountText(component.amount.value, dateTimeFormat: component.context.sharedContext.currentPresentationData.with({ $0 }).dateTimeFormat)).string, font: Font.bold(24.0), textColor: environment.theme.list.itemPrimaryTextColor)),
horizontalAlignment: .center,
maximumNumberOfLines: 0
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0)
)
if let titleView = self.title.view {
if titleView.superview == nil {
self.addSubview(titleView)
}
transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: contentHeight), size: titleSize))
}
contentHeight += titleSize.height
contentHeight += 14.0
let textSize = self.text.update(
transition: transition,
component: AnyComponent(BalancedTextComponent(
text: .plain(NSAttributedString(string: environment.strings.BalanceNeeded_FragmentSubtitle, font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor)),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.18
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0)
)
if let textView = self.text.view {
if textView.superview == nil {
self.addSubview(textView)
}
transition.setFrame(view: textView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - textSize.width) * 0.5), y: contentHeight), size: textSize))
}
contentHeight += textSize.height
contentHeight += 24.0
let buttonSize = self.button.update(
transition: transition,
component: AnyComponent(ButtonComponent(
background: ButtonComponent.Background(
color: environment.theme.list.itemCheckColors.fillColor,
foreground: environment.theme.list.itemCheckColors.foregroundColor,
pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8)
),
content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: environment.strings.BalanceNeeded_FragmentAction, font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor))
))),
isEnabled: true,
allowActionWhenDisabled: true,
displaysProgress: false,
action: { [weak self] in
guard let self, let component = self.component else {
return
}
component.action()
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0)
)
let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: buttonSize)
if let buttonView = self.button.view {
if buttonView.superview == nil {
self.addSubview(buttonView)
}
transition.setFrame(view: buttonView, frame: buttonFrame)
}
contentHeight += buttonSize.height
if environment.safeInsets.bottom.isZero {
contentHeight += 16.0
} else {
contentHeight += environment.safeInsets.bottom + 8.0
}
return CGSize(width: availableSize.width, height: contentHeight)
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private final class BalanceNeededScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let amount: StarsAmount
let buttonAction: (() -> Void)?
init(
context: AccountContext,
amount: StarsAmount,
buttonAction: (() -> Void)?
) {
self.context = context
self.amount = amount
self.buttonAction = buttonAction
}
static func ==(lhs: BalanceNeededScreenComponent, rhs: BalanceNeededScreenComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
return true
}
final class View: UIView {
private let sheet = ComponentView<(ViewControllerComponentContainer.Environment, SheetComponentEnvironment)>()
private let sheetAnimateOut = ActionSlot<Action<Void>>()
private var component: BalanceNeededScreenComponent?
private var environment: EnvironmentType?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: BalanceNeededScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
self.component = component
let environment = environment[ViewControllerComponentContainer.Environment.self].value
self.environment = environment
let sheetEnvironment = SheetComponentEnvironment(
isDisplaying: environment.isVisible,
isCentered: environment.metrics.widthClass == .regular,
hasInputHeight: !environment.inputHeight.isZero,
regularMetricsSize: CGSize(width: 430.0, height: 900.0),
dismiss: { [weak self] _ in
guard let self, let environment = self.environment else {
return
}
self.sheetAnimateOut.invoke(Action { _ in
if let controller = environment.controller() {
controller.dismiss(completion: nil)
}
})
}
)
let _ = self.sheet.update(
transition: transition,
component: AnyComponent(SheetComponent(
content: AnyComponent(BalanceNeededSheetContentComponent(
context: component.context,
amount: component.amount,
action: { [weak self] in
guard let self else {
return
}
self.sheetAnimateOut.invoke(Action { [weak self] _ in
if let controller = environment.controller() {
controller.dismiss(completion: nil)
}
guard let self else {
return
}
self.component?.buttonAction?()
})
},
dismiss: {
self.sheetAnimateOut.invoke(Action { _ in
if let controller = environment.controller() {
controller.dismiss(completion: nil)
}
})
}
)),
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
animateOut: self.sheetAnimateOut
)),
environment: {
environment
sheetEnvironment
},
containerSize: availableSize
)
if let sheetView = self.sheet.view {
if sheetView.superview == nil {
self.addSubview(sheetView)
}
transition.setFrame(view: sheetView, frame: CGRect(origin: CGPoint(), size: availableSize))
}
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public class BalanceNeededScreen: ViewControllerComponentContainer {
public init(
context: AccountContext,
amount: StarsAmount,
buttonAction: (() -> Void)? = nil
) {
super.init(context: context, component: BalanceNeededScreenComponent(
context: context,
amount: amount,
buttonAction: buttonAction
), navigationBarAppearance: .none)
self.statusBar.statusBarStyle = .Ignore
self.navigationPresentation = .flatModal
self.blocksBackgroundWhenInOverlay = true
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.view.disablesInteractiveModalDismiss = true
}
override public func dismiss(completion: (() -> Void)? = nil) {
super.dismiss(completion: {
completion?()
})
self.wasDismissed?()
}
}
func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? {
return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(backgroundColor.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
context.setLineWidth(2.0)
context.setLineCap(.round)
context.setStrokeColor(foregroundColor.cgColor)
context.move(to: CGPoint(x: 10.0, y: 10.0))
context.addLine(to: CGPoint(x: 20.0, y: 20.0))
context.strokePath()
context.move(to: CGPoint(x: 20.0, y: 10.0))
context.addLine(to: CGPoint(x: 10.0, y: 20.0))
context.strokePath()
})
}
@@ -0,0 +1,24 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ItemShimmeringLoadingComponent",
module_name = "ItemShimmeringLoadingComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/Postbox",
"//submodules/ComponentFlow",
"//submodules/AppBundle",
"//submodules/Components/HierarchyTrackingLayer",
"//submodules/TelegramUI/Components/TextLoadingEffect",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,107 @@
import Foundation
import UIKit
import Display
import AppBundle
import HierarchyTrackingLayer
import ComponentFlow
import TextLoadingEffect
public final class ItemShimmeringLoadingComponent: Component {
private let color: UIColor
private let cornerRadius: CGFloat
public init(
color: UIColor,
cornerRadius: CGFloat = 10.0
) {
self.color = color
self.cornerRadius = cornerRadius
}
public static func ==(lhs: ItemShimmeringLoadingComponent, rhs: ItemShimmeringLoadingComponent) -> Bool {
if !lhs.color.isEqual(rhs.color) {
return false
}
if lhs.cornerRadius != rhs.cornerRadius {
return false
}
return true
}
public final class View: UIView {
private let loadingView = TextLoadingEffectView()
private let borderView = UIImageView()
private let borderMaskView = UIView()
private let borderMaskGradientView = UIImageView()
private let borderMaskFillView = UIImageView()
private var component: ItemShimmeringLoadingComponent?
override public init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(self.loadingView)
self.addSubview(self.borderView)
self.borderMaskView.backgroundColor = .clear
self.borderMaskFillView.backgroundColor = .white
self.borderMaskView.addSubview(self.borderMaskFillView)
self.borderMaskFillView.addSubview(self.borderMaskGradientView)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func playAppearanceAnimation() {
self.borderView.mask = self.borderMaskView
let gradientWidth = self.borderView.bounds.width * 0.4
self.borderMaskGradientView.image = generateGradientImage(size: CGSize(width: gradientWidth, height: 24.0), colors: [UIColor.white, UIColor.white.withAlphaComponent(0.0)], locations: [0.0, 1.0], direction: .horizontal)
self.borderMaskGradientView.frame = CGRect(origin: CGPoint(x: self.borderView.bounds.width, y: 0.0), size: CGSize(width: gradientWidth, height: self.borderView.bounds.height))
self.borderMaskFillView.frame = CGRect(origin: .zero, size: self.borderView.bounds.size)
self.borderMaskFillView.layer.animatePosition(from: CGPoint(x: -self.borderView.bounds.width, y: 0.0), to: .zero, duration: 1.0, removeOnCompletion: false, additive: true, completion: { _ in
self.borderView.mask = nil
})
}
func update(component: ItemShimmeringLoadingComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let isFirstTime = self.component == nil
let previousCornerRadius = self.component?.cornerRadius
self.component = component
if previousCornerRadius != component.cornerRadius {
self.borderView.image = generateFilledRoundedRectImage(size: CGSize(width: component.cornerRadius * 2.0 + 2.0, height: component.cornerRadius * 2.0 + 2.0), cornerRadius: component.cornerRadius, color: nil, strokeColor: .white, strokeWidth: 1.0 + UIScreenPixel, backgroundColor: nil)?.stretchableImage(withLeftCapWidth: Int(component.cornerRadius), topCapHeight: Int(component.cornerRadius)).withRenderingMode(.alwaysTemplate)
}
self.borderView.tintColor = component.color
self.loadingView.update(color: component.color, rect: CGRect(origin: .zero, size: availableSize))
self.loadingView.frame = CGRect(origin: .zero, size: availableSize)
self.loadingView.layer.cornerRadius = component.cornerRadius
self.loadingView.clipsToBounds = true
transition.setFrame(view: self.borderView, frame: CGRect(origin: .zero, size: availableSize))
self.borderMaskView.frame = self.borderView.bounds
if isFirstTime {
self.playAppearanceAnimation()
}
return availableSize
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,31 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "StarsAvatarComponent",
module_name = "StarsAvatarComponent",
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/TelegramPresentationData",
"//submodules/PhotoResources",
"//submodules/AvatarNode",
"//submodules/AccountContext",
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/TelegramUI/Components/Gifts/GiftItemComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,505 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import ComponentFlow
import TelegramPresentationData
import PhotoResources
import AvatarNode
import AccountContext
import BundleIconComponent
import MultilineTextComponent
import GiftItemComponent
public final class StarsAvatarComponent: Component {
public enum Peer: Equatable {
case transactionPeer(StarsContext.State.Transaction.Peer)
case search
}
let context: AccountContext
let theme: PresentationTheme
let peer: StarsAvatarComponent.Peer?
let photo: TelegramMediaWebFile?
let media: [Media]
let gift: StarGift?
let backgroundColor: UIColor
let size: CGSize?
public init(
context: AccountContext,
theme: PresentationTheme,
peer: StarsAvatarComponent.Peer?,
photo: TelegramMediaWebFile?,
media: [Media],
gift: StarGift?,
backgroundColor: UIColor,
size: CGSize? = nil
) {
self.context = context
self.theme = theme
self.peer = peer
self.photo = photo
self.media = media
self.gift = gift
self.backgroundColor = backgroundColor
self.size = size
}
public static func ==(lhs: StarsAvatarComponent, rhs: StarsAvatarComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.peer != rhs.peer {
return false
}
if lhs.photo != rhs.photo {
return false
}
if !areMediaArraysEqual(lhs.media, rhs.media) {
return false
}
if lhs.gift != rhs.gift {
return false
}
if lhs.backgroundColor != rhs.backgroundColor {
return false
}
if lhs.size != rhs.size {
return false
}
return true
}
public final class View: UIView {
private let avatarNode: AvatarNode
private let backgroundView = UIImageView()
private let iconView = UIImageView()
private var imageNode: TransformImageNode?
private var imageFrameNode: UIView?
private var secondImageNode: TransformImageNode?
private let giftView = ComponentView<Empty>()
private let fetchDisposable = DisposableSet()
private var component: StarsAvatarComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 20.0))
super.init(frame: frame)
self.iconView.contentMode = .scaleAspectFit
self.addSubnode(self.avatarNode)
self.addSubview(self.backgroundView)
self.addSubview(self.iconView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.fetchDisposable.dispose()
}
func update(component: StarsAvatarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let size = component.size ?? CGSize(width: 40.0, height: 40.0)
var iconInset: CGFloat = 3.0
var iconOffset: CGFloat = 0.0
var dimensions = size
var didSetup = false
if let gift = component.gift {
let giftFrame = CGRect(origin: .zero, size: size)
var subject: GiftItemComponent.Subject
switch gift {
case let .generic(gift):
subject = .starGift(gift: gift, price: "")
case let .unique(gift):
subject = .uniqueGift(gift: gift, price: nil)
}
let _ = self.giftView.update(
transition: .immediate,
component: AnyComponent(
GiftItemComponent(
context: component.context,
theme: component.theme,
strings: component.context.sharedContext.currentPresentationData.with { $0 }.strings,
peer: nil,
subject: subject,
mode: .thumbnail
)
),
environment: {},
containerSize: giftFrame.size
)
if let view = self.giftView.view {
if view.superview == nil {
self.addSubview(view)
}
view.frame = giftFrame
}
} else if !component.media.isEmpty {
let imageNode: TransformImageNode
var isFirstTime = false
if let current = self.imageNode {
imageNode = current
} else {
isFirstTime = true
imageNode = TransformImageNode()
imageNode.contentAnimations = [.subsequentUpdates]
self.addSubview(imageNode.view)
self.imageNode = imageNode
}
if let image = component.media.first as? TelegramMediaImage {
if let imageDimensions = largestImageRepresentation(image.representations)?.dimensions {
dimensions = imageDimensions.cgSize.aspectFilled(size)
}
if isFirstTime {
imageNode.setSignal(chatMessagePhotoThumbnail(account: component.context.account, userLocation: .other, photoReference: .standalone(media: image), onlyFullSize: false, blurred: false))
self.fetchDisposable.add(chatMessagePhotoInteractiveFetched(context: component.context, userLocation: .other, photoReference: .standalone(media: image), displayAtSize: nil, storeToDownloadsPeerId: nil).startStrict())
}
} else if let file = component.media.first as? TelegramMediaFile {
if let videoDimensions = file.dimensions {
dimensions = videoDimensions.cgSize.aspectFilled(size)
}
if isFirstTime {
imageNode.setSignal(mediaGridMessageVideo(postbox: component.context.account.postbox, userLocation: .other, videoReference: .standalone(media: file), autoFetchFullSizeThumbnail: true))
}
}
var imageFrame = CGRect(origin: .zero, size: size)
if component.media.count > 1 {
imageFrame = imageFrame.insetBy(dx: 2.0, dy: 2.0).offsetBy(dx: -2.0, dy: 2.0)
}
imageNode.frame = imageFrame
imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 8.0), imageSize: dimensions, boundingSize: size, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))()
if component.media.count > 1 {
let secondImageNode: TransformImageNode
let imageFrameNode: UIView
var secondDimensions = size
var isFirstTime = false
if let current = self.secondImageNode, let currentFrame = self.imageFrameNode {
secondImageNode = current
imageFrameNode = currentFrame
} else {
isFirstTime = true
secondImageNode = TransformImageNode()
secondImageNode.contentAnimations = [.firstUpdate, .subsequentUpdates]
self.insertSubview(secondImageNode.view, belowSubview: imageNode.view)
self.secondImageNode = secondImageNode
imageFrameNode = UIView()
imageFrameNode.layer.cornerRadius = 8.0
self.insertSubview(imageFrameNode, belowSubview: imageNode.view)
self.imageFrameNode = imageFrameNode
}
if let image = component.media[1] as? TelegramMediaImage {
if let imageDimensions = largestImageRepresentation(image.representations)?.dimensions {
secondDimensions = imageDimensions.cgSize.aspectFilled(size)
}
if isFirstTime {
secondImageNode.setSignal(chatMessagePhotoThumbnail(account: component.context.account, userLocation: .other, photoReference: .standalone(media: image), onlyFullSize: false, blurred: false))
self.fetchDisposable.add(chatMessagePhotoInteractiveFetched(context: component.context, userLocation: .other, photoReference: .standalone(media: image), displayAtSize: nil, storeToDownloadsPeerId: nil).startStrict())
}
} else if let file = component.media[1] as? TelegramMediaFile {
if let videoDimensions = file.dimensions {
secondDimensions = videoDimensions.cgSize.aspectFilled(size)
}
if isFirstTime {
secondImageNode.setSignal(mediaGridMessageVideo(postbox: component.context.account.postbox, userLocation: .other, videoReference: .standalone(media: file), useLargeThumbnail: true, autoFetchFullSizeThumbnail: true))
}
}
imageFrameNode.backgroundColor = component.backgroundColor
secondImageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 8.0), imageSize: secondDimensions, boundingSize: size, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))()
secondImageNode.frame = imageFrame.offsetBy(dx: 4.0, dy: -4.0)
imageFrameNode.frame = imageFrame.insetBy(dx: -1.0 - UIScreenPixel, dy: -1.0 - UIScreenPixel)
}
self.backgroundView.isHidden = true
self.iconView.isHidden = true
self.avatarNode.isHidden = true
didSetup = true
} else if let photo = component.photo {
let imageNode: TransformImageNode
if let current = self.imageNode {
imageNode = current
} else {
imageNode = TransformImageNode()
imageNode.contentAnimations = [.subsequentUpdates]
self.addSubview(imageNode.view)
self.imageNode = imageNode
imageNode.setSignal(chatWebFileImage(account: component.context.account, file: photo))
self.fetchDisposable.add(chatMessageWebFileInteractiveFetched(account: component.context.account, userLocation: .other, image: photo).startStrict())
}
imageNode.frame = CGRect(origin: .zero, size: size)
imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: floor(size.width / 5.0)), imageSize: size, boundingSize: size, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))()
self.backgroundView.isHidden = true
self.iconView.isHidden = true
self.avatarNode.isHidden = true
didSetup = true
}
switch component.peer {
case .none:
break
case let .transactionPeer(peer):
switch peer {
case let .peer(peer):
if !didSetup {
self.avatarNode.setPeer(
context: component.context,
theme: component.theme,
peer: peer,
synchronousLoad: true
)
self.backgroundView.isHidden = true
self.iconView.isHidden = true
self.avatarNode.isHidden = false
}
case .appStore:
self.backgroundView.image = generateGradientFilledCircleImage(
diameter: size.width,
colors: [
UIColor(rgb: 0x2a9ef1).cgColor,
UIColor(rgb: 0x72d5fd).cgColor
],
direction: .mirroredDiagonal
)
self.backgroundView.isHidden = false
self.iconView.isHidden = false
self.avatarNode.isHidden = true
self.iconView.image = UIImage(bundleImageName: "Premium/Stars/Apple")
case .playMarket:
self.backgroundView.image = generateGradientFilledCircleImage(
diameter: size.width,
colors: [
UIColor(rgb: 0x54cb68).cgColor,
UIColor(rgb: 0xa0de7e).cgColor
],
direction: .mirroredDiagonal
)
self.backgroundView.isHidden = false
self.iconView.isHidden = false
self.avatarNode.isHidden = true
self.iconView.image = UIImage(bundleImageName: "Premium/Stars/Google")
case .fragment:
self.backgroundView.image = generateFilledCircleImage(diameter: size.width, color: UIColor(rgb: 0x1b1f24))
self.backgroundView.isHidden = false
self.iconView.isHidden = false
self.avatarNode.isHidden = true
self.iconView.image = UIImage(bundleImageName: "Premium/Stars/Fragment")
iconOffset = 2.0
case .ads:
self.backgroundView.image = generateGradientFilledCircleImage(
diameter: size.width,
colors: [
UIColor(rgb: 0xffa85c).cgColor,
UIColor(rgb: 0xffcd6a).cgColor
],
direction: .mirroredDiagonal
)
self.backgroundView.isHidden = false
self.iconView.isHidden = false
self.avatarNode.isHidden = true
self.iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Channel"), color: .white)
case .premiumBot:
iconInset = 7.0
self.backgroundView.image = generateGradientFilledCircleImage(
diameter: size.width,
colors: [
UIColor(rgb: 0x6b93ff).cgColor,
UIColor(rgb: 0x6b93ff).cgColor,
UIColor(rgb: 0x8d77ff).cgColor,
UIColor(rgb: 0xb56eec).cgColor,
UIColor(rgb: 0xb56eec).cgColor
],
direction: .mirroredDiagonal
)
self.backgroundView.isHidden = false
self.iconView.isHidden = false
self.avatarNode.isHidden = true
self.iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/EntityInputPremiumIcon"), color: .white)
case .apiLimitExtension:
self.backgroundView.image = generateGradientFilledCircleImage(
diameter: size.width,
colors: [
UIColor(rgb: 0x32b83b).cgColor,
UIColor(rgb: 0x87d93b).cgColor
],
direction: .vertical
)
self.backgroundView.isHidden = false
self.iconView.isHidden = false
self.avatarNode.isHidden = true
self.iconView.image = UIImage(bundleImageName: "Premium/Stars/PaidBroadcast")
case .unsupported:
iconInset = 7.0
self.backgroundView.image = generateGradientFilledCircleImage(
diameter: size.width,
colors: [
UIColor(rgb: 0xb1b1b1).cgColor,
UIColor(rgb: 0xcdcdcd).cgColor
],
direction: .mirroredDiagonal
)
self.backgroundView.isHidden = false
self.iconView.isHidden = false
self.avatarNode.isHidden = true
self.iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/EntityInputPremiumIcon"), color: .white)
}
case .search:
iconInset = 6.0
self.backgroundView.image = generateGradientFilledCircleImage(
diameter: size.width,
colors: [
UIColor(rgb: 0x2a9ef1).cgColor,
UIColor(rgb: 0x72d5fd).cgColor
],
direction: .vertical
)
self.backgroundView.isHidden = false
self.iconView.isHidden = false
self.avatarNode.isHidden = true
self.iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat List/SearchInlineButtonIcon"), color: .white)
}
self.avatarNode.frame = CGRect(origin: .zero, size: size)
self.iconView.frame = CGRect(origin: .zero, size: size).insetBy(dx: iconInset, dy: iconInset).offsetBy(dx: 0.0, dy: iconOffset)
self.backgroundView.frame = CGRect(origin: .zero, size: size)
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public final class StarsLabelComponent: CombinedComponent {
let text: NSAttributedString
let subtext: NSAttributedString?
let iconName: String
let iconColor: UIColor?
public init(
text: NSAttributedString,
subtext: NSAttributedString? = nil,
iconName: String = "Premium/Stars/StarMedium",
iconColor: UIColor? = nil
) {
self.text = text
self.subtext = subtext
self.iconName = iconName
self.iconColor = iconColor
}
public static func ==(lhs: StarsLabelComponent, rhs: StarsLabelComponent) -> Bool {
if lhs.text != rhs.text {
return false
}
if lhs.subtext != rhs.subtext {
return false
}
if lhs.iconName != rhs.iconName {
return false
}
if lhs.iconColor != rhs.iconColor {
return false
}
return true
}
public static var body: Body {
let text = Child(MultilineTextComponent.self)
let subLabel = Child(MultilineTextComponent.self)
let icon = Child(BundleIconComponent.self)
return { context in
let component = context.component
let text = text.update(
component: MultilineTextComponent(text: .plain(component.text)),
availableSize: CGSize(width: 140.0, height: 40.0),
transition: context.transition
)
var subtext: _UpdatedChildComponent? = nil
if let sublabel = component.subtext {
subtext = subLabel.update(
component: MultilineTextComponent(text: .plain(sublabel)),
availableSize: CGSize(width: 100.0, height: 40.0),
transition: context.transition
)
}
let iconSize = CGSize(width: 20.0, height: 20.0)
let icon = icon.update(
component: BundleIconComponent(
name: component.iconName,
tintColor: component.iconColor
),
availableSize: iconSize,
transition: context.transition
)
let spacing: CGFloat = 0.0
let totalWidth = text.size.width + spacing + iconSize.width
var size = CGSize(width: totalWidth, height: iconSize.height)
let firstLineSize = size.height
if let subtext {
size.height += subtext.size.height
}
let iconPosition: CGFloat
let textPosition: CGFloat
if let _ = component.subtext {
iconPosition = iconSize.width / 2.0
textPosition = totalWidth - text.size.width / 2.0
} else {
textPosition = text.size.width / 2.0
iconPosition = totalWidth - iconSize.width / 2.0
}
context.add(text
.position(CGPoint(x: textPosition, y: firstLineSize / 2.0))
)
context.add(icon
.position(CGPoint(x: iconPosition, y: firstLineSize / 2.0 - UIScreenPixel))
)
if let subtext {
context.add(subtext
.position(CGPoint(x: size.width - subtext.size.width / 2.0, y: firstLineSize + subtext.size.height / 2.0))
)
}
return size
}
}
}
@@ -0,0 +1,30 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "StarsBalanceOverlayComponent",
module_name = "StarsBalanceOverlayComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/TelegramCore",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/AccountContext",
"//submodules/ComponentFlow",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/MultilineTextWithEntitiesComponent",
"//submodules/TelegramPresentationData",
"//submodules/TextFormat",
"//submodules/Markdown",
"//submodules/TelegramStringFormatting",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,284 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import ComponentDisplayAdapters
import SwiftSignalKit
import TelegramCore
import AccountContext
import TelegramPresentationData
import MultilineTextComponent
import MultilineTextWithEntitiesComponent
import TextFormat
import Markdown
import TelegramStringFormatting
public final class StarsBalanceOverlayComponent: Component {
private let context: AccountContext
private let peerId: EnginePeer.Id
private let theme: PresentationTheme
private let currency: CurrencyAmount.Currency
private let action: () -> Void
public init(
context: AccountContext,
peerId: EnginePeer.Id,
theme: PresentationTheme,
currency: CurrencyAmount.Currency,
action: @escaping () -> Void
) {
self.context = context
self.peerId = peerId
self.theme = theme
self.currency = currency
self.action = action
}
public static func ==(lhs: StarsBalanceOverlayComponent, rhs: StarsBalanceOverlayComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.currency != rhs.currency {
return false
}
return true
}
public final class View: UIView {
private let backgroundView = BlurredBackgroundView(color: nil)
private var text = ComponentView<Empty>()
private let action = ComponentView<Empty>()
private var component: StarsBalanceOverlayComponent?
private var state: EmptyComponentState?
private var starsBalance: Int64 = 0
private var tonBalance: Int64 = 0
private var balanceDisposable: Disposable?
private var starsRevenueStatsContext: StarsRevenueStatsContext?
private var cachedChevronImage: (UIImage, PresentationTheme)?
override public init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(self.backgroundView)
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapped)))
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.balanceDisposable?.dispose()
}
private var didTap = false
@objc private func tapped() {
if let component = self.component, !self.didTap {
self.didTap = true
component.action()
}
}
private var isUpdating = false
func update(component: StarsBalanceOverlayComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
let previousComponent = self.component
self.component = component
self.state = state
if self.balanceDisposable == nil {
if component.peerId == component.context.account.peerId {
if let starsContext = component.context.starsContext, let tonContext = component.context.tonContext {
self.balanceDisposable = combineLatest(queue: Queue.mainQueue(),
starsContext.state,
tonContext.state
).start(next: { [weak self] starsState, tonState in
guard let self else {
return
}
self.starsBalance = starsState?.balance.value ?? 0
self.tonBalance = tonState?.balance.value ?? 0
if !self.isUpdating {
self.state?.updated()
}
})
}
} else {
let starsRevenueStatsContext = StarsRevenueStatsContext(account: component.context.account, peerId: component.peerId, ton: false)
self.starsRevenueStatsContext = starsRevenueStatsContext
self.balanceDisposable = (starsRevenueStatsContext.state
|> map { state -> Int64 in
return state.stats?.balances.currentBalance.amount.value ?? 0
}
|> distinctUntilChanged
|> deliverOnMainQueue).start(next: { [weak self] balance in
guard let self else {
return
}
self.starsBalance = balance
if !self.isUpdating {
self.state?.updated()
}
})
}
}
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let balance = presentationStringsFormattedNumber(Int32(self.starsBalance), presentationData.dateTimeFormat.groupingSeparator)
let rawString: String
if component.peerId == component.context.account.peerId {
let balanceString: String
switch component.currency {
case .stars:
balanceString = "**⭐️\(balance)**"
case .ton:
balanceString = "**💎\(formatTonAmountText(self.tonBalance, dateTimeFormat: presentationData.dateTimeFormat))**"
}
rawString = presentationData.strings.StarsBalance_YourBalance(balanceString).string
} else {
rawString = presentationData.strings.StarsBalance_ChannelBalance("**⭐️\(balance)**").string
}
let attributedText = parseMarkdownIntoAttributedString(
rawString,
attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: component.theme.rootController.navigationBar.primaryTextColor),
bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: component.theme.rootController.navigationBar.primaryTextColor),
link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: component.theme.rootController.navigationBar.primaryTextColor),
linkAttribute: { _ in
return nil
}
)
).mutableCopy() as! NSMutableAttributedString
let starRange = (attributedText.string as NSString).range(of: "⭐️")
if starRange.location != NSNotFound {
attributedText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: starRange)
attributedText.addAttribute(.baselineOffset, value: 1.0, range: starRange)
}
let tonRange = (attributedText.string as NSString).range(of: "💎")
if tonRange.location != NSNotFound {
attributedText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .ton(tinted: false)), range: tonRange)
attributedText.addAttribute(.baselineOffset, value: 1.0, range: tonRange)
}
if previousComponent?.currency != component.currency {
if let textView = self.text.view {
if !transition.animation.isImmediate {
transition.setAlpha(view: textView, alpha: 0.0, completion: { _ in
textView.removeFromSuperview()
})
} else {
textView.removeFromSuperview()
}
}
self.text = ComponentView()
}
let textSize = self.text.update(
transition: .immediate,
component: AnyComponent(
MultilineTextWithEntitiesComponent(
context: component.context,
animationCache: component.context.animationCache,
animationRenderer: component.context.animationRenderer,
placeholderColor: .white,
text: .plain(attributedText),
displaysAsynchronously: false
)
),
environment: {},
containerSize: availableSize
)
let size: CGSize
let hasAction = component.currency == .stars && component.peerId == component.context.account.peerId
if hasAction {
if self.cachedChevronImage == nil || self.cachedChevronImage?.1 !== component.theme {
self.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: component.theme.rootController.navigationBar.accentTextColor)!, component.theme)
}
let actionText = NSMutableAttributedString(string: presentationData.strings.StarsBalance_GetMoreStars, font: Font.regular(13.0), textColor: component.theme.rootController.navigationBar.accentTextColor)
if let range = actionText.string.range(of: ">"), let chevronImage = self.cachedChevronImage?.0 {
actionText.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: actionText.string))
actionText.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: actionText.string))
}
let actionSize = self.action.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(actionText),
maximumNumberOfLines: 1
)
),
environment: {},
containerSize: availableSize
)
size = CGSize(width: max(textSize.width, actionSize.width) + 40.0, height: 54.0)
if let actionView = self.action.view {
if actionView.superview == nil {
actionView.alpha = 1.0
self.backgroundView.addSubview(actionView)
if !transition.animation.isImmediate {
transition.animateAlpha(view: actionView, from: 0.0, to: 1.0)
}
}
actionView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - actionSize.width) / 2.0), y: 29.0), size: actionSize)
}
} else {
size = CGSize(width: textSize.width + 40.0, height: 35.0)
if let actionView = self.action.view, actionView.superview != nil {
if !transition.animation.isImmediate {
transition.setAlpha(view: actionView, alpha: 0.0, completion: { _ in
actionView.removeFromSuperview()
})
} else {
actionView.removeFromSuperview()
}
}
}
if let textView = self.text.view {
if textView.superview == nil {
textView.alpha = 1.0
self.backgroundView.addSubview(textView)
if !transition.animation.isImmediate {
transition.animateAlpha(view: textView, from: 0.0, to: 1.0)
}
}
textView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: 10.0), size: textSize)
}
self.backgroundView.updateColor(color: component.theme.rootController.navigationBar.opaqueBackgroundColor, transition: .immediate)
self.backgroundView.update(size: size, cornerRadius: size.height / 2.0, transition: transition.containedViewLayoutTransition)
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - size.width) / 2.0), y: 0.0), size: size))
return CGSize(width: availableSize.width, height: size.height)
}
public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return self.backgroundView.frame.contains(point)
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,31 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "StarsImageComponent",
module_name = "StarsImageComponent",
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/TelegramPresentationData",
"//submodules/PhotoResources",
"//submodules/AvatarNode",
"//submodules/AccountContext",
"//submodules/InvisibleInkDustNode",
"//submodules/AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,42 @@
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/Premium/PremiumStarComponent",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/Components/BlurredBackgroundComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,533 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import SwiftSignalKit
import TelegramCore
import Markdown
import TextFormat
import TelegramPresentationData
import ViewControllerComponent
import BundleIconComponent
import BalancedTextComponent
import MultilineTextComponent
import ButtonComponent
import AccountContext
import SheetComponent
import BlurredBackgroundComponent
import PremiumStarComponent
private final class SheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
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: SheetContent, rhs: SheetContent) -> 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)
let actionButton = Child(ButtonComponent.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.actionSheet.opaqueItemBackgroundColor
),
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: 79.0))
)
let title = title.update(
component: BalancedTextComponent(
text: .plain(NSAttributedString(string: strings.Stars_Info_Title, 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: strings.Stars_Info_Description, 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: strings.Stars_Info_Gift_Title,
titleColor: textColor,
text: strings.Stars_Info_Gift_Text,
textColor: secondaryTextColor,
accentColor: linkColor,
iconName: "Premium/StarsPerk/Gift",
iconColor: linkColor
))
)
)
items.append(
AnyComponentWithIdentity(
id: "miniapp",
component: AnyComponent(ParagraphComponent(
title: strings.Stars_Info_Miniapp_Title,
titleColor: textColor,
text: strings.Stars_Info_Miniapp_Text,
textColor: secondaryTextColor,
accentColor: linkColor,
iconName: "Premium/StarsPerk/Miniapp",
iconColor: linkColor,
action: {
component.openExamples()
}
))
)
)
items.append(
AnyComponentWithIdentity(
id: "media",
component: AnyComponent(ParagraphComponent(
title: strings.Stars_Info_Media_Title,
titleColor: textColor,
text: strings.Stars_Info_Media_Text,
textColor: secondaryTextColor,
accentColor: linkColor,
iconName: "Premium/StarsPerk/Media",
iconColor: linkColor
))
)
)
items.append(
AnyComponentWithIdentity(
id: "reaction",
component: AnyComponent(ParagraphComponent(
title: strings.Stars_Info_Reaction_Title,
titleColor: textColor,
text: strings.Stars_Info_Reaction_Text,
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
let buttonHeight: CGFloat = 52.0
let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0)
let controller = environment.controller() as? StarsIntroScreen
let actionButton = actionButton.update(
component: ButtonComponent(
background: ButtonComponent.Background(
style: .glass,
color: environment.theme.list.itemCheckColors.fillColor,
foreground: environment.theme.list.itemCheckColors.foregroundColor,
pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8)
),
content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(
Text(text: strings.Stars_Info_Done, font: Font.semibold(17.0), color: environment.theme.list.itemCheckColors.foregroundColor)
)),
isEnabled: true,
displaysProgress: false,
action: {
controller?.dismissAnimated()
}
),
availableSize: CGSize(width: context.availableSize.width - buttonInsets.left - buttonInsets.right, height: buttonHeight),
transition: context.transition
)
context.add(actionButton
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + actionButton.size.height / 2.0))
)
contentSize.height += actionButton.size.height
contentSize.height += buttonInsets.bottom
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 sheet = Child(SheetComponent<(EnvironmentType)>.self)
let animateOut = StoredActionSlot(Action<Void>.self)
return { context in
let environment = context.environment[EnvironmentType.self]
let controller = environment.controller
let sheet = sheet.update(
component: SheetComponent<EnvironmentType>(
content: AnyComponent(SheetContent(
context: context.component.context,
openExamples: context.component.openExamples,
dismiss: {
controller()?.dismiss()
}
)),
style: .glass,
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
followContentSizeChanges: true,
clipsContent: true,
animateOut: animateOut
),
environment: {
environment
SheetComponentEnvironment(
isDisplaying: environment.value.isVisible,
isCentered: environment.metrics.widthClass == .regular,
hasInputHeight: !environment.inputHeight.isZero,
regularMetricsSize: CGSize(width: 430.0, height: 900.0),
dismiss: { animated in
if animated {
animateOut.invoke(Action { _ in
if let controller = controller() {
controller.dismiss(completion: nil)
}
})
} else {
if let controller = controller() {
controller.dismiss(completion: nil)
}
}
}
)
},
availableSize: context.availableSize,
transition: context.transition
)
context.add(sheet
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
)
return context.availableSize
}
}
}
public 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 = .flatModal
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")
}
public func dismissAnimated() {
if let view = self.node.hostView.findTaggedView(tag: SheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? SheetComponent<ViewControllerComponentContainer.Environment>.View {
view.dismissAnimated()
}
}
}
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,
highlightColor: accentColor.withAlphaComponent(0.1),
highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0),
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)
}
}
}
@@ -0,0 +1,47 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "StarsPurchaseScreen",
module_name = "StarsPurchaseScreen",
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/TextLoadingEffect",
"//submodules/TelegramUI/Components/Premium/PremiumStarComponent",
"//submodules/Components/BlurredBackgroundComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/ConfettiEffect",
"//submodules/AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode",
"//submodules/TelegramUI/Components/Stars/ItemShimmeringLoadingComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,46 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "StarsTransactionScreen",
module_name = "StarsTransactionScreen",
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/MultilineTextWithEntitiesComponent",
"//submodules/Components/BalancedTextComponent",
"//submodules/TelegramPresentationData",
"//submodules/AccountContext",
"//submodules/AppBundle",
"//submodules/TelegramStringFormatting",
"//submodules/PresentationDataUtils",
"//submodules/Components/SheetComponent",
"//submodules/UndoUI",
"//submodules/TextFormat",
"//submodules/Components/BundleIconComponent",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/AvatarNode",
"//submodules/TelegramUI/Components/Stars/StarsImageComponent",
"//submodules/TelegramUI/Components/Stars/StarsAvatarComponent",
"//submodules/GalleryUI",
"//submodules/TelegramUI/Components/MiniAppListScreen",
"//submodules/TelegramUI/Components/Premium/PremiumStarComponent",
"//submodules/TelegramUI/Components/Gifts/GiftAnimationComponent",
"//submodules/TelegramUI/Components/GlassBarButtonComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,59 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "StarsTransactionsScreen",
module_name = "StarsTransactionsScreen",
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/BlurredBackgroundComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/Components/SolidRoundedButtonComponent",
"//submodules/TelegramUI/Components/AnimatedTextComponent",
"//submodules/AvatarNode",
"//submodules/PhotoResources",
"//submodules/TelegramUI/Components/Stars/StarsImageComponent",
"//submodules/PasswordSetupUI",
"//submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController",
"//submodules/TelegramUI/Components/ListItemComponentAdaptor",
"//submodules/StatisticsUI",
"//submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen",
"//submodules/TelegramUI/Components/Stars/StarsAvatarComponent",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TelegramUI/Components/LottieComponentResourceContent",
"//submodules/TelegramUI/Components/Gifts/GiftAnimationComponent",
"//submodules/TelegramUI/Components/Premium/PremiumCoinComponent",
"//submodules/TelegramUI/Components/Premium/PremiumDiamondComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,419 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import AccountContext
import MultilineTextComponent
import TelegramPresentationData
import PresentationDataUtils
import ButtonComponent
import BundleIconComponent
import TelegramStringFormatting
import TelegramCore
final class StarsBalanceComponent: Component {
let theme: PresentationTheme
let strings: PresentationStrings
let dateTimeFormat: PresentationDateTimeFormat
let count: StarsAmount
let currency: CurrencyAmount.Currency
let rate: Double?
let actionTitle: String
let actionAvailable: Bool
let actionIsEnabled: Bool
let actionCooldownUntilTimestamp: Int32?
let actionIcon: UIImage?
let action: () -> Void
let secondaryActionTitle: String?
let secondaryActionIcon: UIImage?
let secondaryActionCooldownUntilTimestamp: Int32?
let secondaryAction: (() -> Void)?
let additionalAction: AnyComponent<Empty>?
init(
theme: PresentationTheme,
strings: PresentationStrings,
dateTimeFormat: PresentationDateTimeFormat,
count: StarsAmount,
currency: CurrencyAmount.Currency,
rate: Double?,
actionTitle: String,
actionAvailable: Bool,
actionIsEnabled: Bool,
actionCooldownUntilTimestamp: Int32? = nil,
actionIcon: UIImage? = nil,
action: @escaping () -> Void,
secondaryActionTitle: String? = nil,
secondaryActionIcon: UIImage? = nil,
secondaryActionCooldownUntilTimestamp: Int32? = nil,
secondaryAction: (() -> Void)? = nil,
additionalAction: AnyComponent<Empty>? = nil
) {
self.theme = theme
self.strings = strings
self.dateTimeFormat = dateTimeFormat
self.count = count
self.currency = currency
self.rate = rate
self.actionTitle = actionTitle
self.actionAvailable = actionAvailable
self.actionIsEnabled = actionIsEnabled
self.actionCooldownUntilTimestamp = actionCooldownUntilTimestamp
self.actionIcon = actionIcon
self.action = action
self.secondaryActionTitle = secondaryActionTitle
self.secondaryActionCooldownUntilTimestamp = secondaryActionCooldownUntilTimestamp
self.secondaryActionIcon = secondaryActionIcon
self.secondaryAction = secondaryAction
self.additionalAction = additionalAction
}
static func ==(lhs: StarsBalanceComponent, rhs: StarsBalanceComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.dateTimeFormat != rhs.dateTimeFormat {
return false
}
if lhs.actionTitle != rhs.actionTitle {
return false
}
if lhs.actionAvailable != rhs.actionAvailable {
return false
}
if lhs.actionIsEnabled != rhs.actionIsEnabled {
return false
}
if lhs.actionCooldownUntilTimestamp != rhs.actionCooldownUntilTimestamp {
return false
}
if lhs.secondaryActionTitle != rhs.secondaryActionTitle {
return false
}
if lhs.secondaryActionCooldownUntilTimestamp != rhs.secondaryActionCooldownUntilTimestamp {
return false
}
if lhs.count != rhs.count {
return false
}
if lhs.currency != rhs.currency {
return false
}
if lhs.rate != rhs.rate {
return false
}
return true
}
final class View: UIView {
private let icon = UIImageView()
private let title = ComponentView<Empty>()
private let subtitle = ComponentView<Empty>()
private var button = ComponentView<Empty>()
private var secondaryButton = ComponentView<Empty>()
private var additionalButton = ComponentView<Empty>()
private var component: StarsBalanceComponent?
private weak var state: EmptyComponentState?
private var timer: Timer?
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(self.icon)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: StarsBalanceComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
if self.component == nil {
switch component.currency {
case .ton:
self.icon.image = generateTintedImage(image: UIImage(bundleImageName: "Ads/TonBig"), color: component.theme.list.itemAccentColor)
case .stars:
self.icon.image = UIImage(bundleImageName: "Premium/Stars/BalanceStar")
}
}
self.component = component
self.state = state
var remainingCooldownSeconds: Int32 = 0
if let cooldownUntilTimestamp = component.actionCooldownUntilTimestamp {
remainingCooldownSeconds = cooldownUntilTimestamp - Int32(Date().timeIntervalSince1970)
remainingCooldownSeconds = max(0, remainingCooldownSeconds)
}
var remainingSecondaryCooldownSeconds: Int32 = 0
if let cooldownUntilTimestamp = component.secondaryActionCooldownUntilTimestamp {
remainingSecondaryCooldownSeconds = cooldownUntilTimestamp - Int32(Date().timeIntervalSince1970)
remainingSecondaryCooldownSeconds = max(0, remainingSecondaryCooldownSeconds)
}
if remainingCooldownSeconds > 0 || remainingSecondaryCooldownSeconds > 0 {
if self.timer == nil {
self.timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { [weak self] _ in
guard let self else {
return
}
self.state?.updated(transition: .immediate)
})
}
} else {
if let timer = self.timer {
self.timer = nil
timer.invalidate()
}
}
let sideInset: CGFloat = 16.0
var contentHeight: CGFloat = sideInset
let formattedLabel: String
switch component.currency {
case .ton:
formattedLabel = formatTonAmountText(component.count.value, dateTimeFormat: component.dateTimeFormat, maxDecimalPositions: 3)
case .stars:
formattedLabel = formatStarsAmountText(component.count, dateTimeFormat: component.dateTimeFormat)
}
let labelFont: UIFont
if formattedLabel.contains(component.dateTimeFormat.decimalSeparator) {
labelFont = Font.with(size: 48.0, design: .round, weight: .semibold)
} else {
labelFont = Font.with(size: 48.0, design: .round, weight: .semibold)
}
let smallLabelFont = Font.with(size: 32.0, design: .round, weight: .regular)
let balanceString = tonAmountAttributedString(formattedLabel, integralFont: labelFont, fractionalFont: smallLabelFont, color: component.theme.list.itemPrimaryTextColor, decimalSeparator: component.dateTimeFormat.decimalSeparator)
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(balanceString)
)
),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0)
)
if let titleView = self.title.view {
if titleView.superview == nil {
self.addSubview(titleView)
}
if let icon = self.icon.image {
let spacing: CGFloat = 4.0
let totalWidth = titleSize.width + icon.size.width + spacing
let origin = floorToScreenPixels((availableSize.width - totalWidth) / 2.0)
let titleFrame = CGRect(origin: CGPoint(x: origin + icon.size.width + spacing, y: contentHeight - 3.0), size: titleSize)
titleView.frame = titleFrame
self.icon.frame = CGRect(origin: CGPoint(x: origin, y: floorToScreenPixels(titleFrame.midY - icon.size.height / 2.0)), size: icon.size)
}
}
contentHeight += titleSize.height
let subtitleText: String
if let rate = component.rate {
subtitleText = "~\(formatTonUsdValue(component.count.value, divide: false, rate: rate, dateTimeFormat: component.dateTimeFormat))"
} else {
subtitleText = component.strings.Stars_Intro_YourBalance
}
let subtitleSize = self.subtitle.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: subtitleText, font: Font.regular(17.0), textColor: component.theme.list.itemSecondaryTextColor)),
horizontalAlignment: .center
)
),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0)
)
if let subtitleView = self.subtitle.view {
if subtitleView.superview == nil {
self.addSubview(subtitleView)
}
let subtitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - subtitleSize.width) / 2.0), y: contentHeight - 4.0), size: subtitleSize)
subtitleView.frame = subtitleFrame
}
contentHeight += subtitleSize.height
if component.actionAvailable {
contentHeight += 12.0
var withdrawWidth = availableSize.width - sideInset * 2.0
if let _ = component.secondaryAction {
withdrawWidth = (withdrawWidth - 10.0) / 2.0
}
let content: AnyComponentWithIdentity<Empty>
if remainingCooldownSeconds > 0 {
content = AnyComponentWithIdentity(id: AnyHashable(1 as Int), component: AnyComponent(
VStack([
AnyComponentWithIdentity(id: AnyHashable(1 as Int), component: AnyComponent(Text(text: component.actionTitle, font: Font.semibold(17.0), color: component.theme.list.itemCheckColors.foregroundColor))),
AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(HStack([
AnyComponentWithIdentity(id: 1, component: AnyComponent(BundleIconComponent(name: "Chat List/StatusLockIcon", tintColor: component.theme.list.itemCheckColors.fillColor.mixedWith(component.theme.list.itemCheckColors.foregroundColor, alpha: 0.7)))),
AnyComponentWithIdentity(id: 0, component: AnyComponent(Text(text: stringForRemainingTime(remainingCooldownSeconds), font: Font.with(size: 11.0, weight: .medium, traits: [.monospacedNumbers]), color: component.theme.list.itemCheckColors.fillColor.mixedWith(component.theme.list.itemCheckColors.foregroundColor, alpha: 0.7))))
], spacing: 3.0)))
], spacing: 1.0)
))
} else {
var items: [AnyComponentWithIdentity<Empty>] = []
if let icon = component.actionIcon {
items.append(AnyComponentWithIdentity(id: "icon", component: AnyComponent(Image(image: icon, tintColor: component.theme.list.itemCheckColors.foregroundColor, size: icon.size))))
}
items.append(AnyComponentWithIdentity(id: "label", component: AnyComponent(Text(text: component.actionTitle, font: Font.semibold(17.0), color: component.theme.list.itemCheckColors.foregroundColor))))
content = AnyComponentWithIdentity(
id: AnyHashable(0 as Int),
component: AnyComponent(
HStack(items, spacing: 7.0)
)
)
}
let buttonSize = self.button.update(
transition: transition,
component: AnyComponent(ButtonComponent(
background: ButtonComponent.Background(
style: .glass,
color: component.theme.list.itemCheckColors.fillColor,
foreground: component.theme.list.itemCheckColors.foregroundColor,
pressedColor: component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8)
),
content: content,
isEnabled: component.actionIsEnabled,
allowActionWhenDisabled: false,
displaysProgress: false,
action: { [weak self] in
guard let self, let component = self.component else {
return
}
component.action()
}
)),
environment: {},
containerSize: CGSize(width: withdrawWidth, height: 52.0)
)
if let buttonView = self.button.view {
if buttonView.superview == nil {
self.addSubview(buttonView)
}
let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: buttonSize)
buttonView.frame = buttonFrame
}
if let secondaryActionTitle = component.secondaryActionTitle {
let content: AnyComponentWithIdentity<Empty>
var items: [AnyComponentWithIdentity<Empty>] = []
if let icon = component.secondaryActionIcon {
items.append(AnyComponentWithIdentity(id: "icon", component: AnyComponent(Image(image: icon, tintColor: component.theme.list.itemCheckColors.foregroundColor, size: icon.size))))
}
if remainingSecondaryCooldownSeconds > 0 {
items.append(AnyComponentWithIdentity(id: AnyHashable(1 as Int), component: AnyComponent(
VStack([
AnyComponentWithIdentity(id: AnyHashable(1 as Int), component: AnyComponent(Text(text: secondaryActionTitle, font: Font.semibold(17.0), color: component.theme.list.itemCheckColors.foregroundColor))),
AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(HStack([
AnyComponentWithIdentity(id: 1, component: AnyComponent(BundleIconComponent(name: "Chat List/StatusLockIcon", tintColor: component.theme.list.itemCheckColors.fillColor.mixedWith(component.theme.list.itemCheckColors.foregroundColor, alpha: 0.7)))),
AnyComponentWithIdentity(id: 0, component: AnyComponent(Text(text: stringForRemainingTime(remainingSecondaryCooldownSeconds), font: Font.with(size: 11.0, weight: .medium, traits: [.monospacedNumbers]), color: component.theme.list.itemCheckColors.fillColor.mixedWith(component.theme.list.itemCheckColors.foregroundColor, alpha: 0.7))))
], spacing: 3.0)))
], spacing: 1.0)
)))
} else {
items.append(AnyComponentWithIdentity(id: "label", component: AnyComponent(Text(text: secondaryActionTitle, font: Font.semibold(17.0), color: component.theme.list.itemCheckColors.foregroundColor))))
}
content = AnyComponentWithIdentity(
id: AnyHashable(0 as Int),
component: AnyComponent(
HStack(items, spacing: 7.0)
)
)
let buttonSize = self.secondaryButton.update(
transition: transition,
component: AnyComponent(ButtonComponent(
background: ButtonComponent.Background(
style: .glass,
color: component.theme.list.itemCheckColors.fillColor,
foreground: component.theme.list.itemCheckColors.foregroundColor,
pressedColor: component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8)
),
content: content,
isEnabled: component.actionIsEnabled,
allowActionWhenDisabled: false,
displaysProgress: false,
action: { [weak self] in
guard let self, let component = self.component else {
return
}
component.secondaryAction?()
}
)),
environment: {},
containerSize: CGSize(width: withdrawWidth, height: 52.0)
)
if let buttonView = self.secondaryButton.view {
if buttonView.superview == nil {
self.addSubview(buttonView)
}
let buttonFrame = CGRect(origin: CGPoint(x: sideInset + withdrawWidth + 10.0, y: contentHeight), size: buttonSize)
buttonView.frame = buttonFrame
}
}
contentHeight += buttonSize.height
}
if let additionalAction = component.additionalAction {
contentHeight += 18.0
let buttonSize = self.additionalButton.update(
transition: transition,
component: additionalAction,
environment: {},
containerSize: CGSize(width: availableSize.width, height: 50.0)
)
if let buttonView = self.additionalButton.view {
if buttonView.superview == nil {
self.addSubview(buttonView)
}
let buttonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - buttonSize.width) / 2.0), y: contentHeight), size: buttonSize)
buttonView.frame = buttonFrame
}
contentHeight += buttonSize.height
contentHeight += 2.0
}
contentHeight += sideInset
return CGSize(width: availableSize.width, height: contentHeight)
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
func stringForRemainingTime(_ duration: Int32) -> String {
let hours = duration / 3600
let minutes = duration / 60 % 60
let seconds = duration % 60
let durationString: String
if hours > 0 {
durationString = String(format: "%d:%02d", hours, minutes)
} else {
durationString = String(format: "%02d:%02d", minutes, seconds)
}
return durationString
}
@@ -0,0 +1,160 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import AccountContext
import MultilineTextComponent
import TelegramPresentationData
import PresentationDataUtils
import TelegramStringFormatting
import TelegramCore
final class StarsOverviewItemComponent: Component {
let theme: PresentationTheme
let dateTimeFormat: PresentationDateTimeFormat
let title: String
let value: CurrencyAmount
let rate: Double
init(
theme: PresentationTheme,
dateTimeFormat: PresentationDateTimeFormat,
title: String,
value: CurrencyAmount,
rate: Double
) {
self.theme = theme
self.dateTimeFormat = dateTimeFormat
self.title = title
self.value = value
self.rate = rate
}
static func ==(lhs: StarsOverviewItemComponent, rhs: StarsOverviewItemComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.dateTimeFormat != rhs.dateTimeFormat {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.value != rhs.value {
return false
}
if lhs.rate != rhs.rate {
return false
}
return true
}
final class View: UIView {
private let icon = UIImageView()
private let value = ComponentView<Empty>()
private let usdValue = ComponentView<Empty>()
private let title = ComponentView<Empty>()
private var component: StarsOverviewItemComponent?
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(self.icon)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: StarsOverviewItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
let sideInset: CGFloat = 16.0
let iconY: CGFloat = component.value.currency == .ton ? 13.0 + UIScreenPixel : 10.0
if self.icon.image == nil {
switch component.value.currency {
case .ton:
self.icon.image = generateTintedImage(image: UIImage(bundleImageName: "Ads/TonMedium"), color: component.theme.list.itemAccentColor)
case .stars:
self.icon.image = UIImage(bundleImageName: "Premium/Stars/StarMedium")
}
}
var valueOffset: CGFloat = 0.0
if let icon = self.icon.image {
self.icon.frame = CGRect(origin: CGPoint(x: sideInset - 1.0, y: iconY), size: icon.size)
valueOffset += icon.size.width
}
let valueString = formatCurrencyAmountText(component.value, dateTimeFormat: component.dateTimeFormat, maxDecimalPositions: nil)
let usdValueString = formatTonUsdValue(component.value.amount.value, divide: component.value.currency == .ton, rate: component.rate, dateTimeFormat: component.dateTimeFormat)
let valueSize = self.value.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: valueString, font: Font.semibold(17.0), textColor: component.theme.list.itemPrimaryTextColor))
)
),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0)
)
let valueFrame = CGRect(origin: CGPoint(x: sideInset + valueOffset + 2.0, y: 10.0), size: valueSize)
if let valueView = self.value.view {
if valueView.superview == nil {
self.addSubview(valueView)
}
valueView.frame = valueFrame
}
let usdValueSize = self.usdValue.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: "~\(usdValueString)", font: Font.regular(13.0), textColor: component.theme.list.itemSecondaryTextColor))
)
),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0)
)
let usdValueFrame = CGRect(origin: CGPoint(x: sideInset + valueOffset + valueSize.width + 6.0, y: 14.0), size: usdValueSize)
if let usdValueView = self.usdValue.view {
if usdValueView.superview == nil {
self.addSubview(usdValueView)
}
usdValueView.frame = usdValueFrame
}
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: component.title, font: Font.regular(13.0), textColor: component.theme.list.itemSecondaryTextColor))
)
),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0)
)
if let titleView = self.title.view {
if titleView.superview == nil {
self.addSubview(titleView)
}
let titleFrame = CGRect(origin: CGPoint(x: sideInset, y: 32.0), size: titleSize)
titleView.frame = titleFrame
}
return CGSize(width: availableSize.width, height: 59.0)
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,734 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import SwiftSignalKit
import ViewControllerComponent
import ComponentDisplayAdapters
import TelegramPresentationData
import AccountContext
import TelegramCore
import MultilineTextComponent
import ListActionItemComponent
import TelegramStringFormatting
import AvatarNode
import BundleIconComponent
import PhotoResources
import StarsAvatarComponent
import GiftAnimationComponent
import TelegramStringFormatting
private extension StarsContext.State.Transaction {
var extendedId: String {
if self.count.amount > StarsAmount.zero {
return "\(id)_in"
} else {
return "\(id)_out"
}
}
}
final class StarsTransactionsListPanelComponent: Component {
typealias EnvironmentType = StarsTransactionsPanelEnvironment
let context: AccountContext
let transactionsContext: StarsTransactionsContext
let isAccount: Bool
let action: (StarsContext.State.Transaction) -> Void
init(
context: AccountContext,
transactionsContext: StarsTransactionsContext,
isAccount: Bool,
action: @escaping (StarsContext.State.Transaction) -> Void
) {
self.context = context
self.transactionsContext = transactionsContext
self.isAccount = isAccount
self.action = action
}
static func ==(lhs: StarsTransactionsListPanelComponent, rhs: StarsTransactionsListPanelComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.isAccount != rhs.isAccount {
return false
}
return true
}
private struct ItemLayout: Equatable {
let containerInsets: UIEdgeInsets
let containerWidth: CGFloat
let itemHeight: CGFloat
let itemCount: Int
let contentHeight: CGFloat
init(
containerInsets: UIEdgeInsets,
containerWidth: CGFloat,
itemHeight: CGFloat,
itemCount: Int
) {
self.containerInsets = containerInsets
self.containerWidth = containerWidth
self.itemHeight = itemHeight
self.itemCount = itemCount
self.contentHeight = containerInsets.top + containerInsets.bottom + CGFloat(itemCount) * itemHeight
}
func visibleItems(for rect: CGRect) -> Range<Int>? {
let offsetRect = rect.offsetBy(dx: -self.containerInsets.left, dy: -self.containerInsets.top)
var minVisibleRow = Int(floor((offsetRect.minY) / (self.itemHeight)))
minVisibleRow = max(0, minVisibleRow)
let maxVisibleRow = Int(ceil((offsetRect.maxY) / (self.itemHeight)))
let minVisibleIndex = minVisibleRow
let maxVisibleIndex = maxVisibleRow
if maxVisibleIndex >= minVisibleIndex {
return minVisibleIndex ..< (maxVisibleIndex + 1)
} else {
return nil
}
}
func itemFrame(for index: Int) -> CGRect {
return CGRect(origin: CGPoint(x: 0.0, y: self.containerInsets.top + CGFloat(index) * self.itemHeight), size: CGSize(width: self.containerWidth, height: self.itemHeight))
}
}
private final class ScrollViewImpl: UIScrollView {
var forceDecelerating = false
override var isDecelerating: Bool {
return self.forceDecelerating || super.isDecelerating
}
override func touchesShouldCancel(in view: UIView) -> Bool {
return true
}
}
class View: UIView, UIScrollViewDelegate {
private let scrollView: ScrollViewImpl
private let measureItem = ComponentView<Empty>()
private var visibleItems: [AnyHashable: ComponentView<Empty>] = [:]
private var separatorLayers: [AnyHashable: SimpleLayer] = [:]
private var highlightLayer = SimpleLayer()
private var ignoreScrolling: Bool = false
private var component: StarsTransactionsListPanelComponent?
private var environment: StarsTransactionsPanelEnvironment?
private var itemLayout: ItemLayout?
private var items: [StarsContext.State.Transaction] = []
private var itemsDisposable: Disposable?
private var currentLoadMoreId: String?
override init(frame: CGRect) {
self.scrollView = ScrollViewImpl()
super.init(frame: frame)
self.scrollView.delaysContentTouches = true
self.scrollView.canCancelContentTouches = true
self.scrollView.clipsToBounds = false
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.scrollView.contentInsetAdjustmentBehavior = .never
}
if #available(iOS 13.0, *) {
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
}
self.scrollView.showsVerticalScrollIndicator = false
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.alwaysBounceHorizontal = false
self.scrollView.scrollsToTop = false
self.scrollView.delegate = self
self.scrollView.clipsToBounds = true
self.addSubview(self.scrollView)
self.scrollView.layer.addSublayer(self.highlightLayer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.itemsDisposable?.dispose()
}
func scrollToTop() -> Bool {
if self.scrollView.contentOffset.y > 0.0 {
self.scrollView.setContentOffset(CGPoint(), animated: true)
return true
} else {
return false
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !self.ignoreScrolling {
self.updateScrolling(transition: .immediate)
}
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
cancelContextGestures(view: scrollView)
if let decelerationAnimator = self.decelerationAnimator {
self.scrollView.forceDecelerating = false
self.decelerationAnimator = nil
decelerationAnimator.invalidate()
}
}
private var decelerationAnimator: ConstantDisplayLinkAnimator?
func transferVelocity(_ velocity: CGFloat) {
if velocity <= 0.0 {
return
}
self.decelerationAnimator?.isPaused = true
let startTime = CACurrentMediaTime()
var currentOffset = self.scrollView.contentOffset
let decelerationRate: CGFloat = 0.998
self.scrollView.forceDecelerating = true
//self.scrollViewDidEndDragging(self.scrollView, willDecelerate: true)
self.decelerationAnimator = ConstantDisplayLinkAnimator(update: { [weak self] in
guard let strongSelf = self else {
return
}
let t = CACurrentMediaTime() - startTime
var currentVelocity = velocity * 15.0 * CGFloat(pow(Double(decelerationRate), 1000.0 * t))
currentOffset.y += currentVelocity
let maxOffset = strongSelf.scrollView.contentSize.height - strongSelf.scrollView.bounds.height
if currentOffset.y >= maxOffset {
currentOffset.y = maxOffset
currentVelocity = 0.0
}
if currentOffset.y < 0.0 {
currentOffset.y = 0.0
currentVelocity = 0.0
}
var didEnd = false
if abs(currentVelocity) < 0.1 {
strongSelf.decelerationAnimator?.isPaused = true
strongSelf.decelerationAnimator = nil
didEnd = true
}
var contentOffset = strongSelf.scrollView.contentOffset
contentOffset.y = floorToScreenPixels(currentOffset.y)
strongSelf.scrollView.setContentOffset(contentOffset, animated: false)
strongSelf.scrollViewDidScroll(strongSelf.scrollView)
if didEnd {
//strongSelf.scrollViewDidEndDecelerating(strongSelf.scrollView)
strongSelf.scrollView.forceDecelerating = false
}
})
self.decelerationAnimator?.isPaused = false
}
private var highlightedItemId: AnyHashable?
private func updateHighlightedItem(itemId: AnyHashable?) {
guard let environment = self.environment else {
return
}
if self.highlightedItemId == itemId {
return
}
let previousHighlightedItemId = self.highlightedItemId
self.highlightedItemId = itemId
if let _ = previousHighlightedItemId, itemId == nil {
ComponentTransition.easeInOut(duration: 0.2).setBackgroundColor(layer: self.highlightLayer, color: .clear)
}
if let itemId, let itemView = self.visibleItems[itemId]?.view {
var highlightFrame = itemView.frame
highlightFrame.size.height += UIScreenPixel
self.highlightLayer.frame = highlightFrame
ComponentTransition.immediate.setBackgroundColor(layer: self.highlightLayer, color: environment.theme.list.itemHighlightedBackgroundColor)
}
}
private func updateScrolling(transition: ComponentTransition) {
guard let component = self.component, let environment = self.environment, let itemLayout = self.itemLayout else {
return
}
var visibleBounds = environment.externalScrollBounds ?? self.scrollView.bounds
visibleBounds = visibleBounds.insetBy(dx: 0.0, dy: -100.0)
var validIds = Set<AnyHashable>()
if let visibleItems = itemLayout.visibleItems(for: visibleBounds) {
for index in visibleItems.lowerBound ..< visibleItems.upperBound {
if index >= self.items.count {
continue
}
let item = self.items[index]
let id = AnyHashable(item.extendedId)
validIds.insert(id)
var itemTransition = transition
let itemView: ComponentView<Empty>
let separatorLayer: SimpleLayer
if let current = self.visibleItems[id], let currentSeparator = self.separatorLayers[id] {
itemView = current
separatorLayer = currentSeparator
} else {
itemTransition = .immediate
itemView = ComponentView()
self.visibleItems[id] = itemView
separatorLayer = SimpleLayer()
self.separatorLayers[id] = separatorLayer
self.scrollView.layer.addSublayer(separatorLayer)
}
separatorLayer.backgroundColor = environment.theme.list.itemBlocksSeparatorColor.cgColor
separatorLayer.isHidden = index == self.items.count - 1
let fontBaseDisplaySize = 17.0
var itemTitle: String
let itemSubtitle: String?
var itemDate: String
var itemPeer: StarsAvatarComponent.Peer = .transactionPeer(item.peer)
var itemFile: TelegramMediaFile?
var itemGift: StarGift?
switch item.peer {
case let .peer(peer):
if let months = item.premiumGiftMonths {
itemTitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast)
itemSubtitle = environment.strings.Stars_Intro_Transaction_TelegramPremium(months)
} else if item.flags.contains(.isPostsSearch) {
itemTitle = environment.strings.Stars_Intro_Transaction_SearchFee
itemSubtitle = ""
itemPeer = .search
} else if item.flags.contains(.isLiveStreamPaidMessage) {
itemTitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast)
if item.flags.contains(.isReaction) {
itemSubtitle = environment.strings.Stars_Intro_Transaction_LiveStreamReaction
} else {
itemSubtitle = environment.strings.Stars_Intro_Transaction_LiveStreamPaidMessage(item.paidMessageCount ?? 1)
}
} else if item.flags.contains(.isPaidMessage) {
itemTitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast)
itemSubtitle = environment.strings.Stars_Intro_Transaction_PaidMessage(item.paidMessageCount ?? 1)
} else if let starGift = item.starGift {
if item.flags.contains(.isStarGiftAuctionBid), case let .generic(gift) = starGift {
itemTitle = gift.title ?? "Gift"
itemSubtitle = environment.strings.Stars_Intro_Transaction_GiftAuctionBid
itemGift = starGift
} else if item.flags.contains(.isStarGiftPrepaidUpgrade) {
itemTitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast)
itemSubtitle = environment.strings.Stars_Intro_Transaction_PrepaidGiftUpgrade
} else if item.flags.contains(.isStarGiftDropOriginalDetails), case let .unique(gift) = starGift {
itemTitle = "\(gift.title) #\(presentationStringsFormattedNumber(gift.number, environment.dateTimeFormat.groupingSeparator))"
itemSubtitle = environment.strings.Stars_Intro_Transaction_GiftDropOriginalDetails
itemGift = starGift
} else if item.flags.contains(.isStarGiftUpgrade), case let .unique(gift) = starGift {
itemTitle = "\(gift.title) #\(presentationStringsFormattedNumber(gift.number, environment.dateTimeFormat.groupingSeparator))"
itemSubtitle = environment.strings.Stars_Intro_Transaction_GiftUpgrade
itemGift = starGift
} else {
itemTitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast)
switch starGift {
case let .generic(gift):
itemFile = gift.file
itemSubtitle = item.count.amount > StarsAmount.zero ? environment.strings.Stars_Intro_Transaction_ConvertedGift : environment.strings.Stars_Intro_Transaction_Gift
case let .unique(gift):
for attribute in gift.attributes {
if case let .model(_, file, _) = attribute {
itemFile = file
break
}
}
if item.count.amount > StarsAmount.zero {
itemSubtitle = environment.strings.Stars_Intro_Transaction_GiftSale
} else {
if item.flags.contains(.isStarGiftResale) {
itemSubtitle = environment.strings.Stars_Intro_Transaction_GiftPurchase
} else {
itemSubtitle = environment.strings.Stars_Intro_Transaction_GiftTransfer
}
}
}
}
} else if let _ = item.giveawayMessageId {
itemTitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast)
itemSubtitle = environment.strings.Stars_Intro_Transaction_GiveawayPrize
} else if !item.media.isEmpty {
itemTitle = environment.strings.Stars_Intro_Transaction_MediaPurchase
itemSubtitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast)
} else if let title = item.title {
itemTitle = title
itemSubtitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast)
} else {
itemTitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast)
if item.flags.contains(.isReaction) {
itemSubtitle = environment.strings.Stars_Intro_Transaction_Reaction_Title
} else if item.flags.contains(.isGift) {
itemSubtitle = environment.strings.Stars_Intro_Transaction_Gift_Title
} else if let _ = item.subscriptionPeriod {
itemSubtitle = environment.strings.Stars_Intro_Transaction_SubscriptionFee_Title
} else {
itemSubtitle = nil
}
}
case .appStore:
itemTitle = environment.strings.Stars_Intro_Transaction_AppleTopUp_Title
itemSubtitle = environment.strings.Stars_Intro_Transaction_AppleTopUp_Subtitle
case .playMarket:
itemTitle = environment.strings.Stars_Intro_Transaction_GoogleTopUp_Title
itemSubtitle = environment.strings.Stars_Intro_Transaction_GoogleTopUp_Subtitle
case .fragment:
if component.isAccount {
if item.flags.contains(.isGift) {
itemTitle = environment.strings.Stars_Intro_Transaction_Gift_UnknownUser
itemSubtitle = environment.strings.Stars_Intro_Transaction_Gift_Title
itemPeer = .transactionPeer(.fragment)
} else {
if (item.count.amount.value < 0 && !item.flags.contains(.isRefund)) || (item.count.amount.value > 0 && item.flags.contains(.isRefund)) {
itemTitle = environment.strings.Stars_Intro_Transaction_FragmentWithdrawal_Title
itemSubtitle = environment.strings.Stars_Intro_Transaction_FragmentWithdrawal_Subtitle
} else {
itemTitle = environment.strings.Stars_Intro_Transaction_FragmentTopUp_Title
itemSubtitle = environment.strings.Stars_Intro_Transaction_FragmentTopUp_Subtitle
}
}
} else {
if item.count.amount > StarsAmount.zero && !item.flags.contains(.isRefund) {
itemTitle = environment.strings.Stars_Intro_Transaction_FragmentTopUp_Title
itemSubtitle = environment.strings.Stars_Intro_Transaction_FragmentTopUp_Subtitle
} else {
itemTitle = environment.strings.Stars_Intro_Transaction_FragmentWithdrawal_Title
itemSubtitle = environment.strings.Stars_Intro_Transaction_FragmentWithdrawal_Subtitle
}
}
case .premiumBot:
itemTitle = environment.strings.Stars_Intro_Transaction_PremiumBotTopUp_Title
itemSubtitle = environment.strings.Stars_Intro_Transaction_PremiumBotTopUp_Subtitle
case .ads:
itemTitle = environment.strings.Stars_Intro_Transaction_TelegramAds_Title
itemSubtitle = environment.strings.Stars_Intro_Transaction_TelegramAds_Subtitle
case .apiLimitExtension:
itemTitle = environment.strings.Stars_Intro_Transaction_TelegramBotApi_Title
if let floodskipNumber = item.floodskipNumber {
itemSubtitle = environment.strings.Stars_Intro_Transaction_TelegramBotApi_Messages(floodskipNumber)
} else {
itemSubtitle = nil
}
case .unsupported:
itemTitle = environment.strings.Stars_Intro_Transaction_Unsupported_Title
itemSubtitle = nil
}
let itemLabel: NSAttributedString
let formattedLabel = formatCurrencyAmountText(item.count, dateTimeFormat: environment.dateTimeFormat, showPlus: true)
let smallLabelFont = Font.with(size: floor(fontBaseDisplaySize / 17.0 * 13.0))
let labelFont = Font.medium(fontBaseDisplaySize)
let labelColor = formattedLabel.hasPrefix("-") ? environment.theme.list.itemDestructiveColor : environment.theme.list.itemDisclosureActions.constructive.fillColor
itemLabel = tonAmountAttributedString(formattedLabel, integralFont: labelFont, fractionalFont: smallLabelFont, color: labelColor, decimalSeparator: environment.dateTimeFormat.decimalSeparator)
let itemIconName: String
let itemIconColor: UIColor?
switch item.count.currency {
case .stars:
itemIconName = "Premium/Stars/StarMedium"
itemIconColor = nil
case .ton:
itemIconName = "Ads/TonAbout"
itemIconColor = labelColor
}
var itemDateColor = environment.theme.list.itemSecondaryTextColor
itemDate = stringForMediumCompactDate(timestamp: item.date, strings: environment.strings, dateTimeFormat: environment.dateTimeFormat)
if item.flags.contains(.isRefund) {
itemDate += " \(environment.strings.Stars_Intro_Transaction_Refund)"
} else if item.flags.contains(.isPending) {
itemDate += " \(environment.strings.Monetization_Transaction_Pending)"
} else if item.flags.contains(.isFailed) {
itemDate += " \(environment.strings.Monetization_Transaction_Failed)"
itemDateColor = environment.theme.list.itemDestructiveColor
}
var titleComponents: [AnyComponentWithIdentity<Empty>] = []
titleComponents.append(
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: itemTitle,
font: Font.semibold(fontBaseDisplaySize),
textColor: environment.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
)))
)
if let itemSubtitle {
var items: [AnyComponentWithIdentity<Empty>] = []
if let itemFile {
items.append(AnyComponentWithIdentity(id: "icon", component: AnyComponent(
GiftAnimationComponent(
context: component.context,
theme: environment.theme,
file: itemFile,
still: true,
size: CGSize(width: 20.0, height: 20.0)
)
)))
}
items.append(AnyComponentWithIdentity(id: "title", component: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(
string: itemSubtitle,
font: Font.regular(fontBaseDisplaySize * 16.0 / 17.0),
textColor: environment.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
)
)))
titleComponents.append(
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(HStack(items, spacing: 2.0)))
)
}
titleComponents.append(
AnyComponentWithIdentity(id: AnyHashable(2), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: itemDate,
font: Font.regular(floor(fontBaseDisplaySize * 14.0 / 17.0)),
textColor: itemDateColor
)),
maximumNumberOfLines: 1
)))
)
let _ = itemView.update(
transition: itemTransition,
component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack(titleComponents, alignment: .left, spacing: 2.0)),
contentInsets: UIEdgeInsets(top: 9.0, left: environment.containerInsets.left, bottom: 8.0, right: environment.containerInsets.right),
leftIcon: .custom(AnyComponentWithIdentity(id: "avatar", component: AnyComponent(StarsAvatarComponent(context: component.context, theme: environment.theme, peer: itemPeer, photo: item.photo, media: item.media, gift: itemGift, backgroundColor: environment.theme.list.plainBackgroundColor))), false),
icon: nil,
accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: "label", component: AnyComponent(StarsLabelComponent(text: itemLabel, iconName: itemIconName, iconColor: itemIconColor))), insets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 16.0))),
action: { [weak self] _ in
guard let self, let component = self.component else {
return
}
if !item.flags.contains(.isLocal) {
component.action(item)
}
},
updateIsHighlighted: { [weak self] _, highlighted in
guard let self else {
return
}
self.updateHighlightedItem(itemId: highlighted ? id : nil)
}
)),
environment: {},
containerSize: CGSize(width: itemLayout.containerWidth - itemLayout.containerInsets.left - itemLayout.containerInsets.right, height: itemLayout.itemHeight)
)
let itemFrame = itemLayout.itemFrame(for: index).offsetBy(dx: itemLayout.containerInsets.left, dy: 0.0)
if let itemComponentView = itemView.view {
if itemComponentView.superview == nil {
if !transition.animation.isImmediate {
transition.animateAlpha(view: itemComponentView, from: 0.0, to: 1.0)
}
self.scrollView.addSubview(itemComponentView)
}
itemTransition.setFrame(view: itemComponentView, frame: itemFrame)
}
let sideInset: CGFloat = 60.0 + environment.containerInsets.left
itemTransition.setFrame(layer: separatorLayer, frame: CGRect(x: sideInset, y: itemFrame.maxY, width: itemFrame.width - sideInset - environment.containerInsets.right, height: UIScreenPixel))
}
}
var removeIds: [AnyHashable] = []
for (id, itemView) in self.visibleItems {
if !validIds.contains(id) {
removeIds.append(id)
if let itemComponentView = itemView.view {
transition.setAlpha(view: itemComponentView, alpha: 0.0, completion: { [weak itemComponentView] _ in
itemComponentView?.removeFromSuperview()
})
}
}
}
for (id, separatorLayer) in self.separatorLayers {
if !validIds.contains(id) {
transition.setAlpha(layer: separatorLayer, alpha: 0.0, completion: { [weak separatorLayer] _ in
separatorLayer?.removeFromSuperlayer()
})
}
}
for id in removeIds {
self.visibleItems.removeValue(forKey: id)
}
let bottomOffset = self.environment?.externalBottomOffset ?? max(0.0, self.scrollView.contentSize.height - self.scrollView.contentOffset.y - self.scrollView.frame.height)
let loadMore = bottomOffset < 100.0
if environment.isCurrent, loadMore {
let lastId = self.items.last?.extendedId
if lastId != self.currentLoadMoreId || lastId == nil {
self.currentLoadMoreId = lastId
component.transactionsContext.loadMore()
}
}
}
private var isUpdating = false
func update(component: StarsTransactionsListPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<StarsTransactionsPanelEnvironment>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
self.component = component
if self.itemsDisposable == nil {
self.itemsDisposable = (component.transactionsContext.state
|> deliverOnMainQueue).start(next: { [weak self, weak state] status in
guard let self else {
return
}
let wasEmpty = self.items.isEmpty
let hadLocalTransactions = self.items.contains(where: { $0.flags.contains(.isLocal) })
var existingIds = Set<String>()
var filteredItems: [StarsContext.State.Transaction] = []
for transaction in status.transactions {
let id = transaction.extendedId
if !existingIds.contains(id) {
existingIds.insert(id)
filteredItems.append(transaction)
}
}
self.items = filteredItems
if !status.isLoading {
self.currentLoadMoreId = nil
}
if !self.isUpdating {
state?.updated(transition: wasEmpty || hadLocalTransactions ? .immediate : .easeInOut(duration: 0.2))
}
})
}
let environment = environment[StarsTransactionsPanelEnvironment.self].value
self.environment = environment
let fontBaseDisplaySize = 17.0
let measureItemSize = self.measureItem.update(
transition: .immediate,
component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "ABC",
font: Font.regular(fontBaseDisplaySize),
textColor: environment.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 0
))),
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "abc",
font: Font.regular(fontBaseDisplaySize * 16.0 / 17.0),
textColor: environment.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
))),
AnyComponentWithIdentity(id: AnyHashable(2), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "abc",
font: Font.regular(floor(fontBaseDisplaySize * 14.0 / 17.0)),
textColor: environment.theme.list.itemSecondaryTextColor
)),
maximumNumberOfLines: 0,
lineSpacing: 0.18
)))
], alignment: .left, spacing: 2.0)),
contentInsets: UIEdgeInsets(top: 9.0, left: 0.0, bottom: 8.0, right: 0.0),
leftIcon: nil,
icon: nil,
accessory: nil,
action: { _ in }
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 1000.0)
)
let itemLayout = ItemLayout(
containerInsets: environment.containerInsets,
containerWidth: availableSize.width,
itemHeight: measureItemSize.height,
itemCount: self.items.count
)
self.itemLayout = itemLayout
self.ignoreScrolling = true
let contentOffset = self.scrollView.bounds.minY
var scrollBounds = self.scrollView.bounds
if let _ = environment.externalScrollBounds {
scrollBounds.origin = CGPoint()
scrollBounds.size = CGSize(width: availableSize.width, height: itemLayout.contentHeight)
transition.setPosition(view: self.scrollView, position: scrollBounds.center)
} else {
transition.setPosition(view: self.scrollView, position: CGRect(origin: CGPoint(), size: availableSize).center)
scrollBounds.size = availableSize
if !environment.isScrollable {
scrollBounds.origin = CGPoint()
}
}
transition.setBounds(view: self.scrollView, bounds: scrollBounds)
self.scrollView.isScrollEnabled = environment.isScrollable
let contentSize = CGSize(width: availableSize.width, height: itemLayout.contentHeight)
if self.scrollView.contentSize != contentSize {
self.scrollView.contentSize = contentSize
}
self.scrollView.verticalScrollIndicatorInsets = environment.containerInsets
if !transition.animation.isImmediate && self.scrollView.bounds.minY != contentOffset {
let deltaOffset = self.scrollView.bounds.minY - contentOffset
transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: -deltaOffset), to: CGPoint(), additive: true)
}
self.ignoreScrolling = false
self.updateScrolling(transition: transition)
if let _ = environment.externalScrollBounds {
return CGSize(width: availableSize.width, height: contentSize.height)
} else {
return availableSize
}
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<StarsTransactionsPanelEnvironment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
func cancelContextGestures(view: UIView) {
if let gestureRecognizers = view.gestureRecognizers {
for gesture in gestureRecognizers {
if let gesture = gesture as? ContextGesture {
gesture.cancel()
}
}
}
for subview in view.subviews {
cancelContextGestures(view: subview)
}
}
@@ -0,0 +1,850 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import ComponentDisplayAdapters
import TelegramPresentationData
final class StarsTransactionsPanelContainerEnvironment: Equatable {
let isScrollable: Bool
init(
isScrollable: Bool
) {
self.isScrollable = isScrollable
}
static func ==(lhs: StarsTransactionsPanelContainerEnvironment, rhs: StarsTransactionsPanelContainerEnvironment) -> Bool {
if lhs.isScrollable != rhs.isScrollable {
return false
}
return true
}
}
final class StarsTransactionsPanelEnvironment: Equatable {
let theme: PresentationTheme
let strings: PresentationStrings
let dateTimeFormat: PresentationDateTimeFormat
let containerInsets: UIEdgeInsets
let isScrollable: Bool
let isCurrent: Bool
let externalScrollBounds: CGRect?
let externalBottomOffset: CGFloat?
init(
theme: PresentationTheme,
strings: PresentationStrings,
dateTimeFormat: PresentationDateTimeFormat,
containerInsets: UIEdgeInsets,
isScrollable: Bool,
isCurrent: Bool,
externalScrollBounds: CGRect? = nil,
externalBottomOffset: CGFloat? = nil
) {
self.theme = theme
self.strings = strings
self.dateTimeFormat = dateTimeFormat
self.containerInsets = containerInsets
self.isScrollable = isScrollable
self.isCurrent = isCurrent
self.externalScrollBounds = externalScrollBounds
self.externalBottomOffset = externalBottomOffset
}
static func ==(lhs: StarsTransactionsPanelEnvironment, rhs: StarsTransactionsPanelEnvironment) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.dateTimeFormat != rhs.dateTimeFormat {
return false
}
if lhs.containerInsets != rhs.containerInsets {
return false
}
if lhs.isScrollable != rhs.isScrollable {
return false
}
if lhs.isCurrent != rhs.isCurrent {
return false
}
if lhs.externalScrollBounds != rhs.externalScrollBounds {
return false
}
if lhs.externalBottomOffset != rhs.externalBottomOffset {
return false
}
return true
}
}
private final class StarsTransactionsHeaderItemComponent: CombinedComponent {
let theme: PresentationTheme
let title: String
let activityFraction: CGFloat
init(
theme: PresentationTheme,
title: String,
activityFraction: CGFloat
) {
self.theme = theme
self.title = title
self.activityFraction = activityFraction
}
static func ==(lhs: StarsTransactionsHeaderItemComponent, rhs: StarsTransactionsHeaderItemComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.activityFraction != rhs.activityFraction {
return false
}
return true
}
static var body: Body {
let activeText = Child(Text.self)
let inactiveText = Child(Text.self)
return { context in
let activeText = activeText.update(
component: Text(text: context.component.title, font: Font.medium(14.0), color: context.component.theme.list.itemAccentColor),
availableSize: context.availableSize,
transition: .immediate
)
let inactiveText = inactiveText.update(
component: Text(text: context.component.title, font: Font.medium(14.0), color: context.component.theme.list.itemSecondaryTextColor),
availableSize: context.availableSize,
transition: .immediate
)
context.add(activeText
.position(CGPoint(x: activeText.size.width * 0.5, y: activeText.size.height * 0.5))
.opacity(context.component.activityFraction)
)
context.add(inactiveText
.position(CGPoint(x: inactiveText.size.width * 0.5, y: inactiveText.size.height * 0.5))
.opacity(1.0 - context.component.activityFraction)
)
return activeText.size
}
}
}
private extension CGFloat {
func interpolate(with other: CGFloat, fraction: CGFloat) -> CGFloat {
let invT = 1.0 - fraction
let result = other * fraction + self * invT
return result
}
}
private extension CGPoint {
func interpolate(with other: CGPoint, fraction: CGFloat) -> CGPoint {
return CGPoint(x: self.x.interpolate(with: other.x, fraction: fraction), y: self.y.interpolate(with: other.y, fraction: fraction))
}
}
private extension CGSize {
func interpolate(with other: CGSize, fraction: CGFloat) -> CGSize {
return CGSize(width: self.width.interpolate(with: other.width, fraction: fraction), height: self.height.interpolate(with: other.height, fraction: fraction))
}
}
private extension CGRect {
func interpolate(with other: CGRect, fraction: CGFloat) -> CGRect {
return CGRect(origin: self.origin.interpolate(with: other.origin, fraction: fraction), size: self.size.interpolate(with: other.size, fraction: fraction))
}
}
private final class StarsTransactionsHeaderComponent: Component {
struct Item: Equatable {
let id: AnyHashable
let title: String
init(
id: AnyHashable,
title: String
) {
self.id = id
self.title = title
}
}
let theme: PresentationTheme
let items: [Item]
let activeIndex: Int
let transitionFraction: CGFloat
let switchToPanel: (AnyHashable) -> Void
init(
theme: PresentationTheme,
items: [Item],
activeIndex: Int,
transitionFraction: CGFloat,
switchToPanel: @escaping (AnyHashable) -> Void
) {
self.theme = theme
self.items = items
self.activeIndex = activeIndex
self.transitionFraction = transitionFraction
self.switchToPanel = switchToPanel
}
static func ==(lhs: StarsTransactionsHeaderComponent, rhs: StarsTransactionsHeaderComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.items != rhs.items {
return false
}
if lhs.activeIndex != rhs.activeIndex {
return false
}
if lhs.transitionFraction != rhs.transitionFraction {
return false
}
return true
}
class View: UIView {
private var component: StarsTransactionsHeaderComponent?
private var visibleItems: [AnyHashable: ComponentView<Empty>] = [:]
private let activeItemLayer: SimpleLayer
override init(frame: CGRect) {
self.activeItemLayer = SimpleLayer()
self.activeItemLayer.cornerRadius = 2.0
self.activeItemLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
super.init(frame: frame)
self.layer.addSublayer(self.activeItemLayer)
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
let point = recognizer.location(in: self)
var closestId: (CGFloat, AnyHashable)?
if self.bounds.contains(point) {
for (id, item) in self.visibleItems {
if let itemView = item.view {
let distance: CGFloat = min(abs(point.x - itemView.frame.minX), abs(point.x - itemView.frame.maxX))
if let closestIdValue = closestId {
if distance < closestIdValue.0 {
closestId = (distance, id)
}
} else {
closestId = (distance, id)
}
}
}
}
if let closestId = closestId, let component = self.component {
component.switchToPanel(closestId.1)
}
}
}
func update(component: StarsTransactionsHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let themeUpdated = self.component?.theme !== component.theme
self.component = component
var validIds = Set<AnyHashable>()
for i in 0 ..< component.items.count {
let item = component.items[i]
validIds.insert(item.id)
let itemView: ComponentView<Empty>
var itemTransition = transition
if let current = self.visibleItems[item.id] {
itemView = current
} else {
itemTransition = .immediate
itemView = ComponentView()
self.visibleItems[item.id] = itemView
}
let activeIndex: CGFloat = CGFloat(component.activeIndex) - component.transitionFraction
let activityDistance: CGFloat = abs(activeIndex - CGFloat(i))
let activityFraction: CGFloat
if activityDistance < 1.0 {
activityFraction = 1.0 - activityDistance
} else {
activityFraction = 0.0
}
let itemSize = itemView.update(
transition: itemTransition,
component: AnyComponent(StarsTransactionsHeaderItemComponent(
theme: component.theme,
title: item.title,
activityFraction: activityFraction
)),
environment: {},
containerSize: availableSize
)
let itemHorizontalSpace = availableSize.width / CGFloat(component.items.count)
let itemX: CGFloat
if component.items.count == 1 {
itemX = 37.0
} else {
itemX = itemHorizontalSpace * CGFloat(i) + floor((itemHorizontalSpace - itemSize.width) / 2.0)
}
let itemFrame = CGRect(origin: CGPoint(x: itemX, y: floor((availableSize.height - itemSize.height) / 2.0)), size: itemSize)
if let itemComponentView = itemView.view {
if itemComponentView.superview == nil {
self.addSubview(itemComponentView)
itemComponentView.isUserInteractionEnabled = false
}
itemTransition.setFrame(view: itemComponentView, frame: itemFrame)
}
}
if component.activeIndex < component.items.count {
let activeView = self.visibleItems[component.items[component.activeIndex].id]?.view
let nextIndex: Int
if component.transitionFraction > 0.0 {
nextIndex = max(0, component.activeIndex - 1)
} else {
nextIndex = min(component.items.count - 1, component.activeIndex + 1)
}
let nextView = self.visibleItems[component.items[nextIndex].id]?.view
if let activeView = activeView, let nextView = nextView {
let mergedFrame = activeView.frame.interpolate(with: nextView.frame, fraction: abs(component.transitionFraction))
transition.setFrame(layer: self.activeItemLayer, frame: CGRect(origin: CGPoint(x: mergedFrame.minX, y: availableSize.height - 3.0), size: CGSize(width: mergedFrame.width, height: 3.0)))
}
}
if themeUpdated {
self.activeItemLayer.backgroundColor = component.theme.list.itemAccentColor.cgColor
}
var removeIds: [AnyHashable] = []
for (id, itemView) in self.visibleItems {
if !validIds.contains(id) {
removeIds.append(id)
if let itemComponentView = itemView.view {
itemComponentView.removeFromSuperview()
}
}
}
for id in removeIds {
self.visibleItems.removeValue(forKey: id)
}
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
final class StarsTransactionsPanelContainerComponent: Component {
typealias EnvironmentType = StarsTransactionsPanelContainerEnvironment
struct Item: Equatable {
let id: AnyHashable
let title: String
let panel: AnyComponent<StarsTransactionsPanelEnvironment>
init(
id: AnyHashable,
title: String,
panel: AnyComponent<StarsTransactionsPanelEnvironment>
) {
self.id = id
self.title = title
self.panel = panel
}
}
let theme: PresentationTheme
let strings: PresentationStrings
let dateTimeFormat: PresentationDateTimeFormat
let insets: UIEdgeInsets
let items: [Item]
let currentPanelUpdated: (AnyHashable, ComponentTransition) -> Void
init(
theme: PresentationTheme,
strings: PresentationStrings,
dateTimeFormat: PresentationDateTimeFormat,
insets: UIEdgeInsets,
items: [Item],
currentPanelUpdated: @escaping (AnyHashable, ComponentTransition) -> Void
) {
self.theme = theme
self.strings = strings
self.dateTimeFormat = dateTimeFormat
self.insets = insets
self.items = items
self.currentPanelUpdated = currentPanelUpdated
}
static func ==(lhs: StarsTransactionsPanelContainerComponent, rhs: StarsTransactionsPanelContainerComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.dateTimeFormat != rhs.dateTimeFormat {
return false
}
if lhs.insets != rhs.insets {
return false
}
if lhs.items != rhs.items {
return false
}
return true
}
class View: UIView, UIGestureRecognizerDelegate {
private let topPanelClippingView: UIView
private let topPanelBackgroundView: UIView
private let topPanelMergedBackgroundView: UIView
private let topPanelSeparatorLayer: SimpleLayer
private let header = ComponentView<Empty>()
private var component: StarsTransactionsPanelContainerComponent?
private weak var state: EmptyComponentState?
private let panelsBackgroundLayer: SimpleLayer
private let clippingView: UIView
private var visiblePanels: [AnyHashable: ComponentView<StarsTransactionsPanelEnvironment>] = [:]
private var actualVisibleIds = Set<AnyHashable>()
private var currentId: AnyHashable?
private var transitionFraction: CGFloat = 0.0
private var animatingTransition: Bool = false
override init(frame: CGRect) {
self.topPanelClippingView = UIView()
self.topPanelClippingView.clipsToBounds = true
self.topPanelClippingView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
self.topPanelBackgroundView = UIView()
self.topPanelMergedBackgroundView = UIView()
self.topPanelMergedBackgroundView.alpha = 0.0
self.topPanelSeparatorLayer = SimpleLayer()
self.panelsBackgroundLayer = SimpleLayer()
self.clippingView = UIView()
self.clippingView.clipsToBounds = true
super.init(frame: frame)
self.layer.addSublayer(self.panelsBackgroundLayer)
self.addSubview(self.clippingView)
self.addSubview(self.topPanelClippingView)
self.topPanelClippingView.addSubview(self.topPanelBackgroundView)
self.topPanelClippingView.addSubview(self.topPanelMergedBackgroundView)
self.layer.addSublayer(self.topPanelSeparatorLayer)
let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] point in
guard let self, let component = self.component, let currentId = self.currentId else {
return []
}
guard let index = component.items.firstIndex(where: { $0.id == currentId }) else {
return []
}
/*if strongSelf.tabsContainerNode.bounds.contains(strongSelf.view.convert(point, to: strongSelf.tabsContainerNode.view)) {
return []
}*/
if index == 0 {
return .left
}
return [.left, .right]
})
panRecognizer.delegate = self
panRecognizer.delaysTouchesBegan = false
panRecognizer.cancelsTouchesInView = true
self.addGestureRecognizer(panRecognizer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var currentPanelView: UIView? {
guard let currentId = self.currentId, let panel = self.visiblePanels[currentId] else {
return nil
}
return panel.view
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if let _ = otherGestureRecognizer as? InteractiveTransitionGestureRecognizer {
return false
}
if let _ = otherGestureRecognizer as? UIPanGestureRecognizer {
return true
}
return false
}
@objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
switch recognizer.state {
case .began:
func cancelContextGestures(view: UIView) {
if let gestureRecognizers = view.gestureRecognizers {
for gesture in gestureRecognizers {
if let gesture = gesture as? ContextGesture {
gesture.cancel()
}
}
}
for subview in view.subviews {
cancelContextGestures(view: subview)
}
}
cancelContextGestures(view: self)
//self.animatingTransition = true
case .changed:
guard let component = self.component, let currentId = self.currentId else {
return
}
guard let index = component.items.firstIndex(where: { $0.id == currentId }) else {
return
}
let translation = recognizer.translation(in: self)
var transitionFraction = translation.x / self.bounds.width
if index <= 0 {
transitionFraction = min(0.0, transitionFraction)
}
if index >= component.items.count - 1 {
transitionFraction = max(0.0, transitionFraction)
}
self.transitionFraction = transitionFraction
self.state?.updated(transition: .immediate)
case .cancelled, .ended:
guard let component = self.component, let currentId = self.currentId else {
return
}
guard let index = component.items.firstIndex(where: { $0.id == currentId }) else {
return
}
let translation = recognizer.translation(in: self)
let velocity = recognizer.velocity(in: self)
var directionIsToRight: Bool?
if abs(velocity.x) > 10.0 {
directionIsToRight = velocity.x < 0.0
} else {
if abs(translation.x) > self.bounds.width / 2.0 {
directionIsToRight = translation.x > self.bounds.width / 2.0
}
}
if let directionIsToRight = directionIsToRight {
var updatedIndex = index
if directionIsToRight {
updatedIndex = min(updatedIndex + 1, component.items.count - 1)
} else {
updatedIndex = max(updatedIndex - 1, 0)
}
self.currentId = component.items[updatedIndex].id
}
self.transitionFraction = 0.0
let transition = ComponentTransition(animation: .curve(duration: 0.35, curve: .spring))
if let currentId = self.currentId {
self.state?.updated(transition: transition)
component.currentPanelUpdated(currentId, transition)
}
self.animatingTransition = false
//self.currentPaneUpdated?(false)
//self.currentPaneStatusPromise.set(self.currentPane?.node.status ?? .single(nil))
default:
break
}
}
func updateNavigationMergeFactor(value: CGFloat, transition: ComponentTransition) {
transition.setAlpha(view: self.topPanelMergedBackgroundView, alpha: value)
transition.setAlpha(view: self.topPanelBackgroundView, alpha: 1.0 - value)
}
func transferVelocity(_ velocity: CGFloat) {
if let currentPanelView = self.currentPanelView as? StarsTransactionsListPanelComponent.View {
currentPanelView.transferVelocity(velocity)
}
}
func scrollToTop() -> Bool {
if let currentPanelView = self.currentPanelView as? StarsTransactionsListPanelComponent.View {
return currentPanelView.scrollToTop()
}
return false
}
func update(component: StarsTransactionsPanelContainerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<StarsTransactionsPanelContainerEnvironment>, transition: ComponentTransition) -> CGSize {
let environment = environment[StarsTransactionsPanelContainerEnvironment.self].value
let themeUpdated = self.component?.theme !== component.theme
self.component = component
self.state = state
if themeUpdated {
self.panelsBackgroundLayer.backgroundColor = component.theme.list.itemBlocksBackgroundColor.cgColor
self.topPanelSeparatorLayer.backgroundColor = component.theme.list.itemBlocksSeparatorColor.cgColor
self.topPanelBackgroundView.backgroundColor = component.theme.list.itemBlocksBackgroundColor
self.topPanelMergedBackgroundView.backgroundColor = component.theme.rootController.navigationBar.blurredBackgroundColor
}
let topPanelCoverHeight: CGFloat = 10.0
let containerWidth = availableSize.width - component.insets.left - component.insets.right
let topPanelFrame = CGRect(origin: CGPoint(x: component.insets.left, y: -topPanelCoverHeight), size: CGSize(width: containerWidth, height: 44.0))
transition.setFrame(view: self.topPanelClippingView, frame: topPanelFrame)
transition.setFrame(view: self.topPanelBackgroundView, frame: CGRect(origin: .zero, size: topPanelFrame.size))
transition.setFrame(view: self.topPanelMergedBackgroundView, frame: CGRect(origin: .zero, size: topPanelFrame.size))
transition.setCornerRadius(layer: self.topPanelClippingView.layer, cornerRadius: component.insets.left > 0.0 ? 26.0 : 0.0)
transition.setFrame(layer: self.panelsBackgroundLayer, frame: CGRect(origin: CGPoint(x: component.insets.left, y: topPanelFrame.maxY), size: CGSize(width: containerWidth, height: availableSize.height - topPanelFrame.maxY)))
transition.setFrame(layer: self.topPanelSeparatorLayer, frame: CGRect(origin: CGPoint(x: component.insets.left, y: topPanelFrame.maxY), size: CGSize(width: containerWidth, height: UIScreenPixel)))
if let currentIdValue = self.currentId, !component.items.contains(where: { $0.id == currentIdValue }) {
self.currentId = nil
}
if self.currentId == nil {
self.currentId = component.items.first?.id
}
var visibleIds = Set<AnyHashable>()
var currentIndex: Int?
if let currentId = self.currentId {
visibleIds.insert(currentId)
if let index = component.items.firstIndex(where: { $0.id == currentId }) {
currentIndex = index
if index != 0 {
visibleIds.insert(component.items[index - 1].id)
}
if index != component.items.count - 1 {
visibleIds.insert(component.items[index + 1].id)
}
}
}
let sideInset: CGFloat = 16.0 + component.insets.left
let condensedPanelWidth: CGFloat = availableSize.width - sideInset * 2.0
let headerSize = self.header.update(
transition: transition,
component: AnyComponent(StarsTransactionsHeaderComponent(
theme: component.theme,
items: component.items.map { item -> StarsTransactionsHeaderComponent.Item in
return StarsTransactionsHeaderComponent.Item(
id: item.id,
title: item.title
)
},
activeIndex: currentIndex ?? 0,
transitionFraction: self.transitionFraction,
switchToPanel: { [weak self] id in
guard let self, let component = self.component else {
return
}
if component.items.contains(where: { $0.id == id }) {
self.currentId = id
let transition = ComponentTransition(animation: .curve(duration: 0.35, curve: .spring))
self.state?.updated(transition: transition)
component.currentPanelUpdated(id, transition)
}
}
)),
environment: {},
containerSize: CGSize(width: condensedPanelWidth, height: topPanelFrame.size.height)
)
if let headerView = self.header.view {
if headerView.superview == nil {
self.addSubview(headerView)
}
transition.setFrame(view: headerView, frame: CGRect(origin: topPanelFrame.origin.offsetBy(dx: 16.0, dy: 0.0), size: headerSize))
}
let centralPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY), size: CGSize(width: availableSize.width, height: availableSize.height - topPanelFrame.maxY))
if self.animatingTransition {
visibleIds = visibleIds.filter({ self.visiblePanels[$0] != nil })
}
self.actualVisibleIds = visibleIds
for (id, _) in self.visiblePanels {
visibleIds.insert(id)
}
var validIds = Set<AnyHashable>()
if let currentIndex {
var anyAnchorOffset: CGFloat = 0.0
for (id, panel) in self.visiblePanels {
guard let itemIndex = component.items.firstIndex(where: { $0.id == id }), let panelView = panel.view else {
continue
}
var itemFrame = centralPanelFrame.offsetBy(dx: self.transitionFraction * availableSize.width, dy: 0.0)
if itemIndex < currentIndex {
itemFrame.origin.x -= itemFrame.width
} else if itemIndex > currentIndex {
itemFrame.origin.x += itemFrame.width
}
anyAnchorOffset = itemFrame.minX - panelView.frame.minX
break
}
for id in visibleIds {
guard let itemIndex = component.items.firstIndex(where: { $0.id == id }) else {
continue
}
let panelItem = component.items[itemIndex]
var itemFrame = centralPanelFrame.offsetBy(dx: self.transitionFraction * availableSize.width, dy: 0.0)
if itemIndex < currentIndex {
itemFrame.origin.x -= itemFrame.width
} else if itemIndex > currentIndex {
itemFrame.origin.x += itemFrame.width
}
validIds.insert(panelItem.id)
let panel: ComponentView<StarsTransactionsPanelEnvironment>
var panelTransition = transition
var animateInIfNeeded = false
if let current = self.visiblePanels[panelItem.id] {
panel = current
if let panelView = panel.view, !panelView.bounds.isEmpty {
var wasHidden = false
if abs(panelView.frame.minX - availableSize.width) < .ulpOfOne || abs(panelView.frame.maxX - 0.0) < .ulpOfOne {
wasHidden = true
}
var isHidden = false
if abs(itemFrame.minX - availableSize.width) < .ulpOfOne || abs(itemFrame.maxX - 0.0) < .ulpOfOne {
isHidden = true
}
if wasHidden && isHidden {
panelTransition = .immediate
}
}
} else {
panelTransition = .immediate
animateInIfNeeded = true
panel = ComponentView()
self.visiblePanels[panelItem.id] = panel
}
let childEnvironment = StarsTransactionsPanelEnvironment(
theme: component.theme,
strings: component.strings,
dateTimeFormat: component.dateTimeFormat,
containerInsets: UIEdgeInsets(top: 0.0, left: component.insets.left, bottom: component.insets.bottom, right: component.insets.right),
isScrollable: environment.isScrollable,
isCurrent: self.currentId == panelItem.id
)
let _ = panel.update(
transition: panelTransition,
component: panelItem.panel,
environment: {
childEnvironment
},
containerSize: centralPanelFrame.size
)
if let panelView = panel.view {
if panelView.superview == nil {
self.clippingView.addSubview(panelView)
}
panelTransition.setFrame(view: panelView, frame: itemFrame, completion: { [weak self] _ in
guard let self else {
return
}
if !self.actualVisibleIds.contains(id) {
if let panel = self.visiblePanels[id] {
self.visiblePanels.removeValue(forKey: id)
panel.view?.removeFromSuperview()
}
}
})
if animateInIfNeeded && anyAnchorOffset != 0.0 {
transition.animatePosition(view: panelView, from: CGPoint(x: -anyAnchorOffset, y: 0.0), to: CGPoint(), additive: true)
}
}
}
}
let clippingFrame = CGRect(origin: CGPoint(x: component.insets.left, y: 0.0), size: CGSize(width: availableSize.width - component.insets.left - component.insets.right, height: availableSize.height))
transition.setPosition(view: self.clippingView, position: clippingFrame.center)
transition.setBounds(view: self.clippingView, bounds: CGRect(origin: CGPoint(x: component.insets.left, y: 0.0), size: clippingFrame.size))
var removeIds: [AnyHashable] = []
for (id, panel) in self.visiblePanels {
if !validIds.contains(id) {
removeIds.append(id)
if let panelView = panel.view {
panelView.removeFromSuperview()
}
}
}
for id in removeIds {
self.visiblePanels.removeValue(forKey: id)
}
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<StarsTransactionsPanelContainerEnvironment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,44 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "StarsTransferScreen",
module_name = "StarsTransferScreen",
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/Components/BundleIconComponent",
"//submodules/TelegramPresentationData",
"//submodules/AccountContext",
"//submodules/AppBundle",
"//submodules/ItemListUI",
"//submodules/TelegramStringFormatting",
"//submodules/PresentationDataUtils",
"//submodules/Components/SheetComponent",
"//submodules/UndoUI",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/TelegramUI/Components/GlassBarButtonComponent",
"//submodules/TelegramUI/Components/ListSectionComponent",
"//submodules/TelegramUI/Components/ListActionItemComponent",
"//submodules/TelegramUI/Components/Stars/StarsImageComponent",
"//submodules/TelegramUI/Components/PremiumPeerShortcutComponent",
"//submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent",
"//submodules/ConfettiEffect",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,900 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import SwiftSignalKit
import TelegramCore
import Markdown
import TextFormat
import TelegramPresentationData
import ViewControllerComponent
import SheetComponent
import BalancedTextComponent
import MultilineTextComponent
import BundleIconComponent
import ButtonComponent
import ItemListUI
import UndoUI
import AccountContext
import PresentationDataUtils
import StarsImageComponent
import ConfettiEffect
import PremiumPeerShortcutComponent
import StarsBalanceOverlayComponent
import GlassBarButtonComponent
import TelegramStringFormatting
private final class SheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let starsContext: StarsContext
let invoice: TelegramMediaInvoice
let source: BotPaymentInvoiceSource
let extendedMedia: [TelegramExtendedMedia]
let inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?, EnginePeer?)?, NoError>
let navigateToPeer: (EnginePeer) -> Void
let dismiss: () -> Void
init(
context: AccountContext,
starsContext: StarsContext,
invoice: TelegramMediaInvoice,
source: BotPaymentInvoiceSource,
extendedMedia: [TelegramExtendedMedia],
inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?, EnginePeer?)?, NoError>,
navigateToPeer: @escaping (EnginePeer) -> Void,
dismiss: @escaping () -> Void
) {
self.context = context
self.starsContext = starsContext
self.invoice = invoice
self.source = source
self.extendedMedia = extendedMedia
self.inputData = inputData
self.navigateToPeer = navigateToPeer
self.dismiss = dismiss
}
static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.invoice != rhs.invoice {
return false
}
if lhs.extendedMedia != rhs.extendedMedia {
return false
}
return true
}
final class State: ComponentState {
var cachedStarImage: (UIImage, PresentationTheme)?
private let context: AccountContext
private let starsContext: StarsContext
private let source: BotPaymentInvoiceSource
private let extendedMedia: [TelegramExtendedMedia]
private let invoice: TelegramMediaInvoice
private(set) var botPeer: EnginePeer?
private(set) var chatPeer: EnginePeer?
private(set) var authorPeer: EnginePeer?
private var peerDisposable: Disposable?
private(set) var balance: StarsAmount?
private(set) var form: BotPaymentForm?
private(set) var navigateToPeer: (EnginePeer) -> Void
private var stateDisposable: Disposable?
private var optionsDisposable: Disposable?
private(set) var options: [StarsTopUpOption] = [] {
didSet {
self.optionsPromise.set(self.options)
}
}
private let optionsPromise = ValuePromise<[StarsTopUpOption]?>(nil)
var inProgress = false
init(
context: AccountContext,
starsContext: StarsContext,
source: BotPaymentInvoiceSource,
extendedMedia: [TelegramExtendedMedia],
invoice: TelegramMediaInvoice,
inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?, EnginePeer?)?, NoError>,
navigateToPeer: @escaping (EnginePeer) -> Void
) {
self.context = context
self.starsContext = starsContext
self.source = source
self.extendedMedia = extendedMedia
self.invoice = invoice
self.navigateToPeer = navigateToPeer
super.init()
let chatPeer: Signal<EnginePeer?, NoError>
if case let .message(messageId) = source {
chatPeer = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: messageId.peerId))
} else {
chatPeer = .single(nil)
}
self.peerDisposable = (combineLatest(
inputData,
chatPeer
)
|> deliverOnMainQueue).start(next: { [weak self] inputData, chatPeer in
guard let self else {
return
}
self.balance = inputData?.0.balance ?? StarsAmount.zero
self.form = inputData?.1
self.botPeer = inputData?.2
self.chatPeer = chatPeer
self.authorPeer = inputData?.3
self.updated(transition: .immediate)
if self.optionsDisposable == nil, let balance = self.balance, balance < StarsAmount(value: self.invoice.totalAmount, nanos: 0) {
self.optionsDisposable = (context.engine.payments.starsTopUpOptions()
|> deliverOnMainQueue).start(next: { [weak self] options in
guard let self else {
return
}
self.options = options
})
}
})
self.stateDisposable = (starsContext.state
|> deliverOnMainQueue).start(next: { [weak self] state in
guard let self else {
return
}
self.balance = state?.balance
self.updated(transition: .immediate)
})
}
deinit {
self.peerDisposable?.dispose()
self.stateDisposable?.dispose()
self.optionsDisposable?.dispose()
}
func buy(requestTopUp: @escaping (@escaping () -> Void) -> Void, completion: @escaping (Bool) -> Void) {
guard let form, let balance else {
return
}
let navigateToPeer = self.navigateToPeer
let action = { [weak self] in
guard let self else {
return
}
self.inProgress = true
self.updated()
let _ = (self.context.engine.payments.sendStarsPaymentForm(formId: form.id, source: self.source)
|> deliverOnMainQueue).start(next: { [weak self] _ in
guard let self else {
return
}
completion(true)
if case let .starsChatSubscription(link) = self.source {
let _ = (self.context.engine.peers.joinLinkInformation(link)
|> deliverOnMainQueue).startStandalone(next: { result in
if case let .alreadyJoined(peer) = result {
navigateToPeer(peer)
}
})
}
}, error: { [weak self] error in
guard let self else {
return
}
switch error {
case .alreadyPaid:
if !self.extendedMedia.isEmpty, case let .message(messageId) = self.source {
let _ = self.context.engine.messages.updateExtendedMedia(messageIds: [messageId]).startStandalone()
}
default:
break
}
completion(false)
})
}
if balance < StarsAmount(value: self.invoice.totalAmount, nanos: 0) {
if self.options.isEmpty {
self.inProgress = true
self.updated()
}
let _ = (self.optionsPromise.get()
|> filter { $0 != nil }
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { [weak self] _ in
if let self {
self.inProgress = false
self.updated()
requestTopUp({ [weak self] in
guard let self else {
return
}
self.inProgress = true
self.updated()
let _ = (self.starsContext.state
|> filter { state in
if let state {
return !state.flags.contains(.isPendingBalance)
}
return false
}
|> take(1)
|> deliverOnMainQueue).start(next: { _ in
Queue.mainQueue().after(0.1, { [weak self] in
if let self, let balance = self.balance, balance < StarsAmount(value: self.invoice.totalAmount, nanos: 0) {
self.inProgress = false
self.updated()
self.buy(requestTopUp: requestTopUp, completion: completion)
} else {
action()
}
})
})
})
}
})
} else {
action()
}
}
}
func makeState() -> State {
return State(context: self.context, starsContext: self.starsContext, source: self.source, extendedMedia: self.extendedMedia, invoice: self.invoice, inputData: self.inputData, navigateToPeer: self.navigateToPeer)
}
static var body: Body {
let background = Child(RoundedRectangle.self)
let star = Child(StarsImageComponent.self)
let closeButton = Child(GlassBarButtonComponent.self)
let title = Child(Text.self)
let peerShortcut = Child(PremiumPeerShortcutComponent.self)
let text = Child(BalancedTextComponent.self)
let button = Child(ButtonComponent.self)
let balanceTitle = Child(MultilineTextComponent.self)
let balanceValue = Child(MultilineTextComponent.self)
let balanceIcon = Child(BundleIconComponent.self)
let info = Child(BalancedTextComponent.self)
return { context in
let environment = context.environment[EnvironmentType.self]
let component = context.component
let state = context.state
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let theme = presentationData.theme
let strings = presentationData.strings
var contentSize = CGSize(width: context.availableSize.width, height: 18.0)
let background = background.update(
component: RoundedRectangle(color: theme.actionSheet.opaqueItemBackgroundColor, cornerRadius: 8.0),
availableSize: CGSize(width: context.availableSize.width, height: 1000.0),
transition: .immediate
)
context.add(background
.position(CGPoint(x: context.availableSize.width / 2.0, y: background.size.height / 2.0))
)
var isExtendedMedia = false
let subject: StarsImageComponent.Subject
if !component.extendedMedia.isEmpty {
subject = .extendedMedia(component.extendedMedia)
isExtendedMedia = true
} else if let peer = state.botPeer {
if let photo = component.invoice.photo {
subject = .photo(photo)
} else {
subject = .transactionPeer(.peer(peer))
}
} else {
subject = .none
}
var isBot = false
if case let .user(user) = state.botPeer, user.botInfo != nil {
isBot = true
}
var isSubscription = false
if case .starsChatSubscription = component.source {
isSubscription = true
} else if let _ = component.invoice.subscriptionPeriod {
isSubscription = true
}
let star = star.update(
component: StarsImageComponent(
context: component.context,
subject: subject,
theme: theme,
diameter: 90.0,
backgroundColor: theme.actionSheet.opaqueItemBackgroundColor,
icon: isSubscription && !isBot ? .star : nil,
value: isBot ? component.invoice.totalAmount : nil
),
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: star.size.height / 2.0 - 27.0))
)
let closeButton = closeButton.update(
component: GlassBarButtonComponent(
size: CGSize(width: 40.0, height: 40.0),
backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor,
isDark: theme.overallDarkAppearance,
state: .generic,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: theme.rootController.navigationBar.glassBarButtonForegroundColor
)
)),
action: { _ in
component.dismiss()
}
),
availableSize: CGSize(width: 40.0, height: 40.0),
transition: .immediate
)
context.add(closeButton
.position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0))
)
let constrainedTitleWidth = context.availableSize.width - 16.0 * 2.0
contentSize.height += 126.0
let titleString: String
if isSubscription {
if isBot {
titleString = component.invoice.title
} else {
titleString = strings.Stars_Transfer_Subscribe_Channel_Title
}
} else {
titleString = strings.Stars_Transfer_Title
}
let title = title.update(
component: Text(text: titleString, font: Font.bold(24.0), color: theme.list.itemPrimaryTextColor),
availableSize: CGSize(width: constrainedTitleWidth, 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 += 13.0
if isBot && !isExtendedMedia, let peer = state.botPeer {
contentSize.height -= 3.0
let peerShortcut = peerShortcut.update(
component: PremiumPeerShortcutComponent(
context: component.context,
theme: theme,
peer: peer
),
availableSize: CGSize(width: context.availableSize.width - 32.0, height: context.availableSize.height),
transition: .immediate
)
context.add(peerShortcut
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + peerShortcut.size.height / 2.0))
)
contentSize.height += peerShortcut.size.height
contentSize.height += 13.0
}
let textFont = Font.regular(15.0)
let boldTextFont = Font.semibold(15.0)
let textColor = theme.actionSheet.primaryTextColor
let linkColor = theme.actionSheet.controlAccentColor
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 amount = component.invoice.totalAmount
let infoText: String
if case .starsChatSubscription = context.component.source {
infoText = strings.Stars_Transfer_SubscribeInfo(state.botPeer?.compactDisplayTitle ?? "", strings.Stars_Transfer_Info_Stars(Int32(clamping: amount))).string
} else if let _ = component.invoice.subscriptionPeriod {
infoText = strings.Stars_Transfer_BotSubscribeInfo(component.invoice.title, state.botPeer?.compactDisplayTitle ?? "", strings.Stars_Transfer_BotSubscribeInfo_Stars(Int32(clamping: amount))).string
} else if !component.extendedMedia.isEmpty {
var description: String = ""
var photoCount: Int32 = 0
var videoCount: Int32 = 0
for media in component.extendedMedia {
if case let .preview(_, _, videoDuration) = media, videoDuration != nil {
videoCount += 1
} else {
photoCount += 1
}
}
if photoCount > 0 && videoCount > 0 {
description = strings.Stars_Transfer_MediaAnd("**\(strings.Stars_Transfer_Photos(photoCount))**", "**\(strings.Stars_Transfer_Videos(videoCount))**").string
} else if photoCount > 0 {
if photoCount > 1 {
description += "**\(strings.Stars_Transfer_Photos(photoCount))**"
} else {
description += "**\(strings.Stars_Transfer_SinglePhoto)**"
}
} else if videoCount > 0 {
if videoCount > 1 {
description += "**\(strings.Stars_Transfer_Videos(videoCount))**"
} else {
description += "**\(strings.Stars_Transfer_SingleVideo)**"
}
}
if let authorPeerName = state.authorPeer?.compactDisplayTitle {
infoText = strings.Stars_Transfer_UnlockBotInfo(
description,
authorPeerName,
strings.Stars_Transfer_Info_Stars(Int32(clamping: amount))
).string
} else if let botPeerName = state.botPeer?.compactDisplayTitle {
infoText = strings.Stars_Transfer_UnlockBotInfo(
description,
botPeerName,
strings.Stars_Transfer_Info_Stars(Int32(clamping: amount))
).string
} else {
infoText = strings.Stars_Transfer_UnlockInfo(
description,
state.chatPeer?.compactDisplayTitle ?? "",
strings.Stars_Transfer_Info_Stars(Int32(clamping: amount))
).string
}
} else {
infoText = strings.Stars_Transfer_Info(
component.invoice.title,
state.botPeer?.compactDisplayTitle ?? "",
strings.Stars_Transfer_Info_Stars(Int32(clamping: amount))
).string
}
let text = text.update(
component: BalancedTextComponent(
text: .markdown(
text: infoText,
attributes: markdownAttributes
),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.2
),
availableSize: CGSize(width: constrainedTitleWidth, 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 += 28.0
let balanceTitle = balanceTitle.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.Stars_Transfer_Balance,
font: Font.regular(14.0),
textColor: textColor
)),
maximumNumberOfLines: 1
),
availableSize: context.availableSize,
transition: .immediate
)
let smallLabelFont = Font.regular(11.0)
let labelFont = Font.semibold(14.0)
let formattedBalance = formatStarsAmountText(state.balance ?? StarsAmount.zero, dateTimeFormat: environment.dateTimeFormat)
let balanceText = tonAmountAttributedString(formattedBalance, integralFont: labelFont, fractionalFont: smallLabelFont, color: textColor, decimalSeparator: environment.dateTimeFormat.decimalSeparator)
let balanceValue = balanceValue.update(
component: MultilineTextComponent(
text: .plain(balanceText),
maximumNumberOfLines: 1
),
availableSize: context.availableSize,
transition: .immediate
)
let balanceIcon = balanceIcon.update(
component: BundleIconComponent(name: "Premium/Stars/StarSmall", tintColor: nil),
availableSize: context.availableSize,
transition: .immediate
)
let topBalanceOriginY = 19.0
context.add(balanceTitle
.position(CGPoint(x: context.availableSize.width - 16.0 - environment.safeInsets.left - balanceTitle.size.width / 2.0, y: topBalanceOriginY + balanceTitle.size.height / 2.0))
)
context.add(balanceIcon
.position(CGPoint(x: context.availableSize.width - 16.0 - environment.safeInsets.left - balanceIcon.size.width / 2.0 - balanceValue.size.width - 3.0, y: topBalanceOriginY + balanceTitle.size.height + balanceValue.size.height / 2.0 + 1.0 + UIScreenPixel))
)
context.add(balanceValue
.position(CGPoint(x: context.availableSize.width - 16.0 - environment.safeInsets.left - balanceValue.size.width / 2.0, y: topBalanceOriginY + balanceTitle.size.height + balanceValue.size.height / 2.0))
)
if state.cachedStarImage == nil || state.cachedStarImage?.1 !== theme {
state.cachedStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: theme.list.itemCheckColors.foregroundColor)!, theme)
}
let amountString = presentationStringsFormattedNumber(Int32(amount), presentationData.dateTimeFormat.groupingSeparator)
let buttonAttributedString: NSMutableAttributedString
if case .starsChatSubscription = component.source {
buttonAttributedString = NSMutableAttributedString(string: "\(strings.Stars_Transfer_SubscribeFor) # \(amountString) \(strings.Stars_Transfer_SubscribePerMonth)", font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)
} else if let _ = component.invoice.subscriptionPeriod {
buttonAttributedString = NSMutableAttributedString(string: "\(strings.Stars_Transfer_SubscribeFor) # \(amountString) \(strings.Stars_Transfer_SubscribePerMonth)", font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)
} else {
buttonAttributedString = NSMutableAttributedString(string: "\(strings.Stars_Transfer_Pay) # \(amountString)", font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)
}
if let range = buttonAttributedString.string.range(of: "#"), let starImage = state.cachedStarImage?.0 {
buttonAttributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: buttonAttributedString.string))
buttonAttributedString.addAttribute(.foregroundColor, value: environment.theme.list.itemCheckColors.foregroundColor, range: NSRange(range, in: buttonAttributedString.string))
buttonAttributedString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: buttonAttributedString.string))
}
let controller = environment.controller() as? StarsTransferScreen
let accountContext = component.context
let starsContext = component.starsContext
let botTitle = state.botPeer?.compactDisplayTitle ?? ""
let invoice = component.invoice
let isMedia = !component.extendedMedia.isEmpty
let buttonSideInset: CGFloat = 30.0
let button = button.update(
component: ButtonComponent(
background: ButtonComponent.Background(
style: .glass,
color: theme.list.itemCheckColors.fillColor,
foreground: theme.list.itemCheckColors.foregroundColor,
pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
),
content: AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString)))
),
isEnabled: true,
displaysProgress: state.inProgress,
action: { [weak state, weak controller] in
state?.buy(requestTopUp: { [weak controller] completion in
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: accountContext.currentAppConfiguration.with { $0 })
if !premiumConfiguration.isPremiumDisabled {
let purpose: StarsPurchasePurpose
if isMedia {
purpose = .unlockMedia(requiredStars: invoice.totalAmount)
} else if let peerId = state?.botPeer?.id {
purpose = .transfer(peerId: peerId, requiredStars: invoice.totalAmount)
} else {
purpose = .generic
}
let purchaseController = accountContext.sharedContext.makeStarsPurchaseScreen(
context: accountContext,
starsContext: starsContext,
options: state?.options ?? [],
purpose: purpose,
targetPeerId: nil,
customTheme: nil,
completion: { [weak starsContext] stars in
guard let starsContext else {
return
}
starsContext.add(balance: StarsAmount(value: stars, nanos: 0))
let _ = (starsContext.onUpdate
|> deliverOnMainQueue).start(next: {
completion()
})
}
)
controller?.push(purchaseController)
} else {
let alertController = textAlertController(context: accountContext, title: nil, text: presentationData.strings.Stars_Transfer_Unavailable, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])
controller?.present(alertController, in: .window(.root))
}
}, completion: { [weak controller] success in
if success {
let presentationData = accountContext.sharedContext.currentPresentationData.with { $0 }
var title = presentationData.strings.Stars_Transfer_PurchasedTitle
let text: String
if isSubscription {
title = presentationData.strings.Stars_Transfer_Subscribe_Successful_Title
text = presentationData.strings.Stars_Transfer_Subscribe_Successful_Text(presentationData.strings.Stars_Transfer_Purchased_Stars(Int32(clamping: invoice.totalAmount)), botTitle).string
} else if let _ = component.invoice.extendedMedia {
text = presentationData.strings.Stars_Transfer_UnlockedText( presentationData.strings.Stars_Transfer_Purchased_Stars(Int32(clamping: invoice.totalAmount))).string
} else {
text = presentationData.strings.Stars_Transfer_PurchasedText(invoice.title, botTitle, presentationData.strings.Stars_Transfer_Purchased_Stars(Int32(clamping: invoice.totalAmount))).string
}
if let navigationController = controller?.navigationController {
Queue.mainQueue().after(0.5) {
if let lastController = navigationController.viewControllers.last as? ViewController {
let resultController = UndoOverlayController(
presentationData: presentationData,
content: .universal(
animation: "StarsSend",
scale: 0.066,
colors: [:],
title: title,
text: text,
customUndoText: nil,
timeout: nil
),
elevatedLayout: lastController is ChatController,
action: { _ in return true}
)
lastController.present(resultController, in: .window(.root))
}
}
}
}
controller?.complete(paid: success)
controller?.dismissAnimated()
starsContext.load(force: true)
})
}
),
availableSize: CGSize(width: context.availableSize.width - buttonSideInset * 2.0, height: 52),
transition: .immediate
)
context.add(button
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + button.size.height / 2.0))
)
contentSize.height += button.size.height
let termsText = isSubscription ? strings.Stars_Subscription_Terms : strings.Stars_Transfer_Terms
let termsURL = isSubscription ? strings.Stars_Subscription_Terms_URL : strings.Stars_Transfer_Terms_URL
contentSize.height += 14.0
let termsTextFont = Font.regular(13.0)
let termsTextColor = theme.actionSheet.secondaryTextColor
let termsLinkColor = theme.actionSheet.controlAccentColor
let termsMarkdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: termsTextFont, textColor: termsTextColor), bold: MarkdownAttributeSet(font: termsTextFont, textColor: termsTextColor), link: MarkdownAttributeSet(font: termsTextFont, textColor: termsLinkColor), linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
})
let info = info.update(
component: BalancedTextComponent(
text: .markdown(
text: termsText,
attributes: termsMarkdownAttributes
),
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: { [weak controller] attributes, _ in
if let controller, let navigationController = controller.navigationController as? NavigationController {
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: termsURL, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {})
}
}
),
availableSize: CGSize(width: constrainedTitleWidth, height: context.availableSize.height),
transition: .immediate
)
context.add(info
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + info.size.height / 2.0))
)
contentSize.height += info.size.height
var bottomInset: CGFloat = environment.safeInsets.bottom
if bottomInset < 5.0 {
bottomInset = 8.0
}
contentSize.height += 4.0 + bottomInset
return contentSize
}
}
}
private final class StarsTransferSheetComponent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
private let context: AccountContext
private let starsContext: StarsContext
private let invoice: TelegramMediaInvoice
private let source: BotPaymentInvoiceSource
private let extendedMedia: [TelegramExtendedMedia]
private let inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?, EnginePeer?)?, NoError>
private let navigateToPeer: (EnginePeer) -> Void
init(
context: AccountContext,
starsContext: StarsContext,
invoice: TelegramMediaInvoice,
source: BotPaymentInvoiceSource,
extendedMedia: [TelegramExtendedMedia],
inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?, EnginePeer?)?, NoError>,
navigateToPeer: @escaping (EnginePeer) -> Void
) {
self.context = context
self.starsContext = starsContext
self.invoice = invoice
self.source = source
self.extendedMedia = extendedMedia
self.inputData = inputData
self.navigateToPeer = navigateToPeer
}
static func ==(lhs: StarsTransferSheetComponent, rhs: StarsTransferSheetComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.invoice != rhs.invoice {
return false
}
if lhs.extendedMedia != rhs.extendedMedia {
return false
}
return true
}
static var body: Body {
let sheet = Child(SheetComponent<(EnvironmentType)>.self)
let animateOut = StoredActionSlot(Action<Void>.self)
return { context in
let environment = context.environment[EnvironmentType.self]
let controller = environment.controller
let sheet = sheet.update(
component: SheetComponent<EnvironmentType>(
content: AnyComponent<EnvironmentType>(SheetContent(
context: context.component.context,
starsContext: context.component.starsContext,
invoice: context.component.invoice,
source: context.component.source,
extendedMedia: context.component.extendedMedia,
inputData: context.component.inputData,
navigateToPeer: context.component.navigateToPeer,
dismiss: {
animateOut.invoke(Action { _ in
if let controller = controller() {
controller.dismiss(completion: nil)
}
})
}
)),
style: .glass,
backgroundColor: .color(environment.theme.list.modalBlocksBackgroundColor),
followContentSizeChanges: true,
clipsContent: true,
animateOut: animateOut
),
environment: {
environment
SheetComponentEnvironment(
isDisplaying: environment.value.isVisible,
isCentered: environment.metrics.widthClass == .regular,
hasInputHeight: !environment.inputHeight.isZero,
regularMetricsSize: CGSize(width: 430.0, height: 900.0),
dismiss: { animated in
if animated {
animateOut.invoke(Action { _ in
if let controller = controller() {
controller.dismiss(completion: nil)
}
})
} else {
if let controller = controller() {
controller.dismiss(completion: nil)
}
}
}
)
},
availableSize: context.availableSize,
transition: context.transition
)
context.add(sheet
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
)
return context.availableSize
}
}
}
public final class StarsTransferScreen: ViewControllerComponentContainer {
private let context: AccountContext
private let extendedMedia: [TelegramExtendedMedia]
private let completion: (Bool) -> Void
public init(
context: AccountContext,
starsContext: StarsContext,
invoice: TelegramMediaInvoice,
source: BotPaymentInvoiceSource,
extendedMedia: [TelegramExtendedMedia] = [],
inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?, EnginePeer?)?, NoError>,
navigateToPeer: @escaping (EnginePeer) -> Void = { _ in },
completion: @escaping (Bool) -> Void
) {
self.context = context
self.extendedMedia = extendedMedia
self.completion = completion
super.init(
context: context,
component: StarsTransferSheetComponent(
context: context,
starsContext: starsContext,
invoice: invoice,
source: source,
extendedMedia: extendedMedia,
inputData: inputData,
navigateToPeer: navigateToPeer
),
navigationBarAppearance: .none,
statusBarStyle: .ignore,
theme: .default
)
self.navigationPresentation = .flatModal
starsContext.load(force: false)
}
deinit {
self.complete(paid: false)
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private var didComplete = false
fileprivate func complete(paid: Bool) {
guard !self.didComplete else {
return
}
self.didComplete = true
self.completion(paid)
if !self.extendedMedia.isEmpty && paid {
self.navigationController?.view.addSubview(ConfettiView(frame: self.view.bounds, customImage: UIImage(bundleImageName: "Peer Info/PremiumIcon")))
}
}
public func dismissAnimated() {
if let view = self.node.hostView.findTaggedView(tag: SheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? SheetComponent<ViewControllerComponentContainer.Environment>.View {
view.dismissAnimated()
}
}
}
@@ -0,0 +1,49 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "StarsWithdrawalScreen",
module_name = "StarsWithdrawalScreen",
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/PasswordSetupUI",
"//submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController",
"//submodules/TelegramUI/Components/ChatScheduleTimeController",
"//submodules/TelegramUI/Components/TabSelectorComponent",
"//submodules/TelegramUI/Components/Stars/BalanceNeededScreen",
"//submodules/TelegramUI/Components/GlassBarButtonComponent",
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,111 @@
import Foundation
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import PresentationDataUtils
import AccountContext
import PasswordSetupUI
import Markdown
import OwnershipTransferController
public func confirmStarsRevenueWithdrawalController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: EnginePeer.Id, amount: Int64, present: @escaping (ViewController, Any?) -> Void, completion: @escaping (String) -> Void) -> ViewController {
let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
var dismissImpl: (() -> Void)?
var proceedImpl: (() -> Void)?
let disposable = MetaDisposable()
let contentNode = ChannelOwnershipTransferAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, title: presentationData.strings.Monetization_Withdraw_EnterPassword_Title, text: presentationData.strings.Monetization_Withdraw_EnterPassword_Text, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
dismissImpl?()
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Monetization_Withdraw_EnterPassword_Done, action: {
proceedImpl?()
})])
contentNode.complete = {
proceedImpl?()
}
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode)
let presentationDataDisposable = (updatedPresentationData?.signal ?? context.sharedContext.presentationData).start(next: { [weak controller, weak contentNode] presentationData in
controller?.theme = AlertControllerTheme(presentationData: presentationData)
contentNode?.theme = presentationData.theme
})
controller.dismissed = { _ in
presentationDataDisposable.dispose()
disposable.dispose()
}
dismissImpl = { [weak controller, weak contentNode] in
contentNode?.dismissInput()
controller?.dismissAnimated()
}
proceedImpl = { [weak contentNode] in
guard let contentNode = contentNode else {
return
}
contentNode.updateIsChecking(true)
let signal = context.engine.peers.requestStarsRevenueWithdrawalUrl(peerId: peerId, ton: false, amount: amount, password: contentNode.password)
disposable.set((signal |> deliverOnMainQueue).start(next: { url in
dismissImpl?()
completion(url)
}, error: { [weak contentNode] error in
var errorTextAndActions: (String, [TextAlertAction])?
switch error {
case .invalidPassword:
contentNode?.animateError()
case .limitExceeded:
errorTextAndActions = (presentationData.strings.TwoStepAuth_FloodError, [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])
default:
errorTextAndActions = (presentationData.strings.Login_UnknownError, [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])
}
contentNode?.updateIsChecking(false)
if let (text, actions) = errorTextAndActions {
dismissImpl?()
present(textAlertController(context: context, title: nil, text: text, actions: actions), nil)
}
}))
}
return controller
}
public func starsRevenueWithdrawalController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: EnginePeer.Id, amount: Int64, initialError: RequestStarsRevenueWithdrawalError, present: @escaping (ViewController, Any?) -> Void, completion: @escaping (String) -> Void) -> ViewController {
let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
let theme = AlertControllerTheme(presentationData: presentationData)
var title: NSAttributedString? = NSAttributedString(string: presentationData.strings.OwnershipTransfer_SecurityCheck, font: Font.semibold(presentationData.listsFontSize.itemListBaseFontSize), textColor: theme.primaryColor, paragraphAlignment: .center)
var text = presentationData.strings.Monetization_Withdraw_SecurityRequirements
let textFontSize = presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0
var actions: [TextAlertAction] = []
switch initialError {
case .requestPassword:
return confirmStarsRevenueWithdrawalController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, amount: amount, present: present, completion: completion)
case .twoStepAuthTooFresh, .authSessionTooFresh:
text = text + presentationData.strings.Monetization_Withdraw_ComeBackLater
actions = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]
case .twoStepAuthMissing:
actions = [TextAlertAction(type: .genericAction, title: presentationData.strings.OwnershipTransfer_SetupTwoStepAuth, action: {
let controller = SetupTwoStepVerificationController(context: context, initialState: .automatic, stateUpdated: { update, shouldDismiss, controller in
if shouldDismiss {
controller.dismiss()
}
})
present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {})]
default:
title = nil
text = presentationData.strings.Login_UnknownError
actions = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]
}
let body = MarkdownAttributeSet(font: Font.regular(textFontSize), textColor: theme.primaryColor)
let bold = MarkdownAttributeSet(font: Font.semibold(textFontSize), textColor: theme.primaryColor)
let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .center)
return richTextAlertController(context: context, title: title, text: attributedText, actions: actions)
}