This commit is contained in:
Isaac 2024-05-27 16:43:27 +04:00
commit f550b69540
22 changed files with 1010 additions and 182 deletions

View file

@ -12297,7 +12297,7 @@ Sorry for the inconvenience.";
"Stars.Transfer.Balance" = "Balance";
"Stars.Transfer.Unavailable" = "Unavailable";
"Stars.Transfer.Unavailable" = "Sorry, no star purchases are available from your country.";
"Settings.Stars" = "Your Stars";

View file

@ -2803,14 +2803,14 @@ public func chatWebFileImage(account: Account, file: TelegramMediaWebFile) -> Si
c.setBlendMode(.normal)
}
} else {
context.withFlippedContext { c in
c.setBlendMode(.copy)
c.setFillColor((arguments.emptyColor ?? UIColor.white).cgColor)
c.fill(arguments.drawingRect)
c.setBlendMode(.normal)
}
}
} else {
context.withFlippedContext { c in
c.setBlendMode(.copy)
c.setFillColor((arguments.emptyColor ?? UIColor.white).cgColor)
c.fill(arguments.drawingRect)
c.setBlendMode(.normal)
}
}

View file

@ -197,6 +197,7 @@ class CreateGiveawayHeaderItemNode: ItemListControllerHeaderItemNode {
self.backgroundNode.update(size: CGSize(width: layout.size.width, height: navigationBarHeight), transition: transition)
let component = AnyComponent(PremiumStarComponent(
theme: self.item.theme,
isIntro: true,
isVisible: true,
hasIdleAnimations: true,

View file

@ -267,6 +267,7 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent {
let star = star.update(
component: PremiumStarComponent(
theme: theme,
isIntro: false,
isVisible: true,
hasIdleAnimations: true,

View file

@ -3210,6 +3210,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
} else {
header = star.update(
component: PremiumStarComponent(
theme: environment.theme,
isIntro: isIntro,
isVisible: starIsVisible,
hasIdleAnimations: state.hasIdleAnimations,

View file

@ -72,24 +72,40 @@ struct InternalStarsStatus {
let nextOffset: String?
}
func _internal_requestStarsState(account: Account, peerId: EnginePeer.Id, offset: String?) -> Signal<InternalStarsStatus, NoError> {
private enum RequestStarsStateError {
case generic
}
private func _internal_requestStarsState(account: Account, peerId: EnginePeer.Id, subject: StarsTransactionsContext.Subject, offset: String?) -> Signal<InternalStarsStatus, RequestStarsStateError> {
return account.postbox.transaction { transaction -> Peer? in
return transaction.getPeer(peerId)
} |> mapToSignal { peer -> Signal<InternalStarsStatus, NoError> in
}
|> castError(RequestStarsStateError.self)
|> mapToSignal { peer -> Signal<InternalStarsStatus, RequestStarsStateError> in
guard let peer, let inputPeer = apiInputPeer(peer) else {
return .never()
return .fail(.generic)
}
let signal: Signal<Api.payments.StarsStatus, MTRpcError>
if let offset {
signal = account.network.request(Api.functions.payments.getStarsTransactions(flags: 0, peer: inputPeer, offset: offset))
var flags: Int32 = 0
switch subject {
case .incoming:
flags = 1 << 0
case .outgoing:
flags = 1 << 1
default:
break
}
signal = account.network.request(Api.functions.payments.getStarsTransactions(flags: flags, peer: inputPeer, offset: offset))
} else {
signal = account.network.request(Api.functions.payments.getStarsStatus(peer: inputPeer))
}
return signal
|> retryRequest
|> mapToSignal { result -> Signal<InternalStarsStatus, NoError> in
|> castError(RequestStarsStateError.self)
|> mapToSignal { result -> Signal<InternalStarsStatus, RequestStarsStateError> in
return account.postbox.transaction { transaction -> InternalStarsStatus in
switch result {
case let .starsStatus(_, balance, history, nextOffset, chats, users):
@ -105,15 +121,16 @@ func _internal_requestStarsState(account: Account, peerId: EnginePeer.Id, offset
return InternalStarsStatus(balance: balance, transactions: parsedTransactions, nextOffset: nextOffset)
}
}
|> castError(RequestStarsStateError.self)
}
}
}
private final class StarsContextImpl {
private let account: Account
private let peerId: EnginePeer.Id
fileprivate let peerId: EnginePeer.Id
private var _state: StarsContext.State?
fileprivate var _state: StarsContext.State?
private let _statePromise = Promise<StarsContext.State?>()
var state: Signal<StarsContext.State?, NoError> {
return self._statePromise.get()
@ -160,12 +177,20 @@ private final class StarsContextImpl {
}
self.previousLoadTimestamp = currentTimestamp
self.disposable.set((_internal_requestStarsState(account: self.account, peerId: self.peerId, offset: nil)
self.disposable.set((_internal_requestStarsState(account: self.account, peerId: self.peerId, subject: .all, offset: nil)
|> deliverOnMainQueue).start(next: { [weak self] status in
if let self {
self.updateState(StarsContext.State(flags: [], balance: status.balance, transactions: status.transactions, canLoadMore: status.nextOffset != nil, isLoading: false))
self.nextOffset = status.nextOffset
guard let self else {
return
}
self.updateState(StarsContext.State(flags: [], balance: status.balance, transactions: status.transactions, canLoadMore: status.nextOffset != nil, isLoading: false))
self.nextOffset = status.nextOffset
}, error: { [weak self] _ in
guard let self else {
return
}
Queue.mainQueue().after(2.5, {
self.load(force: true)
})
}))
}
@ -174,7 +199,7 @@ private final class StarsContextImpl {
return
}
var transactions = state.transactions
transactions.insert(.init(id: "\(arc4random())", count: balance, date: Int32(Date().timeIntervalSince1970), peer: .appStore, title: nil, description: nil, photo: nil), at: 0)
transactions.insert(.init(id: "tmp_\(arc4random())", count: balance, date: Int32(Date().timeIntervalSince1970), peer: .appStore, title: nil, description: nil, photo: nil), at: 0)
self.updateState(StarsContext.State(flags: [.isPendingBalance], balance: state.balance + balance, transactions: transactions, canLoadMore: state.canLoadMore, isLoading: state.isLoading))
}
@ -188,7 +213,7 @@ private final class StarsContextImpl {
self._state?.isLoading = true
self.disposable.set((_internal_requestStarsState(account: self.account, peerId: self.peerId, offset: nextOffset)
self.disposable.set((_internal_requestStarsState(account: self.account, peerId: self.peerId, subject: .all, offset: nextOffset)
|> deliverOnMainQueue).start(next: { [weak self] status in
if let self {
self.updateState(StarsContext.State(flags: [], balance: status.balance, transactions: currentState.transactions + status.transactions, canLoadMore: status.nextOffset != nil, isLoading: false))
@ -327,6 +352,22 @@ public final class StarsContext {
}
}
var peerId: EnginePeer.Id {
var peerId: EnginePeer.Id?
self.impl.syncWith { impl in
peerId = impl.peerId
}
return peerId!
}
var currentState: StarsContext.State? {
var state: StarsContext.State?
self.impl.syncWith { impl in
state = impl._state
}
return state
}
public func add(balance: Int64) {
self.impl.with {
$0.add(balance: balance)
@ -352,6 +393,173 @@ public final class StarsContext {
}
}
private final class StarsTransactionsContextImpl {
private let account: Account
private let peerId: EnginePeer.Id
private let subject: StarsTransactionsContext.Subject
private var _state: StarsTransactionsContext.State
private let _statePromise = Promise<StarsTransactionsContext.State>()
var state: Signal<StarsTransactionsContext.State, NoError> {
return self._statePromise.get()
}
private var nextOffset: String? = ""
private let disposable = MetaDisposable()
private var stateDisposable: Disposable?
init(account: Account, starsContext: StarsContext, subject: StarsTransactionsContext.Subject) {
assert(Queue.mainQueue().isCurrent())
self.account = account
self.peerId = starsContext.peerId
self.subject = subject
let currentTransactions = starsContext.currentState?.transactions ?? []
let initialTransactions: [StarsContext.State.Transaction]
switch subject {
case .all:
initialTransactions = currentTransactions
case .incoming:
initialTransactions = currentTransactions.filter { $0.count > 0 }
case .outgoing:
initialTransactions = currentTransactions.filter { $0.count < 0 }
}
self._state = StarsTransactionsContext.State(transactions: initialTransactions, canLoadMore: true, isLoading: false)
self._statePromise.set(.single(self._state))
self.stateDisposable = (starsContext.state
|> deliverOnMainQueue).start(next: { [weak self] state in
guard let self, let state else {
return
}
let currentTransactions = state.transactions
let filteredTransactions: [StarsContext.State.Transaction]
switch subject {
case .all:
filteredTransactions = currentTransactions
case .incoming:
filteredTransactions = currentTransactions.filter { $0.count > 0 }
case .outgoing:
filteredTransactions = currentTransactions.filter { $0.count < 0 }
}
if filteredTransactions != initialTransactions {
var existingIds = Set<String>()
for transaction in self._state.transactions {
if !transaction.id.hasPrefix("tmp_") {
existingIds.insert(transaction.id)
}
}
var updatedState = self._state
updatedState.transactions.removeAll(where: { $0.id.hasPrefix("tmp_") })
for transaction in filteredTransactions.reversed() {
if !existingIds.contains(transaction.id) {
updatedState.transactions.insert(transaction, at: 0)
}
}
self.updateState(updatedState)
}
})
}
deinit {
assert(Queue.mainQueue().isCurrent())
self.disposable.dispose()
self.stateDisposable?.dispose()
}
func loadMore(reload: Bool = false) {
assert(Queue.mainQueue().isCurrent())
if reload {
self.nextOffset = ""
}
guard !self._state.isLoading, let nextOffset = self.nextOffset else {
return
}
var updatedState = self._state
updatedState.isLoading = true
self.updateState(updatedState)
self.disposable.set((_internal_requestStarsState(account: self.account, peerId: self.peerId, subject: self.subject, offset: nextOffset)
|> deliverOnMainQueue).start(next: { [weak self] status in
guard let self else {
return
}
self.nextOffset = status.nextOffset
var updatedState = self._state
updatedState.transactions = nextOffset.isEmpty ? status.transactions : updatedState.transactions + status.transactions
updatedState.isLoading = false
updatedState.canLoadMore = self.nextOffset != nil
self.updateState(updatedState)
}))
}
private func updateState(_ state: StarsTransactionsContext.State) {
self._state = state
self._statePromise.set(.single(state))
}
}
public final class StarsTransactionsContext {
public struct State: Equatable {
public var transactions: [StarsContext.State.Transaction]
public var canLoadMore: Bool
public var isLoading: Bool
init(transactions: [StarsContext.State.Transaction], canLoadMore: Bool, isLoading: Bool) {
self.transactions = transactions
self.canLoadMore = canLoadMore
self.isLoading = isLoading
}
}
fileprivate let impl: QueueLocalObject<StarsTransactionsContextImpl>
public enum Subject {
case all
case incoming
case outgoing
}
public var state: Signal<StarsTransactionsContext.State, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.state.start(next: { value in
subscriber.putNext(value)
}))
}
return disposable
}
}
public func reload() {
self.impl.with {
$0.loadMore(reload: true)
}
}
public func loadMore() {
self.impl.with {
$0.loadMore()
}
}
init(account: Account, starsContext: StarsContext, subject: Subject) {
self.impl = QueueLocalObject(queue: Queue.mainQueue(), generate: {
return StarsTransactionsContextImpl(account: account, starsContext: starsContext, subject: subject)
})
}
}
func _internal_sendStarsPaymentForm(account: Account, formId: Int64, source: BotPaymentInvoiceSource) -> Signal<SendBotPaymentResult, SendBotPaymentFormError> {
return account.postbox.transaction { transaction -> Api.InputInvoice? in
return _internal_parseInputInvoice(transaction: transaction, source: source)

View file

@ -73,7 +73,12 @@ public extension TelegramEngine {
public func peerStarsContext(peerId: EnginePeer.Id) -> StarsContext {
return StarsContext(account: self.account, peerId: peerId)
}
public func peerStarsTransactionsContext(starsContext: StarsContext, subject: StarsTransactionsContext.Subject) -> StarsTransactionsContext {
return StarsTransactionsContext(account: self.account, starsContext: starsContext, subject: subject)
}
public func sendStarsPaymentForm(formId: Int64, source: BotPaymentInvoiceSource) -> Signal<SendBotPaymentResult, SendBotPaymentFormError> {
return _internal_sendStarsPaymentForm(account: self.account, formId: formId, source: source)
}

View file

@ -325,6 +325,7 @@ public final class GiftAvatarComponent: Component {
imageNode = current
} else {
imageNode = TransformImageNode()
imageNode.contentAnimations = [.firstUpdate, .subsequentUpdates]
self.addSubview(imageNode.view)
self.imageNode = imageNode
@ -335,7 +336,7 @@ public final class GiftAvatarComponent: Component {
let imageSize = CGSize(width: component.avatarSize, height: component.avatarSize)
imageNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - imageSize.width) / 2.0), y: 113.0 - imageSize.height / 2.0), size: imageSize)
imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: imageSize.width / 2.0), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))()
imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: imageSize.width / 2.0), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))()
self.avatarNode.isHidden = true
} else if let starsPeer = component.starsPeer {

View file

@ -7,6 +7,7 @@ import SceneKit
import GZip
import AppBundle
import LegacyComponents
import TelegramPresentationData
private let sceneVersion: Int = 7
@ -69,25 +70,31 @@ public func loadCompressedScene(name: String, version: Int) -> SCNScene? {
}
public final class PremiumStarComponent: Component {
let theme: PresentationTheme
let isIntro: Bool
let isVisible: Bool
let hasIdleAnimations: Bool
let colors: [UIColor]?
let particleColor: UIColor?
public init(
theme: PresentationTheme,
isIntro: Bool,
isVisible: Bool,
hasIdleAnimations: Bool,
colors: [UIColor]? = nil
colors: [UIColor]? = nil,
particleColor: UIColor? = nil
) {
self.theme = theme
self.isIntro = isIntro
self.isVisible = isVisible
self.hasIdleAnimations = hasIdleAnimations
self.colors = colors
self.particleColor = particleColor
}
public static func ==(lhs: PremiumStarComponent, rhs: PremiumStarComponent) -> Bool {
return lhs.isIntro == rhs.isIntro && lhs.isVisible == rhs.isVisible && lhs.hasIdleAnimations == rhs.hasIdleAnimations && lhs.colors == rhs.colors
return lhs.theme === rhs.theme && lhs.isIntro == rhs.isIntro && lhs.isVisible == rhs.isVisible && lhs.hasIdleAnimations == rhs.hasIdleAnimations && lhs.colors == rhs.colors && lhs.particleColor == rhs.particleColor
}
public final class View: UIView, SCNSceneRendererDelegate, ComponentTaggedView {
@ -293,9 +300,10 @@ public final class PremiumStarComponent: Component {
self.sceneView.scene = scene
self.sceneView.delegate = self
if let node = scene.rootNode.childNode(withName: "star", recursively: false), let colors = self.component?.colors, let color = colors.first {
if let component = self.component, let node = scene.rootNode.childNode(withName: "star", recursively: false), let colors =
component.colors {
node.geometry?.materials.first?.diffuse.contents = generateDiffuseTexture(colors: colors)
let names: [String] = [
"particles_left",
"particles_right",
@ -304,10 +312,63 @@ public final class PremiumStarComponent: Component {
"particles_center"
]
let starNames: [String] = [
"coins_left",
"coins_right"
]
if let particleColor = component.particleColor {
for name in starNames {
if let node = scene.rootNode.childNode(withName: name, recursively: false), let particleSystem = node.particleSystems?.first {
particleSystem.particleIntensity = 1.0
particleSystem.particleIntensityVariation = 0.05
particleSystem.particleColor = particleColor
particleSystem.particleColorVariation = SCNVector4Make(0.07, 0.0, 0.1, 0.0)
node.isHidden = false
if let propertyControllers = particleSystem.propertyControllers, let sizeController = propertyControllers[.size], let colorController = propertyControllers[.color] {
let animation = CAKeyframeAnimation()
if let existing = colorController.animation as? CAKeyframeAnimation {
animation.keyTimes = existing.keyTimes
animation.values = existing.values?.compactMap { ($0 as? UIColor)?.alpha } ?? []
} else {
animation.values = [ 0.0, 1.0, 1.0, 0.0 ]
}
let opacityController = SCNParticlePropertyController(animation: animation)
particleSystem.propertyControllers = [
.size: sizeController,
.opacity: opacityController
]
}
}
}
}
for name in names {
if let node = scene.rootNode.childNode(withName: name, recursively: false), let particleSystem = node.particleSystems?.first, color.rgb != 0x6a94ff {
particleSystem.particleColor = color
particleSystem.particleColorVariation = SCNVector4Make(0, 0, 0, 0)
if let node = scene.rootNode.childNode(withName: name, recursively: false), let particleSystem = node.particleSystems?.first {
if let particleColor = component.particleColor {
particleSystem.particleIntensity = min(1.0, 2.0 * particleSystem.particleIntensity)
particleSystem.particleIntensityVariation = 0.05
particleSystem.particleColor = particleColor
particleSystem.particleColorVariation = SCNVector4Make(0.1, 0.0, 0.12, 0.0)
} else {
particleSystem.particleColorVariation = SCNVector4Make(0.12, 0.03, 0.035, 0.0)
}
if let propertyControllers = particleSystem.propertyControllers, let sizeController = propertyControllers[.size], let colorController = propertyControllers[.color] {
let animation = CAKeyframeAnimation()
if let existing = colorController.animation as? CAKeyframeAnimation {
animation.keyTimes = existing.keyTimes
animation.values = existing.values?.compactMap { ($0 as? UIColor)?.alpha } ?? []
} else {
animation.values = [ 0.0, 1.0, 1.0, 0.0 ]
}
let opacityController = SCNParticlePropertyController(animation: animation)
particleSystem.propertyControllers = [
.size: sizeController,
.opacity: opacityController
]
}
}
}
}
@ -600,6 +661,10 @@ public final class PremiumStarComponent: Component {
self.setup()
if let _ = component.particleColor {
self.sceneView.backgroundColor = component.theme.list.blocksBackgroundColor
}
self.sceneView.bounds = CGRect(origin: .zero, size: CGSize(width: availableSize.width * 2.0, height: availableSize.height * 2.0))
if self.sceneView.superview == self {
self.sceneView.center = CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)

View file

@ -0,0 +1,28 @@
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",
],
visibility = [
"//visibility:public",
],
)

View file

@ -0,0 +1,456 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import TelegramCore
import ComponentFlow
import TelegramPresentationData
import PhotoResources
import AvatarNode
import AccountContext
final class StarsParticlesView: UIView {
private struct Particle {
var trackIndex: Int
var position: CGPoint
var scale: CGFloat
var alpha: CGFloat
var direction: CGPoint
var velocity: CGFloat
var color: UIColor
var currentTime: CGFloat
var lifeTime: CGFloat
init(
trackIndex: Int,
position: CGPoint,
scale: CGFloat,
alpha: CGFloat,
direction: CGPoint,
velocity: CGFloat,
color: UIColor,
currentTime: CGFloat,
lifeTime: CGFloat
) {
self.trackIndex = trackIndex
self.position = position
self.scale = scale
self.alpha = alpha
self.direction = direction
self.velocity = velocity
self.color = color
self.currentTime = currentTime
self.lifeTime = lifeTime
}
mutating func update(deltaTime: CGFloat) {
var position = self.position
position.x += self.direction.x * self.velocity * deltaTime
position.y += self.direction.y * self.velocity * deltaTime
self.position = position
self.currentTime += deltaTime
}
}
private final class ParticleSet {
private let size: CGSize
private let large: Bool
private(set) var particles: [Particle] = []
init(size: CGSize, large: Bool, preAdvance: Bool) {
self.size = size
self.large = large
self.generateParticles(preAdvance: preAdvance)
}
private func generateParticles(preAdvance: Bool) {
let maxDirections = self.large ? 8 : 80
if self.particles.count < maxDirections {
var allTrackIndices: [Int] = Array(repeating: 0, count: maxDirections)
for i in 0 ..< maxDirections {
allTrackIndices[i] = i
}
var takenIndexCount = 0
for particle in self.particles {
allTrackIndices[particle.trackIndex] = -1
takenIndexCount += 1
}
var availableTrackIndices: [Int] = []
availableTrackIndices.reserveCapacity(maxDirections - takenIndexCount)
for index in allTrackIndices {
if index != -1 {
availableTrackIndices.append(index)
}
}
if !availableTrackIndices.isEmpty {
availableTrackIndices.shuffle()
for takeIndex in availableTrackIndices {
let directionIndex = takeIndex
var angle = (CGFloat(directionIndex % maxDirections) / CGFloat(maxDirections)) * CGFloat.pi * 2.0
var alpha = 1.0
var lifeTimeMultiplier = 1.0
var isUpOrDownSemisphere = false
if angle > CGFloat.pi / 7.0 && angle < CGFloat.pi - CGFloat.pi / 7.0 {
isUpOrDownSemisphere = true
} else if !"".isEmpty, angle > CGFloat.pi + CGFloat.pi / 7.0 && angle < 2.0 * CGFloat.pi - CGFloat.pi / 7.0 {
isUpOrDownSemisphere = true
}
if isUpOrDownSemisphere {
if CGFloat.random(in: 0.0 ... 1.0) < 0.2 {
lifeTimeMultiplier = 0.3
} else {
angle += CGFloat.random(in: 0.0 ... 1.0) > 0.5 ? CGFloat.pi / 1.6 : -CGFloat.pi / 1.6
angle += CGFloat.random(in: -0.2 ... 0.2)
lifeTimeMultiplier = 0.5
}
if self.large {
alpha = 0.0
}
}
if self.large {
angle += CGFloat.random(in: -0.5 ... 0.5)
}
let direction = CGPoint(x: cos(angle), y: sin(angle))
let velocity = self.large ? CGFloat.random(in: 15.0 ..< 20.0) : CGFloat.random(in: 20.0 ..< 35.0)
let scale = self.large ? CGFloat.random(in: 0.65 ... 0.9) : CGFloat.random(in: 0.65 ... 1.0) * 0.75
let lifeTime = (self.large ? CGFloat.random(in: 2.0 ... 3.5) : CGFloat.random(in: 0.7 ... 3.0))
var position = CGPoint(x: self.size.width / 2.0, y: self.size.height / 2.0)
var initialOffset: CGFloat = 0.5
if preAdvance {
initialOffset = CGFloat.random(in: 0.5 ... 1.0)
} else {
let p = CGFloat.random(in: 0.0 ... 1.0)
if p < 0.5 {
initialOffset = CGFloat.random(in: 0.65 ... 1.0)
} else {
initialOffset = 0.5
}
}
position.x += direction.x * initialOffset * 105.0
position.y += direction.y * initialOffset * 105.0
let largeColors: [UInt32] = [0xff9145, 0xfec007, 0xed9303]
let smallColors: [UInt32] = [0xfecc14, 0xf7ab04, 0xff9145, 0xfdda21]
let particle = Particle(
trackIndex: directionIndex,
position: position,
scale: scale,
alpha: alpha,
direction: direction,
velocity: velocity,
color: UIColor(rgb: (self.large ? largeColors : smallColors).randomElement()!),
currentTime: 0.0,
lifeTime: lifeTime * lifeTimeMultiplier
)
self.particles.append(particle)
}
}
}
}
func update(deltaTime: CGFloat) {
for i in (0 ..< self.particles.count).reversed() {
self.particles[i].update(deltaTime: deltaTime)
if self.particles[i].currentTime > self.particles[i].lifeTime {
self.particles.remove(at: i)
}
}
self.generateParticles(preAdvance: false)
}
}
private var displayLink: SharedDisplayLinkDriver.Link?
private var particleSet: ParticleSet?
private let particleImage: UIImage
private var particleLayers: [SimpleLayer] = []
private var size: CGSize?
private let large: Bool
init(size: CGSize, large: Bool) {
if large {
self.particleImage = generateTintedImage(image: UIImage(bundleImageName: "Peer Info/PremiumIcon"), color: .white)!.withRenderingMode(.alwaysTemplate)
} else {
self.particleImage = generateTintedImage(image: UIImage(bundleImageName: "Premium/Stars/Particle"), color: .white)!.withRenderingMode(.alwaysTemplate)
}
self.large = large
super.init(frame: .zero)
self.particleSet = ParticleSet(size: size, large: large, preAdvance: true)
self.displayLink = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] delta in
self?.update(deltaTime: CGFloat(delta))
})
}
required init?(coder: NSCoder) {
preconditionFailure()
}
fileprivate func update(size: CGSize) {
self.size = size
}
private func update(deltaTime: CGFloat) {
guard let particleSet = self.particleSet else {
return
}
particleSet.update(deltaTime: deltaTime)
for i in 0 ..< particleSet.particles.count {
let particle = particleSet.particles[i]
let particleLayer: SimpleLayer
if i < self.particleLayers.count {
particleLayer = self.particleLayers[i]
particleLayer.isHidden = false
} else {
particleLayer = SimpleLayer()
particleLayer.contents = self.particleImage.cgImage
particleLayer.bounds = CGRect(origin: CGPoint(), size: particleImage.size)
self.particleLayers.append(particleLayer)
self.layer.addSublayer(particleLayer)
}
particleLayer.layerTintColor = particle.color.cgColor
particleLayer.position = particle.position
particleLayer.opacity = Float(particle.alpha)
let particleScale = min(1.0, particle.currentTime / 0.3) * min(1.0, (particle.lifeTime - particle.currentTime) / 0.2) * particle.scale
particleLayer.transform = CATransform3DMakeScale(particleScale, particleScale, 1.0)
}
if particleSet.particles.count < self.particleLayers.count {
for i in particleSet.particles.count ..< self.particleLayers.count {
self.particleLayers[i].isHidden = true
}
}
}
}
public final class StarsImageComponent: Component {
public enum Subject: Equatable {
case none
case photo(TelegramMediaWebFile)
case transactionPeer(StarsContext.State.Transaction.Peer)
}
public let context: AccountContext
public let subject: Subject
public let theme: PresentationTheme
public let diameter: CGFloat
public init(
context: AccountContext,
subject: Subject,
theme: PresentationTheme,
diameter: CGFloat
) {
self.context = context
self.subject = subject
self.theme = theme
self.diameter = diameter
}
public static func ==(lhs: StarsImageComponent, rhs: StarsImageComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.subject != rhs.subject {
return false
}
if lhs.diameter != rhs.diameter {
return false
}
return true
}
public final class View: UIView {
private var component: StarsImageComponent?
private var smallParticlesView: StarsParticlesView?
private var largeParticlesView: StarsParticlesView?
private var imageNode: TransformImageNode?
private var avatarNode: ImageNode?
private var iconBackgroundView: UIImageView?
private var iconView: UIImageView?
private let fetchDisposable = MetaDisposable()
public override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
preconditionFailure()
}
deinit {
self.fetchDisposable.dispose()
}
func update(component: StarsImageComponent, availableSize: CGSize, transition: Transition) -> CGSize {
self.component = component
let smallParticlesView: StarsParticlesView
if let current = self.smallParticlesView {
smallParticlesView = current
} else {
smallParticlesView = StarsParticlesView(size: availableSize, large: false)
self.addSubview(smallParticlesView)
self.smallParticlesView = smallParticlesView
}
smallParticlesView.update(size: availableSize)
smallParticlesView.frame = CGRect(origin: .zero, size: availableSize)
let largeParticlesView: StarsParticlesView
if let current = self.largeParticlesView {
largeParticlesView = current
} else {
largeParticlesView = StarsParticlesView(size: availableSize, large: true)
self.addSubview(largeParticlesView)
self.largeParticlesView = largeParticlesView
}
largeParticlesView.update(size: availableSize)
largeParticlesView.frame = CGRect(origin: .zero, size: availableSize)
let imageSize = CGSize(width: component.diameter, height: component.diameter)
let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - imageSize.width) / 2.0), y: floorToScreenPixels((availableSize.height - imageSize.height) / 2.0)), size: imageSize)
switch component.subject {
case .none:
break
case let .photo(photo):
let imageNode: TransformImageNode
if let current = self.imageNode {
imageNode = current
} else {
imageNode = TransformImageNode()
imageNode.contentAnimations = [.firstUpdate, .subsequentUpdates]
self.addSubview(imageNode.view)
self.imageNode = imageNode
imageNode.setSignal(chatWebFileImage(account: component.context.account, file: photo))
self.fetchDisposable.set(chatMessageWebFileInteractiveFetched(account: component.context.account, userLocation: .other, image: photo).startStrict())
}
imageNode.frame = imageFrame
imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: imageSize.width / 2.0), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))()
case let .transactionPeer(peer):
if case let .peer(peer) = peer {
let avatarNode: ImageNode
if let current = self.avatarNode {
avatarNode = current
} else {
avatarNode = ImageNode()
avatarNode.displaysAsynchronously = false
self.addSubview(avatarNode.view)
self.avatarNode = avatarNode
avatarNode.setSignal(peerAvatarCompleteImage(account: component.context.account, peer: peer, size: imageSize, font: avatarPlaceholderFont(size: 43.0), fullSize: true))
}
avatarNode.frame = imageFrame
} else {
let iconBackgroundView: UIImageView
let iconView: UIImageView
if let currentBackground = self.iconBackgroundView, let current = self.iconView {
iconBackgroundView = currentBackground
iconView = current
} else {
iconBackgroundView = UIImageView()
iconView = UIImageView()
self.addSubview(iconBackgroundView)
self.addSubview(iconView)
self.iconBackgroundView = iconBackgroundView
self.iconView = iconView
}
var iconInset: CGFloat = 9.0
var iconOffset: CGFloat = 0.0
switch peer {
case .appStore:
iconBackgroundView.image = generateGradientFilledCircleImage(
diameter: imageSize.width,
colors: [
UIColor(rgb: 0x2a9ef1).cgColor,
UIColor(rgb: 0x72d5fd).cgColor
],
direction: .mirroredDiagonal
)
iconView.image = UIImage(bundleImageName: "Premium/Stars/Apple")
case .playMarket:
iconBackgroundView.image = generateGradientFilledCircleImage(
diameter: imageSize.width,
colors: [
UIColor(rgb: 0x54cb68).cgColor,
UIColor(rgb: 0xa0de7e).cgColor
],
direction: .mirroredDiagonal
)
iconView.image = UIImage(bundleImageName: "Premium/Stars/Google")
case .fragment:
iconBackgroundView.image = generateFilledCircleImage(
diameter: imageSize.width,
color: UIColor(rgb: 0x1b1f24)
)
iconView.image = UIImage(bundleImageName: "Premium/Stars/Fragment")
iconOffset = 5.0
case .premiumBot:
iconInset = 15.0
iconBackgroundView.image = generateGradientFilledCircleImage(
diameter: imageSize.width,
colors: [
UIColor(rgb: 0x6b93ff).cgColor,
UIColor(rgb: 0x6b93ff).cgColor,
UIColor(rgb: 0x8d77ff).cgColor,
UIColor(rgb: 0xb56eec).cgColor,
UIColor(rgb: 0xb56eec).cgColor
],
direction: .mirroredDiagonal
)
iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/EntityInputPremiumIcon"), color: .white)
case .peer, .unsupported:
iconInset = 15.0
iconBackgroundView.image = generateGradientFilledCircleImage(
diameter: imageSize.width,
colors: [
UIColor(rgb: 0xb1b1b1).cgColor,
UIColor(rgb: 0xcdcdcd).cgColor
],
direction: .mirroredDiagonal
)
iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/EntityInputPremiumIcon"), color: .white)
}
iconBackgroundView.frame = imageFrame
iconView.frame = imageFrame.insetBy(dx: iconInset, dy: iconInset).offsetBy(dx: 0.0, dy: iconOffset)
}
}
return availableSize
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}

View file

@ -167,7 +167,7 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent {
static var body: Body {
let overscroll = Child(Rectangle.self)
let fade = Child(RoundedRectangle.self)
// let fade = Child(RoundedRectangle.self)
let text = Child(BalancedTextComponent.self)
let list = Child(VStack<Empty>.self)
let termsText = Child(BalancedTextComponent.self)
@ -203,21 +203,21 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent {
.position(CGPoint(x: overscroll.size.width / 2.0, y: -overscroll.size.height / 2.0))
)
let fade = fade.update(
component: RoundedRectangle(
colors: [
topBackgroundColor,
bottomBackgroundColor
],
cornerRadius: 0.0,
gradientDirection: .vertical
),
availableSize: CGSize(width: availableWidth, height: 300),
transition: context.transition
)
context.add(fade
.position(CGPoint(x: fade.size.width / 2.0, y: fade.size.height / 2.0))
)
// let fade = fade.update(
// component: RoundedRectangle(
// colors: [
// topBackgroundColor,
// bottomBackgroundColor
// ],
// cornerRadius: 0.0,
// gradientDirection: .vertical
// ),
// availableSize: CGSize(width: availableWidth, height: 300),
// transition: context.transition
// )
// context.add(fade
// .position(CGPoint(x: fade.size.width / 2.0, y: fade.size.height / 2.0))
// )
size.height += 183.0 + 10.0 + environment.navigationHeight - 56.0
@ -724,6 +724,7 @@ private final class StarsPurchaseScreenComponent: CombinedComponent {
let header = star.update(
component: PremiumStarComponent(
theme: environment.theme,
isIntro: true,
isVisible: starIsVisible,
hasIdleAnimations: state.hasIdleAnimations,
@ -732,7 +733,8 @@ private final class StarsPurchaseScreenComponent: CombinedComponent {
UIColor(rgb: 0xf09903),
UIColor(rgb: 0xf9b004),
UIColor(rgb: 0xfdd219)
]
],
particleColor: UIColor(rgb: 0xf9b004)
),
availableSize: CGSize(width: min(414.0, context.availableSize.width), height: 220.0),
transition: context.transition

View file

@ -39,6 +39,7 @@ swift_library(
"//submodules/TelegramUI/Components/AnimatedTextComponent",
"//submodules/AvatarNode",
"//submodules/PhotoResources",
"//submodules/TelegramUI/Components/Stars/StarsImageComponent",
],
visibility = [
"//visibility:public",

View file

@ -19,7 +19,7 @@ import AvatarNode
import TextFormat
import TelegramStringFormatting
import UndoUI
import PremiumStarComponent
import StarsImageComponent
private final class StarsTransactionSheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -117,7 +117,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
static var body: Body {
let closeButton = Child(Button.self)
let title = Child(MultilineTextComponent.self)
let star = Child(GiftAvatarComponent.self)
let star = Child(StarsImageComponent.self)
let amount = Child(BalancedTextComponent.self)
let amountStar = Child(BundleIconComponent.self)
let description = Child(MultilineTextComponent.self)
@ -249,18 +249,20 @@ private final class StarsTransactionSheetContent: CombinedComponent {
transition: .immediate
)
let imageSubject: StarsImageComponent.Subject
if let photo {
imageSubject = .photo(photo)
} else if let transactionPeer {
imageSubject = .transactionPeer(transactionPeer)
} else {
imageSubject = .none
}
let star = star.update(
component: GiftAvatarComponent(
component: StarsImageComponent(
context: component.context,
subject: imageSubject,
theme: theme,
peers: toPeer.flatMap { [$0] } ?? [],
photo: photo,
starsPeer: transactionPeer,
isVisible: true,
hasIdleAnimations: true,
hasScaleAnimation: false,
avatarSize: 90.0,
color: UIColor(rgb: 0xf7ab04)
diameter: 90.0
),
availableSize: CGSize(width: context.availableSize.width, height: 200.0),
transition: .immediate
@ -426,7 +428,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
)
context.add(star
.position(CGPoint(x: context.availableSize.width / 2.0, y: star.size.height / 2.0 - 32.0))
.position(CGPoint(x: context.availableSize.width / 2.0, y: star.size.height / 2.0 - 19.0))
)
var originY: CGFloat = 0.0

View file

@ -18,50 +18,18 @@ import PhotoResources
final class StarsTransactionsListPanelComponent: Component {
typealias EnvironmentType = StarsTransactionsPanelEnvironment
final class Item: Equatable {
let transaction: StarsContext.State.Transaction
init(
transaction: StarsContext.State.Transaction
) {
self.transaction = transaction
}
static func ==(lhs: Item, rhs: Item) -> Bool {
if lhs.transaction != rhs.transaction {
return false
}
return true
}
}
final class Items: Equatable {
let items: [Item]
init(items: [Item]) {
self.items = items
}
static func ==(lhs: Items, rhs: Items) -> Bool {
if lhs === rhs {
return true
}
return lhs.items == rhs.items
}
}
let context: AccountContext
let items: Items?
let transactionsContext: StarsTransactionsContext
let action: (StarsContext.State.Transaction) -> Void
init(
context: AccountContext,
items: Items?,
transactionsContext: StarsTransactionsContext,
action: @escaping (StarsContext.State.Transaction) -> Void
) {
self.context = context
self.items = items
self.transactionsContext = transactionsContext
self.action = action
}
@ -69,9 +37,6 @@ final class StarsTransactionsListPanelComponent: Component {
if lhs.context !== rhs.context {
return false
}
if lhs.items != rhs.items {
return false
}
return true
}
@ -137,6 +102,10 @@ final class StarsTransactionsListPanelComponent: Component {
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()
@ -164,6 +133,10 @@ final class StarsTransactionsListPanelComponent: Component {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.itemsDisposable?.dispose()
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !self.ignoreScrolling {
self.updateScrolling(transition: .immediate)
@ -175,7 +148,7 @@ final class StarsTransactionsListPanelComponent: Component {
}
private func updateScrolling(transition: Transition) {
guard let component = self.component, let environment = self.environment, let items = component.items, let itemLayout = self.itemLayout else {
guard let component = self.component, let environment = self.environment, let itemLayout = self.itemLayout else {
return
}
@ -184,11 +157,11 @@ final class StarsTransactionsListPanelComponent: Component {
var validIds = Set<String>()
if let visibleItems = itemLayout.visibleItems(for: visibleBounds) {
for index in visibleItems.lowerBound ..< visibleItems.upperBound {
if index >= items.items.count {
if index >= self.items.count {
continue
}
let item = items.items[index]
let id = item.transaction.id
let item = self.items[index]
let id = item.id
validIds.insert(id)
var itemTransition = transition
@ -214,9 +187,9 @@ final class StarsTransactionsListPanelComponent: Component {
let itemTitle: String
let itemSubtitle: String?
let itemDate: String
switch item.transaction.peer {
switch item.peer {
case let .peer(peer):
if let title = item.transaction.title {
if let title = item.title {
itemTitle = title
itemSubtitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast)
} else {
@ -243,15 +216,15 @@ final class StarsTransactionsListPanelComponent: Component {
let itemLabel: NSAttributedString
let labelString: String
let formattedLabel = presentationStringsFormattedNumber(abs(Int32(item.transaction.count)), environment.dateTimeFormat.groupingSeparator)
if item.transaction.count < 0 {
let formattedLabel = presentationStringsFormattedNumber(abs(Int32(item.count)), environment.dateTimeFormat.groupingSeparator)
if item.count < 0 {
labelString = "- \(formattedLabel)"
} else {
labelString = "+ \(formattedLabel)"
}
itemLabel = NSAttributedString(string: labelString, font: Font.medium(fontBaseDisplaySize), textColor: labelString.hasPrefix("-") ? environment.theme.list.itemDestructiveColor : environment.theme.list.itemDisclosureActions.constructive.fillColor)
itemDate = stringForMediumCompactDate(timestamp: item.transaction.date, strings: environment.strings, dateTimeFormat: environment.dateTimeFormat)
itemDate = stringForMediumCompactDate(timestamp: item.date, strings: environment.strings, dateTimeFormat: environment.dateTimeFormat)
var titleComponents: [AnyComponentWithIdentity<Empty>] = []
titleComponents.append(
@ -292,14 +265,16 @@ final class StarsTransactionsListPanelComponent: Component {
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(AvatarComponent(context: component.context, theme: environment.theme, peer: item.transaction.peer, photo: item.transaction.photo))), false),
leftIcon: .custom(AnyComponentWithIdentity(id: "avatar", component: AnyComponent(AvatarComponent(context: component.context, theme: environment.theme, peer: item.peer, photo: item.photo))), false),
icon: nil,
accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: "label", component: AnyComponent(LabelComponent(text: itemLabel))), insets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 16.0))),
action: { [weak self] _ in
guard let self, let component = self.component else {
return
}
component.action(item.transaction)
if !item.id.hasPrefix("tmp_") {
component.action(item)
}
}
)),
environment: {},
@ -308,6 +283,9 @@ final class StarsTransactionsListPanelComponent: Component {
let itemFrame = itemLayout.itemFrame(for: index)
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)
@ -338,11 +316,45 @@ final class StarsTransactionsListPanelComponent: Component {
for id in removeIds {
self.visibleItems.removeValue(forKey: id)
}
let bottomOffset = 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 lastTransaction = self.items.last {
if lastTransaction.id != self.currentLoadMoreId {
self.currentLoadMoreId = lastTransaction.id
component.transactionsContext.loadMore()
}
}
}
private var isUpdating = false
func update(component: StarsTransactionsListPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<StarsTransactionsPanelEnvironment>, transition: Transition) -> 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 hadTemporaryTransactions = self.items.contains(where: { $0.id.hasPrefix("tmp_") })
self.items = status.transactions
if !status.isLoading {
self.currentLoadMoreId = nil
}
if !self.isUpdating {
state?.updated(transition: wasEmpty || hadTemporaryTransactions ? .immediate : .easeInOut(duration: 0.2))
}
})
}
let environment = environment[StarsTransactionsPanelEnvironment.self].value
self.environment = environment
@ -392,7 +404,7 @@ final class StarsTransactionsListPanelComponent: Component {
containerInsets: environment.containerInsets,
containerWidth: availableSize.width,
itemHeight: measureItemSize.height,
itemCount: component.items?.items.count ?? 0
itemCount: self.items.count
)
self.itemLayout = itemLayout

View file

@ -28,19 +28,22 @@ final class StarsTransactionsPanelEnvironment: Equatable {
let dateTimeFormat: PresentationDateTimeFormat
let containerInsets: UIEdgeInsets
let isScrollable: Bool
let isCurrent: Bool
init(
theme: PresentationTheme,
strings: PresentationStrings,
dateTimeFormat: PresentationDateTimeFormat,
containerInsets: UIEdgeInsets,
isScrollable: Bool
isScrollable: Bool,
isCurrent: Bool
) {
self.theme = theme
self.strings = strings
self.dateTimeFormat = dateTimeFormat
self.containerInsets = containerInsets
self.isScrollable = isScrollable
self.isCurrent = isCurrent
}
static func ==(lhs: StarsTransactionsPanelEnvironment, rhs: StarsTransactionsPanelEnvironment) -> Bool {
@ -59,6 +62,9 @@ final class StarsTransactionsPanelEnvironment: Equatable {
if lhs.isScrollable != rhs.isScrollable {
return false
}
if lhs.isCurrent != rhs.isCurrent {
return false
}
return true
}
}
@ -658,15 +664,7 @@ final class StarsTransactionsPanelContainerComponent: Component {
}
transition.setFrame(view: headerView, frame: CGRect(origin: topPanelFrame.origin.offsetBy(dx: sideInset, dy: 0.0), size: headerSize))
}
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
)
let centralPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY), size: CGSize(width: availableSize.width, height: availableSize.height - topPanelFrame.maxY))
if self.animatingTransition {
@ -739,6 +737,16 @@ final class StarsTransactionsPanelContainerComponent: Component {
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,

View file

@ -112,6 +112,12 @@ final class StarsTransactionsScreenComponent: Component {
private var stateDisposable: Disposable?
private var starsState: StarsContext.State?
private var previousBalance: Int64?
private var allTransactionsContext: StarsTransactionsContext?
private var incomingTransactionsContext: StarsTransactionsContext?
private var outgoingTransactionsContext: StarsTransactionsContext?
override init(frame: CGRect) {
self.headerOffsetContainer = UIView()
self.headerOffsetContainer.isUserInteractionEnabled = false
@ -264,9 +270,7 @@ final class StarsTransactionsScreenComponent: Component {
}
)
}
private var previousBalance: Int64?
private var isUpdating = false
func update(component: StarsTransactionsScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: Transition) -> CGSize {
self.isUpdating = true
@ -294,6 +298,7 @@ final class StarsTransactionsScreenComponent: Component {
return
}
self.starsState = state
if !self.isUpdating {
self.state?.updated()
}
@ -378,6 +383,7 @@ final class StarsTransactionsScreenComponent: Component {
let starSize = self.starView.update(
transition: .immediate,
component: AnyComponent(PremiumStarComponent(
theme: environment.theme,
isIntro: true,
isVisible: true,
hasIdleAnimations: true,
@ -386,7 +392,8 @@ final class StarsTransactionsScreenComponent: Component {
UIColor(rgb: 0xf09903),
UIColor(rgb: 0xf9b004),
UIColor(rgb: 0xfdd219)
]
],
particleColor: UIColor(rgb: 0xf9b004)
)),
environment: {},
containerSize: CGSize(width: min(414.0, availableSize.width), height: 220.0)
@ -545,56 +552,65 @@ final class StarsTransactionsScreenComponent: Component {
contentHeight += balanceSize.height
contentHeight += 44.0
let transactions = self.starsState?.transactions ?? []
let allItems = StarsTransactionsListPanelComponent.Items(
items: transactions.map { StarsTransactionsListPanelComponent.Item(transaction: $0) }
)
let incomingItems = StarsTransactionsListPanelComponent.Items(
items: transactions.filter { $0.count > 0 }.map { StarsTransactionsListPanelComponent.Item(transaction: $0) }
)
let outgoingItems = StarsTransactionsListPanelComponent.Items(
items: transactions.filter { $0.count < 0 }.map { StarsTransactionsListPanelComponent.Item(transaction: $0) }
)
let initialTransactions = self.starsState?.transactions ?? []
var panelItems: [StarsTransactionsPanelContainerComponent.Item] = []
if !allItems.items.isEmpty {
if !initialTransactions.isEmpty {
let allTransactionsContext: StarsTransactionsContext
if let current = self.allTransactionsContext {
allTransactionsContext = current
} else {
allTransactionsContext = component.context.engine.payments.peerStarsTransactionsContext(starsContext: component.starsContext, subject: .all)
}
let incomingTransactionsContext: StarsTransactionsContext
if let current = self.incomingTransactionsContext {
incomingTransactionsContext = current
} else {
incomingTransactionsContext = component.context.engine.payments.peerStarsTransactionsContext(starsContext: component.starsContext, subject: .incoming)
}
let outgoingTransactionsContext: StarsTransactionsContext
if let current = self.outgoingTransactionsContext {
outgoingTransactionsContext = current
} else {
outgoingTransactionsContext = component.context.engine.payments.peerStarsTransactionsContext(starsContext: component.starsContext, subject: .outgoing)
}
panelItems.append(StarsTransactionsPanelContainerComponent.Item(
id: "all",
title: environment.strings.Stars_Intro_AllTransactions,
panel: AnyComponent(StarsTransactionsListPanelComponent(
context: component.context,
items: allItems,
transactionsContext: allTransactionsContext,
action: { transaction in
component.openTransaction(transaction)
}
))
))
if !outgoingItems.items.isEmpty {
panelItems.append(StarsTransactionsPanelContainerComponent.Item(
id: "incoming",
title: environment.strings.Stars_Intro_Incoming,
panel: AnyComponent(StarsTransactionsListPanelComponent(
context: component.context,
items: incomingItems,
action: { transaction in
component.openTransaction(transaction)
}
))
panelItems.append(StarsTransactionsPanelContainerComponent.Item(
id: "incoming",
title: environment.strings.Stars_Intro_Incoming,
panel: AnyComponent(StarsTransactionsListPanelComponent(
context: component.context,
transactionsContext: incomingTransactionsContext,
action: { transaction in
component.openTransaction(transaction)
}
))
panelItems.append(StarsTransactionsPanelContainerComponent.Item(
id: "outgoing",
title: environment.strings.Stars_Intro_Outgoing,
panel: AnyComponent(StarsTransactionsListPanelComponent(
context: component.context,
items: outgoingItems,
action: { transaction in
component.openTransaction(transaction)
}
))
))
panelItems.append(StarsTransactionsPanelContainerComponent.Item(
id: "outgoing",
title: environment.strings.Stars_Intro_Outgoing,
panel: AnyComponent(StarsTransactionsListPanelComponent(
context: component.context,
transactionsContext: outgoingTransactionsContext,
action: { transaction in
component.openTransaction(transaction)
}
))
}
))
}
var panelTransition = transition
@ -742,10 +758,6 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer {
}
self.starsContext.load(force: false)
Queue.mainQueue().after(0.5, {
self.starsContext.loadMore()
})
}
required public init(coder aDecoder: NSCoder) {

View file

@ -31,7 +31,7 @@ swift_library(
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/TelegramUI/Components/ListSectionComponent",
"//submodules/TelegramUI/Components/ListActionItemComponent",
"//submodules/TelegramUI/Components/Premium/PremiumStarComponent",
"//submodules/TelegramUI/Components/Stars/StarsImageComponent",
],
visibility = [
"//visibility:public",

View file

@ -13,11 +13,11 @@ import BalancedTextComponent
import MultilineTextComponent
import BundleIconComponent
import ButtonComponent
import PremiumStarComponent
import ItemListUI
import UndoUI
import AccountContext
import PresentationDataUtils
import StarsImageComponent
private final class SheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -196,7 +196,7 @@ private final class SheetContent: CombinedComponent {
static var body: Body {
let background = Child(RoundedRectangle.self)
let star = Child(GiftAvatarComponent.self)
let star = Child(StarsImageComponent.self)
let closeButton = Child(Button.self)
let title = Child(Text.self)
let text = Child(BalancedTextComponent.self)
@ -228,24 +228,25 @@ private final class SheetContent: CombinedComponent {
)
if let peer = state.peer {
let subject: StarsImageComponent.Subject
if let photo = component.invoice.photo {
subject = .photo(photo)
} else {
subject = .transactionPeer(.peer(peer))
}
let star = star.update(
component: GiftAvatarComponent(
context: context.component.context,
theme: environment.theme,
peers: [peer],
photo: component.invoice.photo,
isVisible: true,
hasIdleAnimations: true,
hasScaleAnimation: false,
avatarSize: 90.0,
color: UIColor(rgb: 0xf7ab04)
component: StarsImageComponent(
context: component.context,
subject: subject,
theme: theme,
diameter: 90.0
),
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: 0.0 + star.size.height / 2.0 - 30.0))
.position(CGPoint(x: context.availableSize.width / 2.0, y: star.size.height / 2.0 - 27.0))
)
}
@ -342,7 +343,7 @@ private final class SheetContent: CombinedComponent {
transition: .immediate
)
let balanceIcon = balanceIcon.update(
component: BundleIconComponent(name: "Premium/Stars/StarLarge", tintColor: nil),
component: BundleIconComponent(name: "Premium/Stars/StarSmall", tintColor: nil),
availableSize: context.availableSize,
transition: .immediate
)
@ -352,10 +353,10 @@ private final class SheetContent: CombinedComponent {
.position(CGPoint(x: 16.0 + environment.safeInsets.left + balanceTitle.size.width / 2.0, y: topBalanceOriginY + balanceTitle.size.height / 2.0))
)
context.add(balanceIcon
.position(CGPoint(x: 16.0 + environment.safeInsets.left + balanceIcon.size.width / 2.0, y: topBalanceOriginY + balanceTitle.size.height + balanceValue.size.height / 2.0 - UIScreenPixel))
.position(CGPoint(x: 16.0 + environment.safeInsets.left + balanceIcon.size.width / 2.0, y: topBalanceOriginY + balanceTitle.size.height + balanceValue.size.height / 2.0 + 1.0 + UIScreenPixel))
)
context.add(balanceValue
.position(CGPoint(x: 16.0 + environment.safeInsets.left + balanceIcon.size.width + 3.0 + balanceValue.size.width / 2.0, y: topBalanceOriginY + balanceTitle.size.height + balanceValue.size.height / 2.0))
.position(CGPoint(x: 16.0 + environment.safeInsets.left + balanceIcon.size.width + 3.0 + balanceValue.size.width / 2.0, y: topBalanceOriginY + balanceTitle.size.height + balanceValue.size.height / 2.0 + 2.0 - UIScreenPixel))
)
if state.cachedStarImage == nil || state.cachedStarImage?.1 !== theme {
@ -416,7 +417,7 @@ private final class SheetContent: CombinedComponent {
let resultController = UndoOverlayController(
presentationData: presentationData,
content: .image(
image: UIImage(bundleImageName: "Premium/Stars/StarMedium")!,
image: UIImage(bundleImageName: "Premium/Stars/StarLarge")!,
title: presentationData.strings.Stars_Transfer_PurchasedTitle,
text: presentationData.strings.Stars_Transfer_PurchasedText(invoice.title, botTitle, presentationData.strings.Stars_Transfer_Purchased_Stars(Int32(invoice.totalAmount))).string,
round: false,

View file

@ -0,0 +1,24 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "particle.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 701 B