mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
2045 lines
92 KiB
Swift
2045 lines
92 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import AsyncDisplayKit
|
|
import TelegramPresentationData
|
|
import ChatPresentationInterfaceState
|
|
import ComponentFlow
|
|
import AccountContext
|
|
import ViewControllerComponent
|
|
import TelegramCore
|
|
import SwiftSignalKit
|
|
import Display
|
|
import MultilineTextComponent
|
|
import ButtonComponent
|
|
import PlainButtonComponent
|
|
import Markdown
|
|
import EmojiStatusComponent
|
|
import SliderComponent
|
|
import RoundedRectWithTailPath
|
|
import AvatarNode
|
|
import BundleIconComponent
|
|
|
|
private final class BalanceComponent: CombinedComponent {
|
|
let context: AccountContext
|
|
let theme: PresentationTheme
|
|
let strings: PresentationStrings
|
|
let balance: Int64?
|
|
|
|
init(
|
|
context: AccountContext,
|
|
theme: PresentationTheme,
|
|
strings: PresentationStrings,
|
|
balance: Int64?
|
|
) {
|
|
self.context = context
|
|
self.theme = theme
|
|
self.strings = strings
|
|
self.balance = balance
|
|
}
|
|
|
|
static func ==(lhs: BalanceComponent, rhs: BalanceComponent) -> Bool {
|
|
if lhs.context !== rhs.context {
|
|
return false
|
|
}
|
|
if lhs.theme !== rhs.theme {
|
|
return false
|
|
}
|
|
if lhs.strings !== rhs.strings {
|
|
return false
|
|
}
|
|
if lhs.balance != rhs.balance {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
static var body: Body {
|
|
let title = Child(MultilineTextComponent.self)
|
|
let balance = Child(MultilineTextComponent.self)
|
|
let icon = Child(BundleIconComponent.self)
|
|
|
|
return { context in
|
|
var size = CGSize(width: 0.0, height: 0.0)
|
|
|
|
//TODO:localize
|
|
let title = title.update(
|
|
component: MultilineTextComponent(
|
|
text: .plain(NSAttributedString(string: "Balance", font: Font.regular(14.0), textColor: context.component.theme.list.itemPrimaryTextColor))
|
|
),
|
|
availableSize: context.availableSize,
|
|
transition: .immediate
|
|
)
|
|
|
|
size.width = max(size.width, title.size.width)
|
|
size.height += title.size.height
|
|
|
|
let balanceText: String
|
|
if let value = context.component.balance {
|
|
balanceText = "\(value)"
|
|
} else {
|
|
balanceText = "..."
|
|
}
|
|
let balance = balance.update(
|
|
component: MultilineTextComponent(
|
|
text: .plain(NSAttributedString(string: balanceText, font: Font.medium(15.0), textColor: context.component.theme.list.itemPrimaryTextColor))
|
|
),
|
|
availableSize: context.availableSize,
|
|
transition: .immediate
|
|
)
|
|
|
|
let iconSize = CGSize(width: 18.0, height: 18.0)
|
|
let icon = icon.update(
|
|
component: BundleIconComponent(
|
|
name: "Premium/Stars/StarLarge",
|
|
tintColor: nil
|
|
),
|
|
availableSize: iconSize,
|
|
transition: context.transition
|
|
)
|
|
|
|
let titleSpacing: CGFloat = 1.0
|
|
let iconSpacing: CGFloat = 2.0
|
|
|
|
size.height += titleSpacing
|
|
|
|
size.width = max(size.width, icon.size.width + iconSpacing + balance.size.width)
|
|
size.height += balance.size.height
|
|
|
|
context.add(
|
|
title.position(
|
|
title.size.centered(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: title.size)).center
|
|
)
|
|
)
|
|
context.add(
|
|
balance.position(
|
|
balance.size.centered(in: CGRect(origin: CGPoint(x: icon.size.width + iconSpacing, y: title.size.height + titleSpacing), size: balance.size)).center
|
|
)
|
|
)
|
|
context.add(
|
|
icon.position(
|
|
icon.size.centered(in: CGRect(origin: CGPoint(x: -1.0, y: title.size.height + titleSpacing), size: icon.size)).center
|
|
)
|
|
)
|
|
|
|
return size
|
|
}
|
|
}
|
|
}
|
|
|
|
private final class BadgeComponent: Component {
|
|
enum Direction {
|
|
case left
|
|
case right
|
|
}
|
|
let theme: PresentationTheme
|
|
let title: String
|
|
let inertiaDirection: Direction?
|
|
|
|
init(
|
|
theme: PresentationTheme,
|
|
title: String,
|
|
inertiaDirection: Direction?
|
|
) {
|
|
self.theme = theme
|
|
self.title = title
|
|
self.inertiaDirection = inertiaDirection
|
|
}
|
|
|
|
static func ==(lhs: BadgeComponent, rhs: BadgeComponent) -> Bool {
|
|
if lhs.theme !== rhs.theme {
|
|
return false
|
|
}
|
|
if lhs.title != rhs.title {
|
|
return false
|
|
}
|
|
if lhs.inertiaDirection != rhs.inertiaDirection {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
final class View: UIView {
|
|
private let badgeView: UIView
|
|
private let badgeMaskView: UIView
|
|
private let badgeShapeLayer = SimpleShapeLayer()
|
|
|
|
private let badgeForeground: SimpleLayer
|
|
let badgeIcon: UIImageView
|
|
private let badgeLabel: BadgeLabelView
|
|
private let badgeLabelMaskView = UIImageView()
|
|
|
|
private var badgeTailPosition: CGFloat = 0.0
|
|
private var badgeShapeArguments: (Double, Double, CGSize, CGFloat, CGFloat)?
|
|
|
|
private var component: BadgeComponent?
|
|
|
|
private var previousAvailableSize: CGSize?
|
|
private var previousInertiaDirection: BadgeComponent.Direction?
|
|
|
|
override init(frame: CGRect) {
|
|
self.badgeView = UIView()
|
|
self.badgeView.alpha = 0.0
|
|
|
|
self.badgeShapeLayer.fillColor = UIColor.white.cgColor
|
|
self.badgeShapeLayer.transform = CATransform3DMakeScale(1.0, -1.0, 1.0)
|
|
|
|
self.badgeMaskView = UIView()
|
|
self.badgeMaskView.layer.addSublayer(self.badgeShapeLayer)
|
|
self.badgeView.mask = self.badgeMaskView
|
|
|
|
self.badgeForeground = SimpleLayer()
|
|
|
|
self.badgeIcon = UIImageView()
|
|
self.badgeIcon.contentMode = .center
|
|
|
|
self.badgeLabel = BadgeLabelView()
|
|
let _ = self.badgeLabel.update(value: "0", transition: .immediate)
|
|
self.badgeLabel.mask = self.badgeLabelMaskView
|
|
|
|
super.init(frame: frame)
|
|
|
|
self.addSubview(self.badgeView)
|
|
self.badgeView.layer.addSublayer(self.badgeForeground)
|
|
self.badgeView.addSubview(self.badgeIcon)
|
|
self.badgeView.addSubview(self.badgeLabel)
|
|
|
|
self.badgeLabelMaskView.contentMode = .scaleToFill
|
|
self.badgeLabelMaskView.image = generateImage(CGSize(width: 2.0, height: 36.0), rotatedContext: { size, context in
|
|
let bounds = CGRect(origin: .zero, size: size)
|
|
context.clear(bounds)
|
|
|
|
let colorsArray: [CGColor] = [
|
|
UIColor(rgb: 0xffffff, alpha: 0.0).cgColor,
|
|
UIColor(rgb: 0xffffff).cgColor,
|
|
UIColor(rgb: 0xffffff).cgColor,
|
|
UIColor(rgb: 0xffffff, alpha: 0.0).cgColor,
|
|
]
|
|
var locations: [CGFloat] = [0.0, 0.24, 0.76, 1.0]
|
|
let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray as CFArray, locations: &locations)!
|
|
|
|
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
|
|
})
|
|
|
|
self.isUserInteractionEnabled = false
|
|
}
|
|
|
|
required init(coder: NSCoder) {
|
|
preconditionFailure()
|
|
}
|
|
|
|
func update(component: BadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
|
if self.component == nil {
|
|
self.badgeIcon.image = UIImage(bundleImageName: "Premium/SendStarsStarSliderIcon")?.withRenderingMode(.alwaysTemplate)
|
|
}
|
|
|
|
self.component = component
|
|
self.badgeIcon.tintColor = .white
|
|
|
|
self.badgeLabel.color = .white
|
|
|
|
let badgeLabelSize = self.badgeLabel.update(value: component.title, transition: .easeInOut(duration: 0.12))
|
|
let countWidth: CGFloat = badgeLabelSize.width + 3.0
|
|
let badgeWidth: CGFloat = countWidth + 54.0
|
|
|
|
let badgeSize = CGSize(width: badgeWidth, height: 48.0)
|
|
let badgeFullSize = CGSize(width: badgeWidth, height: 48.0 + 12.0)
|
|
self.badgeMaskView.frame = CGRect(origin: .zero, size: badgeFullSize)
|
|
self.badgeShapeLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -4.0), size: badgeFullSize)
|
|
|
|
self.badgeView.bounds = CGRect(origin: .zero, size: badgeFullSize)
|
|
|
|
transition.setAnchorPoint(layer: self.badgeView.layer, anchorPoint: CGPoint(x: 0.5, y: 1.0))
|
|
|
|
if component.inertiaDirection != self.previousInertiaDirection {
|
|
self.previousInertiaDirection = component.inertiaDirection
|
|
|
|
var angle: CGFloat = 0.0
|
|
let transition: ContainedViewLayoutTransition
|
|
if let inertiaDirection = component.inertiaDirection {
|
|
switch inertiaDirection {
|
|
case .left:
|
|
angle = 0.22
|
|
case .right:
|
|
angle = -0.22
|
|
}
|
|
transition = .animated(duration: 0.45, curve: .spring)
|
|
} else {
|
|
transition = .animated(duration: 0.45, curve: .customSpring(damping: 65.0, initialVelocity: 0.0))
|
|
}
|
|
transition.updateTransformRotation(view: self.badgeView, angle: angle)
|
|
}
|
|
|
|
self.badgeForeground.bounds = CGRect(origin: CGPoint(), size: CGSize(width: badgeFullSize.width * 3.0, height: badgeFullSize.height))
|
|
if self.badgeForeground.animation(forKey: "movement") == nil {
|
|
self.badgeForeground.position = CGPoint(x: badgeSize.width * 3.0 / 2.0 - self.badgeForeground.frame.width * 0.35, y: badgeFullSize.height / 2.0)
|
|
}
|
|
|
|
self.badgeIcon.frame = CGRect(x: 10.0, y: 9.0, width: 30.0, height: 30.0)
|
|
self.badgeLabelMaskView.frame = CGRect(x: 0.0, y: 0.0, width: 100.0, height: 36.0)
|
|
|
|
self.badgeView.alpha = 1.0
|
|
|
|
let size = badgeSize
|
|
transition.setFrame(view: self.badgeLabel, frame: CGRect(origin: CGPoint(x: 14.0 + floorToScreenPixels((badgeFullSize.width - badgeLabelSize.width) / 2.0), y: 5.0), size: badgeLabelSize))
|
|
|
|
if self.previousAvailableSize != availableSize {
|
|
self.previousAvailableSize = availableSize
|
|
|
|
let activeColors: [UIColor] = [
|
|
UIColor(rgb: 0xFFAB03),
|
|
UIColor(rgb: 0xFFCB37)
|
|
]
|
|
|
|
var locations: [CGFloat] = []
|
|
let delta = 1.0 / CGFloat(activeColors.count - 1)
|
|
for i in 0 ..< activeColors.count {
|
|
locations.append(delta * CGFloat(i))
|
|
}
|
|
|
|
let gradient = generateGradientImage(size: CGSize(width: 200.0, height: 60.0), colors: activeColors, locations: locations, direction: .horizontal)
|
|
self.badgeForeground.contentsGravity = .resizeAspectFill
|
|
self.badgeForeground.contents = gradient?.cgImage
|
|
|
|
self.setupGradientAnimations()
|
|
}
|
|
|
|
return size
|
|
}
|
|
|
|
func adjustTail(size: CGSize, overflowWidth: CGFloat) {
|
|
var tailPosition = size.width * 0.5
|
|
tailPosition += overflowWidth
|
|
tailPosition = max(0.0, min(size.width, tailPosition))
|
|
|
|
self.badgeShapeLayer.path = generateRoundedRectWithTailPath(rectSize: size, tailPosition: tailPosition / size.width).cgPath
|
|
}
|
|
|
|
private func setupGradientAnimations() {
|
|
guard let _ = self.component else {
|
|
return
|
|
}
|
|
if let _ = self.badgeForeground.animation(forKey: "movement") {
|
|
} else {
|
|
CATransaction.begin()
|
|
|
|
let badgeOffset = (self.badgeForeground.frame.width - self.badgeView.bounds.width) / 2.0
|
|
let badgePreviousValue = self.badgeForeground.position.x
|
|
var badgeNewValue: CGFloat = badgeOffset
|
|
if badgeOffset - badgePreviousValue < self.badgeForeground.frame.width * 0.25 {
|
|
badgeNewValue -= self.badgeForeground.frame.width * 0.35
|
|
}
|
|
self.badgeForeground.position = CGPoint(x: badgeNewValue, y: self.badgeForeground.bounds.size.height / 2.0)
|
|
|
|
let badgeAnimation = CABasicAnimation(keyPath: "position.x")
|
|
badgeAnimation.duration = 4.5
|
|
badgeAnimation.fromValue = badgePreviousValue
|
|
badgeAnimation.toValue = badgeNewValue
|
|
badgeAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
|
|
|
CATransaction.setCompletionBlock { [weak self] in
|
|
self?.setupGradientAnimations()
|
|
}
|
|
self.badgeForeground.add(badgeAnimation, forKey: "movement")
|
|
|
|
CATransaction.commit()
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
private final class PeerBadgeComponent: Component {
|
|
let theme: PresentationTheme
|
|
let title: String
|
|
|
|
init(
|
|
theme: PresentationTheme,
|
|
title: String
|
|
) {
|
|
self.theme = theme
|
|
self.title = title
|
|
}
|
|
|
|
static func ==(lhs: PeerBadgeComponent, rhs: PeerBadgeComponent) -> Bool {
|
|
if lhs.theme !== rhs.theme {
|
|
return false
|
|
}
|
|
if lhs.title != rhs.title {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
final class View: UIView {
|
|
private let backgroundMaskLayer = SimpleLayer()
|
|
private let backgroundLayer = SimpleLayer()
|
|
private let title = ComponentView<Empty>()
|
|
private let icon = ComponentView<Empty>()
|
|
|
|
private var component: PeerBadgeComponent?
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
|
|
self.layer.addSublayer(self.backgroundMaskLayer)
|
|
self.layer.addSublayer(self.backgroundLayer)
|
|
}
|
|
|
|
required init(coder: NSCoder) {
|
|
preconditionFailure()
|
|
}
|
|
|
|
func update(component: PeerBadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
|
self.component = component
|
|
|
|
let iconSize = self.icon.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(BundleIconComponent(
|
|
name: "Premium/SendStarsPeerBadgeStarIcon",
|
|
tintColor: .white)
|
|
),
|
|
environment: {},
|
|
containerSize: CGSize(width: 100.0, height: 100.0)
|
|
)
|
|
|
|
let sideInset: CGFloat = 3.0
|
|
let titleSpacing: CGFloat = 1.0
|
|
|
|
let titleSize = self.title.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(MultilineTextComponent(
|
|
text: .plain(NSAttributedString(string: component.title, font: Font.bold(9.0), textColor: .white))
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - titleSpacing - iconSize.width, height: 100.0)
|
|
)
|
|
|
|
let contentSize = CGSize(width: iconSize.width + titleSpacing + titleSize.width, height: titleSize.height)
|
|
let size = CGSize(width: contentSize.width + sideInset * 2.0, height: contentSize.height + 3.0 * 2.0)
|
|
|
|
self.backgroundMaskLayer.backgroundColor = component.theme.list.plainBackgroundColor.cgColor
|
|
self.backgroundLayer.backgroundColor = UIColor(rgb: 0xFFB10D).cgColor
|
|
|
|
let backgroundFrame = CGRect(origin: CGPoint(), size: size)
|
|
self.backgroundLayer.frame = backgroundFrame
|
|
|
|
let badkgroundMaskFrame = backgroundFrame.insetBy(dx: -1.0 - UIScreenPixel, dy: -1.0 - UIScreenPixel)
|
|
self.backgroundMaskLayer.frame = badkgroundMaskFrame
|
|
|
|
self.backgroundLayer.cornerRadius = backgroundFrame.height * 0.5
|
|
self.backgroundMaskLayer.cornerRadius = badkgroundMaskFrame.height * 0.5
|
|
|
|
let titleFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + sideInset + iconSize.width + titleSpacing, y: floor((backgroundFrame.height - titleSize.height) * 0.5)), size: titleSize)
|
|
if let titleView = self.title.view {
|
|
if titleView.superview == nil {
|
|
self.addSubview(titleView)
|
|
}
|
|
titleView.frame = titleFrame
|
|
}
|
|
|
|
let iconFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + sideInset + 1.0, y: floor((backgroundFrame.height - iconSize.height) * 0.5)), size: iconSize)
|
|
if let iconView = self.icon.view {
|
|
if iconView.superview == nil {
|
|
self.addSubview(iconView)
|
|
}
|
|
iconView.frame = iconFrame
|
|
}
|
|
|
|
return size
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
private final class PeerComponent: Component {
|
|
let context: AccountContext
|
|
let theme: PresentationTheme
|
|
let strings: PresentationStrings
|
|
let peer: EnginePeer?
|
|
let count: Int
|
|
|
|
init(
|
|
context: AccountContext,
|
|
theme: PresentationTheme,
|
|
strings: PresentationStrings,
|
|
peer: EnginePeer?,
|
|
count: Int
|
|
) {
|
|
self.context = context
|
|
self.theme = theme
|
|
self.strings = strings
|
|
self.peer = peer
|
|
self.count = count
|
|
}
|
|
|
|
static func ==(lhs: PeerComponent, rhs: PeerComponent) -> Bool {
|
|
if lhs.context !== rhs.context {
|
|
return false
|
|
}
|
|
if lhs.theme !== rhs.theme {
|
|
return false
|
|
}
|
|
if lhs.strings !== rhs.strings {
|
|
return false
|
|
}
|
|
if lhs.peer != rhs.peer {
|
|
return false
|
|
}
|
|
if lhs.count != rhs.count {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
final class View: UIView {
|
|
private var avatarNode: AvatarNode?
|
|
private let badge = ComponentView<Empty>()
|
|
private let title = ComponentView<Empty>()
|
|
|
|
private var component: PeerComponent?
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
}
|
|
|
|
required init(coder: NSCoder) {
|
|
preconditionFailure()
|
|
}
|
|
|
|
func update(component: PeerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
|
self.component = component
|
|
|
|
let avatarNode: AvatarNode
|
|
if let current = self.avatarNode {
|
|
avatarNode = current
|
|
} else {
|
|
avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 24.0))
|
|
self.avatarNode = avatarNode
|
|
self.addSubview(avatarNode.view)
|
|
}
|
|
|
|
let avatarSize = CGSize(width: 60.0, height: 60.0)
|
|
let avatarFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: avatarSize)
|
|
avatarNode.frame = avatarFrame
|
|
if let peer = component.peer {
|
|
avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer)
|
|
} else {
|
|
avatarNode.setPeer(context: component.context, theme: component.theme, peer: nil, overrideImage: .anonymousSavedMessagesIcon)
|
|
}
|
|
avatarNode.updateSize(size: avatarFrame.size)
|
|
|
|
let badgeSize = self.badge.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(PeerBadgeComponent(
|
|
theme: component.theme,
|
|
title: "\(component.count)"
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: 200.0, height: 200.0)
|
|
)
|
|
let badgeFrame = CGRect(origin: CGPoint(x: avatarFrame.minX + floor((avatarFrame.width - badgeSize.width) * 0.5), y: avatarFrame.maxY - badgeSize.height + 3.0), size: badgeSize)
|
|
if let badgeView = self.badge.view {
|
|
if badgeView.superview == nil {
|
|
self.addSubview(badgeView)
|
|
}
|
|
badgeView.frame = badgeFrame
|
|
}
|
|
|
|
let titleSpacing: CGFloat = 8.0
|
|
|
|
let peerTitle: String
|
|
if let peer = component.peer {
|
|
peerTitle = peer.compactDisplayTitle
|
|
} else {
|
|
//TODO:localize
|
|
peerTitle = "Anonymous"
|
|
}
|
|
|
|
let titleSize = self.title.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(MultilineTextComponent(
|
|
text: .plain(NSAttributedString(string: peerTitle, font: Font.regular(11.0), textColor: component.theme.list.itemPrimaryTextColor))
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: avatarSize.width + 10.0 * 2.0, height: 100.0)
|
|
)
|
|
let titleFrame = CGRect(origin: CGPoint(x: floor((avatarSize.width - titleSize.width) * 0.5), y: avatarSize.height + titleSpacing), size: titleSize)
|
|
if let titleView = self.title.view {
|
|
if titleView.superview == nil {
|
|
self.addSubview(titleView)
|
|
}
|
|
titleView.frame = titleFrame
|
|
}
|
|
|
|
return CGSize(width: avatarSize.width, height: avatarSize.height + titleSpacing + titleSize.height)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
private final class SliderBackgroundComponent: Component {
|
|
let theme: PresentationTheme
|
|
let value: CGFloat
|
|
let topCutoff: CGFloat?
|
|
|
|
init(
|
|
theme: PresentationTheme,
|
|
value: CGFloat,
|
|
topCutoff: CGFloat?
|
|
) {
|
|
self.theme = theme
|
|
self.value = value
|
|
self.topCutoff = topCutoff
|
|
}
|
|
|
|
static func ==(lhs: SliderBackgroundComponent, rhs: SliderBackgroundComponent) -> Bool {
|
|
if lhs.theme !== rhs.theme {
|
|
return false
|
|
}
|
|
if lhs.value != rhs.value {
|
|
return false
|
|
}
|
|
if lhs.topCutoff != rhs.topCutoff {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
final class View: UIView {
|
|
private let sliderBackground = UIView()
|
|
private let sliderForeground = UIView()
|
|
private let sliderStars = SliderStarsView()
|
|
|
|
private let topForegroundLine = SimpleLayer()
|
|
private let topBackgroundLine = SimpleLayer()
|
|
private let topForegroundText = ComponentView<Empty>()
|
|
private let topBackgroundText = ComponentView<Empty>()
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
|
|
self.sliderBackground.clipsToBounds = true
|
|
|
|
self.sliderForeground.clipsToBounds = true
|
|
self.sliderForeground.addSubview(self.sliderStars)
|
|
|
|
self.addSubview(self.sliderBackground)
|
|
self.addSubview(self.sliderForeground)
|
|
|
|
self.sliderBackground.layer.addSublayer(self.topBackgroundLine)
|
|
self.sliderForeground.layer.addSublayer(self.topForegroundLine)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func update(component: SliderBackgroundComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
|
self.sliderBackground.backgroundColor = UIColor(rgb: 0xEEEEEF)
|
|
self.sliderForeground.backgroundColor = UIColor(rgb: 0xFFB10D)
|
|
self.topForegroundLine.backgroundColor = component.theme.list.plainBackgroundColor.cgColor
|
|
self.topBackgroundLine.backgroundColor = component.theme.list.plainBackgroundColor.cgColor
|
|
|
|
transition.setFrame(view: self.sliderBackground, frame: CGRect(origin: CGPoint(), size: availableSize))
|
|
|
|
let sliderMinWidth = availableSize.height
|
|
let sliderAreaWidth: CGFloat = availableSize.width - sliderMinWidth
|
|
let sliderForegroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: sliderMinWidth + floorToScreenPixels(sliderAreaWidth * component.value), height: availableSize.height))
|
|
transition.setFrame(view: self.sliderForeground, frame: sliderForegroundFrame)
|
|
|
|
self.sliderBackground.layer.cornerRadius = availableSize.height * 0.5
|
|
self.sliderForeground.layer.cornerRadius = availableSize.height * 0.5
|
|
|
|
self.sliderStars.frame = CGRect(origin: .zero, size: availableSize)
|
|
self.sliderStars.update(size: availableSize, value: component.value)
|
|
|
|
self.sliderForeground.isHidden = sliderForegroundFrame.width <= sliderMinWidth
|
|
|
|
let topCutoff = component.topCutoff ?? 0.0
|
|
|
|
let topLineFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(sliderAreaWidth * topCutoff), y: 0.0), size: CGSize(width: 1.0, height: availableSize.height))
|
|
transition.setFrame(layer: self.topForegroundLine, frame: topLineFrame)
|
|
transition.setFrame(layer: self.topBackgroundLine, frame: topLineFrame)
|
|
|
|
//TODO:localize
|
|
let topTextSize = self.topForegroundText.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(MultilineTextComponent(
|
|
text: .plain(NSAttributedString(string: "TOP", font: Font.medium(17.0), textColor: UIColor(white: 1.0, alpha: 0.4)))
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width, height: 100.0)
|
|
)
|
|
let _ = self.topBackgroundText.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(MultilineTextComponent(
|
|
text: .plain(NSAttributedString(string: "TOP", font: Font.medium(17.0), textColor: UIColor(white: 0.0, alpha: 0.1)))
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width, height: 100.0)
|
|
)
|
|
let topTextFrame = CGRect(origin: CGPoint(x: topLineFrame.maxX + 6.0, y: floor((availableSize.height - topTextSize.height) * 0.5)), size: topTextSize)
|
|
if let topForegroundTextView = self.topForegroundText.view, let topBackgroundTextView = self.topBackgroundText.view {
|
|
if topForegroundTextView.superview == nil {
|
|
topBackgroundTextView.layer.anchorPoint = CGPoint()
|
|
self.sliderBackground.addSubview(topBackgroundTextView)
|
|
|
|
topForegroundTextView.layer.anchorPoint = CGPoint()
|
|
self.sliderForeground.addSubview(topForegroundTextView)
|
|
}
|
|
|
|
transition.setPosition(view: topForegroundTextView, position: topTextFrame.origin)
|
|
transition.setPosition(view: topBackgroundTextView, position: topTextFrame.origin)
|
|
|
|
topForegroundTextView.bounds = CGRect(origin: CGPoint(), size: topTextFrame.size)
|
|
topBackgroundTextView.bounds = CGRect(origin: CGPoint(), size: topTextFrame.size)
|
|
|
|
topForegroundTextView.isHidden = component.topCutoff == nil || topTextFrame.minX <= 10.0 || topTextFrame.maxX >= availableSize.width - 4.0
|
|
topBackgroundTextView.isHidden = topForegroundTextView.isHidden
|
|
self.topBackgroundLine.isHidden = topForegroundTextView.isHidden
|
|
self.topForegroundLine.isHidden = topForegroundTextView.isHidden
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
private final class ChatSendStarsScreenComponent: Component {
|
|
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
|
|
|
let context: AccountContext
|
|
let peer: EnginePeer
|
|
let maxAmount: Int
|
|
let balance: Int64?
|
|
let currentSentAmount: Int?
|
|
let topPeers: [ChatSendStarsScreen.TopPeer]
|
|
let completion: (Int64, Bool, ChatSendStarsScreen.TransitionOut) -> Void
|
|
|
|
init(
|
|
context: AccountContext,
|
|
peer: EnginePeer,
|
|
maxAmount: Int,
|
|
balance: Int64?,
|
|
currentSentAmount: Int?,
|
|
topPeers: [ChatSendStarsScreen.TopPeer],
|
|
completion: @escaping (Int64, Bool, ChatSendStarsScreen.TransitionOut) -> Void
|
|
) {
|
|
self.context = context
|
|
self.peer = peer
|
|
self.maxAmount = maxAmount
|
|
self.balance = balance
|
|
self.currentSentAmount = currentSentAmount
|
|
self.topPeers = topPeers
|
|
self.completion = completion
|
|
}
|
|
|
|
static func ==(lhs: ChatSendStarsScreenComponent, rhs: ChatSendStarsScreenComponent) -> Bool {
|
|
if lhs.context !== rhs.context {
|
|
return false
|
|
}
|
|
if lhs.peer != rhs.peer {
|
|
return false
|
|
}
|
|
if lhs.maxAmount != rhs.maxAmount {
|
|
return false
|
|
}
|
|
if lhs.balance != rhs.balance {
|
|
return false
|
|
}
|
|
if lhs.currentSentAmount != rhs.currentSentAmount {
|
|
return false
|
|
}
|
|
if lhs.topPeers != rhs.topPeers {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
private struct ItemLayout: Equatable {
|
|
var containerSize: CGSize
|
|
var containerInset: CGFloat
|
|
var bottomInset: CGFloat
|
|
var topInset: CGFloat
|
|
|
|
init(containerSize: CGSize, containerInset: CGFloat, bottomInset: CGFloat, topInset: CGFloat) {
|
|
self.containerSize = containerSize
|
|
self.containerInset = containerInset
|
|
self.bottomInset = bottomInset
|
|
self.topInset = topInset
|
|
}
|
|
}
|
|
|
|
private final class ScrollView: UIScrollView {
|
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
return super.hitTest(point, with: event)
|
|
}
|
|
}
|
|
|
|
final class View: UIView, UIScrollViewDelegate {
|
|
private let dimView: UIView
|
|
private let backgroundLayer: SimpleLayer
|
|
private let navigationBarContainer: SparseContainerView
|
|
private let scrollView: ScrollView
|
|
private let scrollContentClippingView: SparseContainerView
|
|
private let scrollContentView: UIView
|
|
|
|
private let leftButton = ComponentView<Empty>()
|
|
private let closeButton = ComponentView<Empty>()
|
|
|
|
private let title = ComponentView<Empty>()
|
|
private let descriptionText = ComponentView<Empty>()
|
|
|
|
private let badgeStars = BadgeStarsView()
|
|
private let sliderBackground = ComponentView<Empty>()
|
|
private let slider = ComponentView<Empty>()
|
|
private let badge = ComponentView<Empty>()
|
|
|
|
private var topPeersLeftSeparator: SimpleLayer?
|
|
private var topPeersRightSeparator: SimpleLayer?
|
|
private var topPeersTitleBackground: SimpleLayer?
|
|
private var topPeersTitle: ComponentView<Empty>?
|
|
|
|
private var topPeerItems: [ChatSendStarsScreen.TopPeer.Id: ComponentView<Empty>] = [:]
|
|
|
|
private let actionButton = ComponentView<Empty>()
|
|
private let buttonDescriptionText = ComponentView<Empty>()
|
|
|
|
private let bottomOverscrollLimit: CGFloat
|
|
|
|
private var ignoreScrolling: Bool = false
|
|
|
|
private var component: ChatSendStarsScreenComponent?
|
|
private weak var state: EmptyComponentState?
|
|
private var environment: ViewControllerComponentContainer.Environment?
|
|
private var itemLayout: ItemLayout?
|
|
|
|
private var topOffsetDistance: CGFloat?
|
|
|
|
private var amount: Int64 = 1
|
|
private var cachedStarImage: (UIImage, PresentationTheme)?
|
|
private var cachedCloseImage: UIImage?
|
|
|
|
private var isPastTopCutoff: Bool?
|
|
|
|
override init(frame: CGRect) {
|
|
self.bottomOverscrollLimit = 200.0
|
|
|
|
self.dimView = UIView()
|
|
|
|
self.backgroundLayer = SimpleLayer()
|
|
self.backgroundLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
|
self.backgroundLayer.cornerRadius = 10.0
|
|
|
|
self.navigationBarContainer = SparseContainerView()
|
|
|
|
self.scrollView = ScrollView()
|
|
|
|
self.scrollContentClippingView = SparseContainerView()
|
|
self.scrollContentClippingView.clipsToBounds = true
|
|
|
|
self.scrollContentView = UIView()
|
|
|
|
super.init(frame: frame)
|
|
|
|
self.addSubview(self.dimView)
|
|
self.layer.addSublayer(self.backgroundLayer)
|
|
|
|
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.alwaysBounceVertical = true
|
|
self.scrollView.scrollsToTop = false
|
|
self.scrollView.delegate = self
|
|
self.scrollView.clipsToBounds = true
|
|
|
|
self.addSubview(self.scrollContentClippingView)
|
|
self.scrollContentClippingView.addSubview(self.scrollView)
|
|
|
|
self.scrollView.addSubview(self.scrollContentView)
|
|
|
|
self.addSubview(self.navigationBarContainer)
|
|
|
|
self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
if !self.ignoreScrolling {
|
|
self.updateScrolling(transition: .immediate)
|
|
}
|
|
}
|
|
|
|
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
|
|
guard let itemLayout = self.itemLayout, let topOffsetDistance = self.topOffsetDistance else {
|
|
return
|
|
}
|
|
|
|
var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset
|
|
topOffset = max(0.0, topOffset)
|
|
|
|
if topOffset < topOffsetDistance {
|
|
targetContentOffset.pointee.y = scrollView.contentOffset.y
|
|
scrollView.setContentOffset(CGPoint(x: 0.0, y: itemLayout.topInset), animated: true)
|
|
}
|
|
}
|
|
|
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
if !self.bounds.contains(point) {
|
|
return nil
|
|
}
|
|
if !self.backgroundLayer.frame.contains(point) {
|
|
return self.dimView
|
|
}
|
|
|
|
if let result = self.navigationBarContainer.hitTest(self.convert(point, to: self.navigationBarContainer), with: event) {
|
|
return result
|
|
}
|
|
|
|
let result = super.hitTest(point, with: event)
|
|
return result
|
|
}
|
|
|
|
@objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
|
|
if case .ended = recognizer.state {
|
|
guard let environment = self.environment, let controller = environment.controller() else {
|
|
return
|
|
}
|
|
controller.dismiss()
|
|
}
|
|
}
|
|
|
|
private func updateScrolling(transition: ComponentTransition) {
|
|
guard let environment = self.environment, let controller = environment.controller(), let itemLayout = self.itemLayout else {
|
|
return
|
|
}
|
|
var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset
|
|
topOffset = max(0.0, topOffset)
|
|
transition.setTransform(layer: self.backgroundLayer, transform: CATransform3DMakeTranslation(0.0, topOffset + itemLayout.containerInset, 0.0))
|
|
|
|
transition.setPosition(view: self.navigationBarContainer, position: CGPoint(x: 0.0, y: topOffset + itemLayout.containerInset))
|
|
|
|
let topOffsetDistance: CGFloat = min(200.0, floor(itemLayout.containerSize.height * 0.25))
|
|
self.topOffsetDistance = topOffsetDistance
|
|
var topOffsetFraction = topOffset / topOffsetDistance
|
|
topOffsetFraction = max(0.0, min(1.0, topOffsetFraction))
|
|
|
|
let transitionFactor: CGFloat = 1.0 - topOffsetFraction
|
|
controller.updateModalStyleOverlayTransitionFactor(transitionFactor, transition: transition.containedViewLayoutTransition)
|
|
}
|
|
|
|
func animateIn() {
|
|
self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
|
let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY
|
|
self.scrollContentClippingView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
|
self.backgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
|
self.navigationBarContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
|
if let actionButtonView = self.actionButton.view {
|
|
actionButtonView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
|
}
|
|
if let buttonDescriptionTextView = self.buttonDescriptionText.view {
|
|
buttonDescriptionTextView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
|
}
|
|
}
|
|
|
|
func animateOut(completion: @escaping () -> Void) {
|
|
let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY
|
|
|
|
self.dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
|
|
self.scrollContentClippingView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in
|
|
completion()
|
|
})
|
|
self.backgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
|
|
self.navigationBarContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
|
|
if let actionButtonView = self.actionButton.view {
|
|
actionButtonView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
|
|
}
|
|
if let buttonDescriptionTextView = self.buttonDescriptionText.view {
|
|
buttonDescriptionTextView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
|
|
}
|
|
}
|
|
|
|
private var previousSliderValue: Float = 0.0
|
|
private var previousTimestamp: Double?
|
|
private var inertiaDirection: BadgeComponent.Direction?
|
|
|
|
func update(component: ChatSendStarsScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
|
|
let environment = environment[ViewControllerComponentContainer.Environment.self].value
|
|
let themeUpdated = self.environment?.theme !== environment.theme
|
|
|
|
let resetScrolling = self.scrollView.bounds.width != availableSize.width
|
|
|
|
let sideInset: CGFloat = 16.0
|
|
|
|
if self.component == nil {
|
|
self.amount = 50
|
|
}
|
|
|
|
self.component = component
|
|
self.state = state
|
|
self.environment = environment
|
|
|
|
if themeUpdated {
|
|
self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
|
|
self.backgroundLayer.backgroundColor = environment.theme.list.plainBackgroundColor.cgColor
|
|
|
|
var locations: [NSNumber] = []
|
|
var colors: [CGColor] = []
|
|
let numStops = 6
|
|
for i in 0 ..< numStops {
|
|
let step = CGFloat(i) / CGFloat(numStops - 1)
|
|
locations.append(step as NSNumber)
|
|
colors.append(environment.theme.list.blocksBackgroundColor.withAlphaComponent(1.0 - step * step).cgColor)
|
|
}
|
|
}
|
|
|
|
transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize))
|
|
|
|
var contentHeight: CGFloat = 0.0
|
|
|
|
let sliderInset: CGFloat = sideInset + 8.0
|
|
let sliderSize = self.slider.update(
|
|
transition: transition,
|
|
component: AnyComponent(SliderComponent(
|
|
valueCount: component.maxAmount,
|
|
value: Int(self.amount),
|
|
markPositions: false,
|
|
trackBackgroundColor: .clear,
|
|
trackForegroundColor: .clear,
|
|
knobSize: 26.0,
|
|
knobColor: .white,
|
|
valueUpdated: { [weak self] value in
|
|
guard let self, let component = self.component else {
|
|
return
|
|
}
|
|
self.amount = 1 + Int64(value)
|
|
self.state?.updated(transition: .immediate)
|
|
|
|
let sliderValue = Float(value) / Float(component.maxAmount)
|
|
let currentTimestamp = CACurrentMediaTime()
|
|
|
|
if let previousTimestamp {
|
|
let deltaTime = currentTimestamp - previousTimestamp
|
|
let delta = sliderValue - self.previousSliderValue
|
|
let deltaValue = abs(sliderValue - self.previousSliderValue)
|
|
|
|
let speed = deltaValue / Float(deltaTime)
|
|
let newSpeed = max(0, min(65.0, speed * 70.0))
|
|
|
|
var inertiaDirection: BadgeComponent.Direction?
|
|
if newSpeed >= 1.0 {
|
|
if delta > 0.0 {
|
|
inertiaDirection = .right
|
|
} else {
|
|
inertiaDirection = .left
|
|
}
|
|
}
|
|
if inertiaDirection != self.inertiaDirection {
|
|
self.inertiaDirection = inertiaDirection
|
|
self.state?.updated(transition: .immediate)
|
|
}
|
|
|
|
if newSpeed < 0.01 && deltaValue < 0.001 {
|
|
|
|
} else {
|
|
self.badgeStars.update(speed: newSpeed, delta: delta)
|
|
}
|
|
}
|
|
|
|
self.previousSliderValue = sliderValue
|
|
self.previousTimestamp = currentTimestamp
|
|
},
|
|
isTrackingUpdated: { [weak self] isTracking in
|
|
guard let self else {
|
|
return
|
|
}
|
|
if !isTracking {
|
|
self.previousTimestamp = nil
|
|
self.badgeStars.update(speed: 0.0)
|
|
}
|
|
if self.inertiaDirection != nil {
|
|
self.inertiaDirection = nil
|
|
self.state?.updated(transition: .immediate)
|
|
}
|
|
}
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width - sliderInset * 2.0, height: 30.0)
|
|
)
|
|
let sliderFrame = CGRect(origin: CGPoint(x: sliderInset, y: contentHeight + 127.0), size: sliderSize)
|
|
let sliderBackgroundFrame = CGRect(origin: CGPoint(x: sliderFrame.minX - 8.0, y: sliderFrame.minY + 7.0), size: CGSize(width: sliderFrame.width + 16.0, height: sliderFrame.height - 14.0))
|
|
|
|
let progressFraction: CGFloat = CGFloat(self.amount) / CGFloat(component.maxAmount - 1)
|
|
|
|
let topCount = component.topPeers.max(by: { $0.count < $1.count })?.count
|
|
|
|
var topCutoffFraction: CGFloat?
|
|
if let topCount {
|
|
let topCutoffFractionValue = CGFloat(topCount) / CGFloat(component.maxAmount - 1)
|
|
topCutoffFraction = topCutoffFractionValue
|
|
|
|
let isPastCutoff = progressFraction >= topCutoffFractionValue
|
|
if let isPastTopCutoff = self.isPastTopCutoff, isPastTopCutoff != isPastCutoff {
|
|
HapticFeedback().tap()
|
|
}
|
|
self.isPastTopCutoff = isPastCutoff
|
|
} else {
|
|
self.isPastTopCutoff = nil
|
|
}
|
|
|
|
let _ = self.sliderBackground.update(
|
|
transition: transition,
|
|
component: AnyComponent(SliderBackgroundComponent(
|
|
theme: environment.theme,
|
|
value: progressFraction,
|
|
topCutoff: topCutoffFraction
|
|
)),
|
|
environment: {},
|
|
containerSize: sliderBackgroundFrame.size
|
|
)
|
|
|
|
if let sliderView = self.slider.view, let sliderBackgroundView = self.sliderBackground.view {
|
|
if sliderView.superview == nil {
|
|
self.scrollContentView.addSubview(self.badgeStars)
|
|
self.scrollContentView.addSubview(sliderBackgroundView)
|
|
self.scrollContentView.addSubview(sliderView)
|
|
}
|
|
transition.setFrame(view: sliderView, frame: sliderFrame)
|
|
|
|
transition.setFrame(view: sliderBackgroundView, frame: sliderBackgroundFrame)
|
|
|
|
var effectiveInertiaDirection = self.inertiaDirection
|
|
if progressFraction <= 0.03 || progressFraction >= 0.97 {
|
|
effectiveInertiaDirection = nil
|
|
}
|
|
|
|
let badgeSize = self.badge.update(
|
|
transition: transition,
|
|
component: AnyComponent(BadgeComponent(
|
|
theme: environment.theme,
|
|
title: "\(self.amount)",
|
|
inertiaDirection: effectiveInertiaDirection
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: 200.0, height: 200.0)
|
|
)
|
|
|
|
let sliderMinWidth = sliderBackgroundFrame.height
|
|
let sliderAreaWidth: CGFloat = sliderBackgroundFrame.width - sliderMinWidth
|
|
let sliderForegroundFrame = CGRect(origin: sliderBackgroundFrame.origin, size: CGSize(width: sliderMinWidth + floorToScreenPixels(sliderAreaWidth * progressFraction), height: sliderBackgroundFrame.height))
|
|
|
|
var badgeFrame = CGRect(origin: CGPoint(x: sliderForegroundFrame.minX + sliderForegroundFrame.width - floorToScreenPixels(sliderMinWidth * 0.5), y: sliderForegroundFrame.minY - 8.0), size: badgeSize)
|
|
if let badgeView = self.badge.view as? BadgeComponent.View {
|
|
if badgeView.superview == nil {
|
|
self.scrollContentView.insertSubview(badgeView, belowSubview: self.badgeStars)
|
|
}
|
|
|
|
let badgeSideInset = sideInset + 15.0
|
|
|
|
let badgeOverflowWidth: CGFloat
|
|
if badgeFrame.minX - badgeSize.width * 0.5 < badgeSideInset {
|
|
badgeOverflowWidth = badgeSideInset - (badgeFrame.minX - badgeSize.width * 0.5)
|
|
} else if badgeFrame.minX + badgeSize.width * 0.5 > availableSize.width - badgeSideInset {
|
|
badgeOverflowWidth = availableSize.width - badgeSideInset - (badgeFrame.minX + badgeSize.width * 0.5)
|
|
} else {
|
|
badgeOverflowWidth = 0.0
|
|
}
|
|
|
|
badgeFrame.origin.x += badgeOverflowWidth
|
|
|
|
badgeView.frame = badgeFrame
|
|
|
|
badgeView.adjustTail(size: badgeSize, overflowWidth: -badgeOverflowWidth)
|
|
}
|
|
|
|
let starsRect = CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: sliderForegroundFrame.midY))
|
|
self.badgeStars.frame = starsRect
|
|
self.badgeStars.update(size: starsRect.size, emitterPosition: CGPoint(x: badgeFrame.minX, y: badgeFrame.midY - 64.0))
|
|
}
|
|
|
|
contentHeight += 123.0
|
|
|
|
let leftButtonSize = self.leftButton.update(
|
|
transition: transition,
|
|
component: AnyComponent(BalanceComponent(
|
|
context: component.context,
|
|
theme: environment.theme,
|
|
strings: environment.strings,
|
|
balance: component.balance
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: 120.0, height: 100.0)
|
|
)
|
|
let leftButtonFrame = CGRect(origin: CGPoint(x: 16.0, y: floor((56.0 - leftButtonSize.height) * 0.5)), size: leftButtonSize)
|
|
if let leftButtonView = self.leftButton.view {
|
|
if leftButtonView.superview == nil {
|
|
self.navigationBarContainer.addSubview(leftButtonView)
|
|
}
|
|
transition.setFrame(view: leftButtonView, frame: leftButtonFrame)
|
|
}
|
|
|
|
if themeUpdated {
|
|
self.cachedCloseImage = nil
|
|
}
|
|
let closeImage: UIImage
|
|
if let current = self.cachedCloseImage {
|
|
closeImage = current
|
|
} else {
|
|
closeImage = generateCloseButtonImage(backgroundColor: UIColor(rgb: 0x808084, alpha: 0.1), foregroundColor: environment.theme.actionSheet.inputClearButtonColor)!
|
|
self.cachedCloseImage = closeImage
|
|
}
|
|
let closeButtonSize = self.closeButton.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(PlainButtonComponent(
|
|
content: AnyComponent(Image(image: closeImage)),
|
|
effectAlignment: .center,
|
|
action: { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.environment?.controller()?.dismiss()
|
|
}
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: 30.0, height: 30.0)
|
|
)
|
|
let closeButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - sideInset - closeButtonSize.width, y: floor((56.0 - leftButtonSize.height) * 0.5)), size: closeButtonSize)
|
|
if let closeButtonView = self.closeButton.view {
|
|
if closeButtonView.superview == nil {
|
|
self.navigationBarContainer.addSubview(closeButtonView)
|
|
}
|
|
transition.setFrame(view: closeButtonView, frame: closeButtonFrame)
|
|
}
|
|
|
|
let containerInset: CGFloat = environment.statusBarHeight + 10.0
|
|
|
|
var initialContentHeight = contentHeight
|
|
let clippingY: CGFloat
|
|
|
|
let title = self.title
|
|
let descriptionText = self.descriptionText
|
|
let actionButton = self.actionButton
|
|
|
|
let titleSize = title.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(MultilineTextComponent(
|
|
text: .plain(NSAttributedString(string: "React with Stars", font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor))
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width - leftButtonFrame.maxX * 2.0, height: 100.0)
|
|
)
|
|
let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: floor((56.0 - titleSize.height) * 0.5)), size: titleSize)
|
|
if let titleView = title.view {
|
|
if titleView.superview == nil {
|
|
self.navigationBarContainer.addSubview(titleView)
|
|
}
|
|
transition.setFrame(view: titleView, frame: titleFrame)
|
|
}
|
|
|
|
contentHeight += 56.0
|
|
contentHeight += 8.0
|
|
|
|
let text: String
|
|
if let currentSentAmount = component.currentSentAmount {
|
|
text = "You sent **\(currentSentAmount)** stars to support this post."
|
|
} else {
|
|
text = "Choose how many stars you want to send to **\(component.peer.debugDisplayTitle)** to support this post."
|
|
}
|
|
|
|
let body = MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor)
|
|
let bold = MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.itemPrimaryTextColor)
|
|
|
|
let descriptionTextSize = descriptionText.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(MultilineTextComponent(
|
|
text: .markdown(text: text, attributes: MarkdownAttributes(
|
|
body: body,
|
|
bold: bold,
|
|
link: body,
|
|
linkAttribute: { _ in nil }
|
|
)),
|
|
horizontalAlignment: .center,
|
|
maximumNumberOfLines: 0
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 16.0 * 2.0, height: 1000.0)
|
|
)
|
|
let descriptionTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - descriptionTextSize.width) * 0.5), y: contentHeight), size: descriptionTextSize)
|
|
if let descriptionTextView = descriptionText.view {
|
|
if descriptionTextView.superview == nil {
|
|
self.scrollContentView.addSubview(descriptionTextView)
|
|
}
|
|
transition.setFrame(view: descriptionTextView, frame: descriptionTextFrame)
|
|
}
|
|
|
|
contentHeight += descriptionTextFrame.height
|
|
contentHeight += 22.0
|
|
contentHeight += 2.0
|
|
|
|
if !component.topPeers.isEmpty {
|
|
contentHeight += 3.0
|
|
|
|
let topPeersLeftSeparator: SimpleLayer
|
|
if let current = self.topPeersLeftSeparator {
|
|
topPeersLeftSeparator = current
|
|
} else {
|
|
topPeersLeftSeparator = SimpleLayer()
|
|
self.topPeersLeftSeparator = topPeersLeftSeparator
|
|
self.scrollContentView.layer.addSublayer(topPeersLeftSeparator)
|
|
}
|
|
|
|
let topPeersRightSeparator: SimpleLayer
|
|
if let current = self.topPeersRightSeparator {
|
|
topPeersRightSeparator = current
|
|
} else {
|
|
topPeersRightSeparator = SimpleLayer()
|
|
self.topPeersRightSeparator = topPeersRightSeparator
|
|
self.scrollContentView.layer.addSublayer(topPeersRightSeparator)
|
|
}
|
|
|
|
let topPeersTitleBackground: SimpleLayer
|
|
if let current = self.topPeersTitleBackground {
|
|
topPeersTitleBackground = current
|
|
} else {
|
|
topPeersTitleBackground = SimpleLayer()
|
|
self.topPeersTitleBackground = topPeersTitleBackground
|
|
self.scrollContentView.layer.addSublayer(topPeersTitleBackground)
|
|
}
|
|
|
|
let topPeersTitle: ComponentView<Empty>
|
|
if let current = self.topPeersTitle {
|
|
topPeersTitle = current
|
|
} else {
|
|
topPeersTitle = ComponentView()
|
|
self.topPeersTitle = topPeersTitle
|
|
}
|
|
|
|
topPeersLeftSeparator.backgroundColor = environment.theme.list.itemPlainSeparatorColor.cgColor
|
|
topPeersRightSeparator.backgroundColor = environment.theme.list.itemPlainSeparatorColor.cgColor
|
|
|
|
//TODO:localize
|
|
let topPeersTitleSize = topPeersTitle.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(MultilineTextComponent(
|
|
text: .plain(NSAttributedString(string: "Top Senders", font: Font.semibold(15.0), textColor: .white))
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: 300.0, height: 100.0)
|
|
)
|
|
let topPeersBackgroundSize = CGSize(width: topPeersTitleSize.width + 16.0 * 2.0, height: topPeersTitleSize.height + 9.0 * 2.0)
|
|
let topPeersBackgroundFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - topPeersBackgroundSize.width) * 0.5), y: contentHeight), size: topPeersBackgroundSize)
|
|
|
|
topPeersTitleBackground.backgroundColor = UIColor(rgb: 0xFFB10D).cgColor
|
|
topPeersTitleBackground.cornerRadius = topPeersBackgroundFrame.height * 0.5
|
|
transition.setFrame(layer: topPeersTitleBackground, frame: topPeersBackgroundFrame)
|
|
|
|
let topPeersTitleFrame = CGRect(origin: CGPoint(x: topPeersBackgroundFrame.minX + floor((topPeersBackgroundFrame.width - topPeersTitleSize.width) * 0.5), y: topPeersBackgroundFrame.minY + floor((topPeersBackgroundFrame.height - topPeersTitleSize.height) * 0.5)), size: topPeersTitleSize)
|
|
if let topPeersTitleView = topPeersTitle.view {
|
|
if topPeersTitleView.superview == nil {
|
|
self.scrollContentView.addSubview(topPeersTitleView)
|
|
}
|
|
transition.setFrame(view: topPeersTitleView, frame: topPeersTitleFrame)
|
|
}
|
|
|
|
let separatorY = topPeersBackgroundFrame.midY
|
|
let separatorSpacing: CGFloat = 10.0
|
|
transition.setFrame(layer: topPeersLeftSeparator, frame: CGRect(origin: CGPoint(x: sideInset, y: separatorY), size: CGSize(width: max(0.0, topPeersBackgroundFrame.minX - separatorSpacing - sideInset), height: UIScreenPixel)))
|
|
transition.setFrame(layer: topPeersRightSeparator, frame: CGRect(origin: CGPoint(x: topPeersBackgroundFrame.maxX + separatorSpacing, y: separatorY), size: CGSize(width: max(0.0, availableSize.width - sideInset - (topPeersBackgroundFrame.maxX + separatorSpacing)), height: UIScreenPixel)))
|
|
|
|
var validIds: [ChatSendStarsScreen.TopPeer.Id] = []
|
|
var items: [(itemView: ComponentView<Empty>, size: CGSize)] = []
|
|
for topPeer in component.topPeers {
|
|
validIds.append(topPeer.id)
|
|
|
|
let itemView: ComponentView<Empty>
|
|
if let current = self.topPeerItems[topPeer.id] {
|
|
itemView = current
|
|
} else {
|
|
itemView = ComponentView()
|
|
self.topPeerItems[topPeer.id] = itemView
|
|
}
|
|
|
|
let itemSize = itemView.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(PlainButtonComponent(
|
|
content: AnyComponent(PeerComponent(
|
|
context: component.context,
|
|
theme: environment.theme,
|
|
strings: environment.strings,
|
|
peer: topPeer.peer,
|
|
count: topPeer.count
|
|
)),
|
|
effectAlignment: .center,
|
|
action: { [weak self] in
|
|
guard let self, let component = self.component, let peer = topPeer.peer else {
|
|
return
|
|
}
|
|
if let peerInfoController = component.context.sharedContext.makePeerInfoController(
|
|
context: component.context,
|
|
updatedPresentationData: nil,
|
|
peer: peer._asPeer(),
|
|
mode: .generic,
|
|
avatarInitiallyExpanded: false,
|
|
fromChat: false,
|
|
requestsContext: nil
|
|
) {
|
|
self.environment?.controller()?.push(peerInfoController)
|
|
}
|
|
},
|
|
isEnabled: topPeer.peer != nil && topPeer.peer?.id != component.context.account.peerId,
|
|
animateAlpha: false
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: 200.0, height: 200.0)
|
|
)
|
|
items.append((itemView, itemSize))
|
|
}
|
|
var removedIds: [ChatSendStarsScreen.TopPeer.Id] = []
|
|
for (id, itemView) in self.topPeerItems {
|
|
if !validIds.contains(id) {
|
|
removedIds.append(id)
|
|
itemView.view?.removeFromSuperview()
|
|
}
|
|
}
|
|
for id in removedIds {
|
|
self.topPeerItems.removeValue(forKey: id)
|
|
}
|
|
|
|
var itemsWidth: CGFloat = 0.0
|
|
for (_, itemSize) in items {
|
|
itemsWidth += itemSize.width
|
|
}
|
|
|
|
let maxItemSpacing = 48.0
|
|
var itemSpacing = floor((availableSize.width - itemsWidth) / CGFloat(items.count + 1))
|
|
itemSpacing = min(itemSpacing, maxItemSpacing)
|
|
|
|
let totalWidth = itemsWidth + itemSpacing * CGFloat(items.count + 1)
|
|
var itemX: CGFloat = floor((availableSize.width - totalWidth) * 0.5) + itemSpacing
|
|
for (itemView, itemSize) in items {
|
|
if let itemComponentView = itemView.view {
|
|
if itemComponentView.superview == nil {
|
|
self.scrollContentView.addSubview(itemComponentView)
|
|
}
|
|
itemComponentView.frame = CGRect(origin: CGPoint(x: itemX, y: contentHeight + 56.0), size: itemSize)
|
|
}
|
|
itemX += itemSize.width + itemSpacing
|
|
}
|
|
|
|
contentHeight += 161.0
|
|
}
|
|
|
|
initialContentHeight = contentHeight
|
|
|
|
if self.cachedStarImage == nil || self.cachedStarImage?.1 !== environment.theme {
|
|
self.cachedStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: .white)!, environment.theme)
|
|
}
|
|
|
|
let buttonString = "Send # \(self.amount)"
|
|
let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: .white, paragraphAlignment: .center)
|
|
if let range = buttonAttributedString.string.range(of: "#"), let starImage = self.cachedStarImage?.0 {
|
|
buttonAttributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: buttonAttributedString.string))
|
|
buttonAttributedString.addAttribute(.foregroundColor, value: UIColor(rgb: 0xffffff), range: NSRange(range, in: buttonAttributedString.string))
|
|
buttonAttributedString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: buttonAttributedString.string))
|
|
}
|
|
|
|
let actionButtonSize = actionButton.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.9),
|
|
cornerRadius: 10.0
|
|
),
|
|
content: AnyComponentWithIdentity(
|
|
id: AnyHashable(0),
|
|
component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString)))
|
|
),
|
|
isEnabled: true,
|
|
displaysProgress: false,
|
|
action: { [weak self] in
|
|
guard let self, let component = self.component else {
|
|
return
|
|
}
|
|
guard let balance = component.balance else {
|
|
return
|
|
}
|
|
|
|
if balance < self.amount {
|
|
let _ = (component.context.engine.payments.starsTopUpOptions()
|
|
|> take(1)
|
|
|> deliverOnMainQueue).startStandalone(next: { [weak self] options in
|
|
guard let self, let component = self.component else {
|
|
return
|
|
}
|
|
guard let starsContext = component.context.starsContext else {
|
|
return
|
|
}
|
|
|
|
let purchaseScreen = component.context.sharedContext.makeStarsPurchaseScreen(context: component.context, starsContext: starsContext, options: options, purpose: .transfer(peerId: component.peer.id, requiredStars: self.amount), completion: { result in
|
|
let _ = result
|
|
//TODO:release
|
|
})
|
|
self.environment?.controller()?.push(purchaseScreen)
|
|
self.environment?.controller()?.dismiss()
|
|
})
|
|
|
|
return
|
|
}
|
|
|
|
guard let badgeView = self.badge.view as? BadgeComponent.View else {
|
|
return
|
|
}
|
|
let isBecomingTop: Bool
|
|
if let topCount {
|
|
isBecomingTop = self.amount > topCount
|
|
} else {
|
|
isBecomingTop = true
|
|
}
|
|
|
|
component.completion(
|
|
self.amount,
|
|
isBecomingTop,
|
|
ChatSendStarsScreen.TransitionOut(
|
|
sourceView: badgeView.badgeIcon
|
|
)
|
|
)
|
|
self.environment?.controller()?.dismiss()
|
|
}
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0)
|
|
)
|
|
|
|
let buttonDescriptionTextSize = self.buttonDescriptionText.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(MultilineTextComponent(
|
|
text: .markdown(text: "By sending Stars you agree to the [Terms of Service]()", attributes: MarkdownAttributes(
|
|
body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemSecondaryTextColor),
|
|
bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: environment.theme.list.itemSecondaryTextColor),
|
|
link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemAccentColor),
|
|
linkAttribute: { url in
|
|
return ("URL", url)
|
|
}
|
|
)),
|
|
horizontalAlignment: .center,
|
|
maximumNumberOfLines: 0
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width - sideInset, height: 1000.0)
|
|
)
|
|
let buttonDescriptionSpacing: CGFloat = 14.0
|
|
|
|
let bottomPanelHeight = 13.0 + environment.safeInsets.bottom + actionButtonSize.height + buttonDescriptionSpacing + buttonDescriptionTextSize.height
|
|
let actionButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - bottomPanelHeight), size: actionButtonSize)
|
|
if let actionButtonView = actionButton.view {
|
|
if actionButtonView.superview == nil {
|
|
self.addSubview(actionButtonView)
|
|
}
|
|
transition.setFrame(view: actionButtonView, frame: actionButtonFrame)
|
|
}
|
|
|
|
let buttonDescriptionTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - buttonDescriptionTextSize.width) * 0.5), y: actionButtonFrame.maxY + buttonDescriptionSpacing), size: buttonDescriptionTextSize)
|
|
if let buttonDescriptionTextView = buttonDescriptionText.view {
|
|
if buttonDescriptionTextView.superview == nil {
|
|
self.addSubview(buttonDescriptionTextView)
|
|
}
|
|
transition.setFrame(view: buttonDescriptionTextView, frame: buttonDescriptionTextFrame)
|
|
}
|
|
|
|
contentHeight += bottomPanelHeight
|
|
initialContentHeight += bottomPanelHeight
|
|
|
|
clippingY = actionButtonFrame.minY - 24.0
|
|
|
|
let topInset: CGFloat = max(0.0, availableSize.height - containerInset - initialContentHeight)
|
|
|
|
let scrollContentHeight = max(topInset + contentHeight + containerInset, availableSize.height - containerInset)
|
|
|
|
self.scrollContentClippingView.layer.cornerRadius = 10.0
|
|
|
|
self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, bottomInset: environment.safeInsets.bottom, topInset: topInset)
|
|
|
|
transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + containerInset), size: CGSize(width: availableSize.width, height: contentHeight)))
|
|
|
|
transition.setPosition(layer: self.backgroundLayer, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0))
|
|
transition.setBounds(layer: self.backgroundLayer, bounds: CGRect(origin: CGPoint(), size: availableSize))
|
|
|
|
let scrollClippingFrame = CGRect(origin: CGPoint(x: 0.0, y: containerInset), size: CGSize(width: availableSize.width, height: clippingY - containerInset))
|
|
transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center)
|
|
transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size))
|
|
|
|
self.ignoreScrolling = true
|
|
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height)))
|
|
let contentSize = CGSize(width: availableSize.width, height: scrollContentHeight)
|
|
if contentSize != self.scrollView.contentSize {
|
|
self.scrollView.contentSize = contentSize
|
|
}
|
|
if resetScrolling {
|
|
self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: availableSize)
|
|
}
|
|
self.ignoreScrolling = false
|
|
self.updateScrolling(transition: transition)
|
|
|
|
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 ChatSendStarsScreen: ViewControllerComponentContainer {
|
|
public final class InitialData {
|
|
fileprivate let peer: EnginePeer
|
|
fileprivate let balance: Int64?
|
|
fileprivate let currentSentAmount: Int?
|
|
fileprivate let topPeers: [ChatSendStarsScreen.TopPeer]
|
|
|
|
fileprivate init(
|
|
peer: EnginePeer,
|
|
balance: Int64?,
|
|
currentSentAmount: Int?,
|
|
topPeers: [ChatSendStarsScreen.TopPeer]
|
|
) {
|
|
self.peer = peer
|
|
self.balance = balance
|
|
self.currentSentAmount = currentSentAmount
|
|
self.topPeers = topPeers
|
|
}
|
|
}
|
|
|
|
fileprivate final class TopPeer: Equatable {
|
|
struct Id: Hashable {
|
|
var value: EnginePeer.Id?
|
|
|
|
init(_ value: EnginePeer.Id?) {
|
|
self.value = value
|
|
}
|
|
}
|
|
|
|
var id: Id {
|
|
return Id(self.peer?.id)
|
|
}
|
|
|
|
let peer: EnginePeer?
|
|
let count: Int
|
|
|
|
init(peer: EnginePeer?, count: Int) {
|
|
self.peer = peer
|
|
self.count = count
|
|
}
|
|
|
|
static func ==(lhs: TopPeer, rhs: TopPeer) -> Bool {
|
|
if lhs.peer != rhs.peer {
|
|
return false
|
|
}
|
|
if lhs.count != rhs.count {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
public final class TransitionOut {
|
|
public let sourceView: UIView
|
|
|
|
init(sourceView: UIView) {
|
|
self.sourceView = sourceView
|
|
}
|
|
}
|
|
|
|
private let context: AccountContext
|
|
|
|
private var didPlayAppearAnimation: Bool = false
|
|
private var isDismissed: Bool = false
|
|
|
|
private var presenceDisposable: Disposable?
|
|
|
|
public init(context: AccountContext, initialData: InitialData, completion: @escaping (Int64, Bool, TransitionOut) -> Void) {
|
|
self.context = context
|
|
|
|
var maxAmount = 2500
|
|
if let data = context.currentAppConfiguration.with({ $0 }).data, let value = data["stars_paid_reaction_amount_max"] as? Double {
|
|
maxAmount = Int(value)
|
|
}
|
|
|
|
super.init(context: context, component: ChatSendStarsScreenComponent(
|
|
context: context,
|
|
peer: initialData.peer,
|
|
maxAmount: maxAmount,
|
|
balance: initialData.balance,
|
|
currentSentAmount: initialData.currentSentAmount,
|
|
topPeers: initialData.topPeers,
|
|
completion: completion
|
|
), 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 {
|
|
self.presenceDisposable?.dispose()
|
|
}
|
|
|
|
override public func viewDidAppear(_ animated: Bool) {
|
|
super.viewDidAppear(animated)
|
|
|
|
self.view.disablesInteractiveModalDismiss = true
|
|
|
|
if !self.didPlayAppearAnimation {
|
|
self.didPlayAppearAnimation = true
|
|
|
|
if let componentView = self.node.hostView.componentView as? ChatSendStarsScreenComponent.View {
|
|
componentView.animateIn()
|
|
}
|
|
}
|
|
}
|
|
|
|
public static func initialData(context: AccountContext, peerId: EnginePeer.Id, topPeers: [ReactionsMessageAttribute.TopPeer]) -> Signal<InitialData?, NoError> {
|
|
let balance: Signal<Int64?, NoError>
|
|
if let starsContext = context.starsContext {
|
|
balance = starsContext.state
|
|
|> map { state in
|
|
return state?.balance
|
|
}
|
|
|> take(1)
|
|
} else {
|
|
balance = .single(nil)
|
|
}
|
|
|
|
var currentSentAmount: Int?
|
|
if let myPeer = topPeers.first(where: { $0.isMy }) {
|
|
currentSentAmount = Int(myPeer.count)
|
|
}
|
|
|
|
var topPeers = topPeers.sorted(by: { $0.count > $1.count })
|
|
if topPeers.count > 3 {
|
|
topPeers = Array(topPeers.prefix(3))
|
|
}
|
|
|
|
return combineLatest(
|
|
context.engine.data.get(
|
|
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId),
|
|
EngineDataMap(topPeers.map(\.peerId).compactMap {
|
|
$0.flatMap(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))
|
|
})
|
|
),
|
|
balance
|
|
)
|
|
|> map { peerAndTopPeerMap, balance -> InitialData? in
|
|
let (peer, topPeerMap) = peerAndTopPeerMap
|
|
guard let peer else {
|
|
return nil
|
|
}
|
|
|
|
return InitialData(
|
|
peer: peer,
|
|
balance: balance,
|
|
currentSentAmount: currentSentAmount,
|
|
topPeers: topPeers.compactMap { topPeer -> ChatSendStarsScreen.TopPeer? in
|
|
guard let topPeerId = topPeer.peerId else {
|
|
return ChatSendStarsScreen.TopPeer(
|
|
peer: nil,
|
|
count: Int(topPeer.count)
|
|
)
|
|
}
|
|
guard let topPeerValue = topPeerMap[topPeerId] else {
|
|
return nil
|
|
}
|
|
guard let topPeerValue else {
|
|
return nil
|
|
}
|
|
return ChatSendStarsScreen.TopPeer(
|
|
peer: topPeerValue,
|
|
count: Int(topPeer.count)
|
|
)
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
override public func dismiss(completion: (() -> Void)? = nil) {
|
|
if !self.isDismissed {
|
|
self.isDismissed = true
|
|
|
|
if let componentView = self.node.hostView.componentView as? ChatSendStarsScreenComponent.View {
|
|
componentView.animateOut(completion: { [weak self] in
|
|
completion?()
|
|
self?.dismiss(animated: false)
|
|
})
|
|
} else {
|
|
self.dismiss(animated: false)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private 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()
|
|
})
|
|
}
|
|
|
|
private final class BadgeStarsView: UIView {
|
|
private let staticEmitterLayer = CAEmitterLayer()
|
|
private let dynamicEmitterLayer = CAEmitterLayer()
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
|
|
self.layer.addSublayer(self.staticEmitterLayer)
|
|
self.layer.addSublayer(self.dynamicEmitterLayer)
|
|
}
|
|
|
|
required init(coder: NSCoder) {
|
|
preconditionFailure()
|
|
}
|
|
|
|
private func setupEmitter() {
|
|
let color = UIColor(rgb: 0xffbe27)
|
|
|
|
self.staticEmitterLayer.emitterShape = .circle
|
|
self.staticEmitterLayer.emitterSize = CGSize(width: 10.0, height: 5.0)
|
|
self.staticEmitterLayer.emitterMode = .outline
|
|
self.layer.addSublayer(self.staticEmitterLayer)
|
|
|
|
self.dynamicEmitterLayer.birthRate = 0.0
|
|
self.dynamicEmitterLayer.emitterShape = .circle
|
|
self.dynamicEmitterLayer.emitterSize = CGSize(width: 10.0, height: 55.0)
|
|
self.dynamicEmitterLayer.emitterMode = .surface
|
|
self.layer.addSublayer(self.dynamicEmitterLayer)
|
|
|
|
let staticEmitter = CAEmitterCell()
|
|
staticEmitter.name = "emitter"
|
|
staticEmitter.contents = UIImage(bundleImageName: "Premium/Stars/Particle")?.cgImage
|
|
staticEmitter.birthRate = 20.0
|
|
staticEmitter.lifetime = 2.7
|
|
staticEmitter.velocity = 30.0
|
|
staticEmitter.velocityRange = 3
|
|
staticEmitter.scale = 0.15
|
|
staticEmitter.scaleRange = 0.08
|
|
staticEmitter.emissionRange = .pi * 2.0
|
|
staticEmitter.setValue(3.0, forKey: "mass")
|
|
staticEmitter.setValue(2.0, forKey: "massRange")
|
|
|
|
let dynamicEmitter = CAEmitterCell()
|
|
dynamicEmitter.name = "emitter"
|
|
dynamicEmitter.contents = UIImage(bundleImageName: "Premium/Stars/Particle")?.cgImage
|
|
dynamicEmitter.birthRate = 0.0
|
|
dynamicEmitter.lifetime = 2.7
|
|
dynamicEmitter.velocity = 30.0
|
|
dynamicEmitter.velocityRange = 3
|
|
dynamicEmitter.scale = 0.15
|
|
dynamicEmitter.scaleRange = 0.08
|
|
dynamicEmitter.emissionRange = .pi / 3.0
|
|
dynamicEmitter.setValue(3.0, forKey: "mass")
|
|
dynamicEmitter.setValue(2.0, forKey: "massRange")
|
|
|
|
let staticColors: [Any] = [
|
|
UIColor.white.withAlphaComponent(0.0).cgColor,
|
|
UIColor.white.withAlphaComponent(0.35).cgColor,
|
|
color.cgColor,
|
|
color.cgColor,
|
|
color.withAlphaComponent(0.0).cgColor
|
|
]
|
|
let staticColorBehavior = CAEmitterCell.createEmitterBehavior(type: "colorOverLife")
|
|
staticColorBehavior.setValue(staticColors, forKey: "colors")
|
|
staticEmitter.setValue([staticColorBehavior], forKey: "emitterBehaviors")
|
|
|
|
let dynamicColors: [Any] = [
|
|
UIColor.white.withAlphaComponent(0.35).cgColor,
|
|
color.withAlphaComponent(0.85).cgColor,
|
|
color.cgColor,
|
|
color.cgColor,
|
|
color.withAlphaComponent(0.0).cgColor
|
|
]
|
|
let dynamicColorBehavior = CAEmitterCell.createEmitterBehavior(type: "colorOverLife")
|
|
dynamicColorBehavior.setValue(dynamicColors, forKey: "colors")
|
|
dynamicEmitter.setValue([dynamicColorBehavior], forKey: "emitterBehaviors")
|
|
|
|
let attractor = CAEmitterCell.createEmitterBehavior(type: "simpleAttractor")
|
|
attractor.setValue("attractor", forKey: "name")
|
|
attractor.setValue(20, forKey: "falloff")
|
|
attractor.setValue(35, forKey: "radius")
|
|
self.staticEmitterLayer.setValue([attractor], forKey: "emitterBehaviors")
|
|
self.staticEmitterLayer.setValue(4.0, forKeyPath: "emitterBehaviors.attractor.stiffness")
|
|
self.staticEmitterLayer.setValue(false, forKeyPath: "emitterBehaviors.attractor.enabled")
|
|
|
|
self.staticEmitterLayer.emitterCells = [staticEmitter]
|
|
self.dynamicEmitterLayer.emitterCells = [dynamicEmitter]
|
|
}
|
|
|
|
func update(speed: Float, delta: Float? = nil) {
|
|
if speed > 0.0 {
|
|
if self.dynamicEmitterLayer.birthRate.isZero {
|
|
self.dynamicEmitterLayer.beginTime = CACurrentMediaTime()
|
|
}
|
|
|
|
self.dynamicEmitterLayer.setValue(Float(20.0 + speed * 1.4), forKeyPath: "emitterCells.emitter.birthRate")
|
|
self.dynamicEmitterLayer.setValue(2.7 - min(1.1, 1.5 * speed / 120.0), forKeyPath: "emitterCells.emitter.lifetime")
|
|
self.dynamicEmitterLayer.setValue(30.0 + CGFloat(speed / 80.0), forKeyPath: "emitterCells.emitter.velocity")
|
|
|
|
if let delta, speed > 15.0 {
|
|
self.dynamicEmitterLayer.setValue(delta > 0 ? .pi : 0, forKeyPath: "emitterCells.emitter.emissionLongitude")
|
|
self.dynamicEmitterLayer.setValue(.pi / 2.0, forKeyPath: "emitterCells.emitter.emissionRange")
|
|
} else {
|
|
self.dynamicEmitterLayer.setValue(0.0, forKeyPath: "emitterCells.emitter.emissionLongitude")
|
|
self.dynamicEmitterLayer.setValue(.pi * 2.0, forKeyPath: "emitterCells.emitter.emissionRange")
|
|
}
|
|
self.staticEmitterLayer.setValue(true, forKeyPath: "emitterBehaviors.attractor.enabled")
|
|
|
|
self.dynamicEmitterLayer.birthRate = 1.0
|
|
self.staticEmitterLayer.birthRate = 0.0
|
|
} else {
|
|
self.dynamicEmitterLayer.birthRate = 0.0
|
|
|
|
if let staticEmitter = self.staticEmitterLayer.emitterCells?.first {
|
|
staticEmitter.beginTime = CACurrentMediaTime()
|
|
}
|
|
self.staticEmitterLayer.birthRate = 1.0
|
|
self.staticEmitterLayer.setValue(false, forKeyPath: "emitterBehaviors.attractor.enabled")
|
|
}
|
|
}
|
|
|
|
func update(size: CGSize, emitterPosition: CGPoint) {
|
|
if self.staticEmitterLayer.emitterCells == nil {
|
|
self.setupEmitter()
|
|
}
|
|
|
|
self.staticEmitterLayer.frame = CGRect(origin: .zero, size: size)
|
|
self.staticEmitterLayer.emitterPosition = emitterPosition
|
|
|
|
self.dynamicEmitterLayer.frame = CGRect(origin: .zero, size: size)
|
|
self.dynamicEmitterLayer.emitterPosition = emitterPosition
|
|
self.staticEmitterLayer.setValue(emitterPosition, forKeyPath: "emitterBehaviors.attractor.position")
|
|
}
|
|
}
|
|
|
|
private final class SliderStarsView: UIView {
|
|
private let emitterLayer = CAEmitterLayer()
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
|
|
self.layer.addSublayer(self.emitterLayer)
|
|
}
|
|
|
|
required init(coder: NSCoder) {
|
|
preconditionFailure()
|
|
}
|
|
|
|
private func setupEmitter() {
|
|
self.emitterLayer.emitterShape = .rectangle
|
|
self.emitterLayer.emitterMode = .surface
|
|
self.layer.addSublayer(self.emitterLayer)
|
|
|
|
let emitter = CAEmitterCell()
|
|
emitter.name = "emitter"
|
|
emitter.contents = UIImage(bundleImageName: "Premium/Stars/Particle")?.cgImage
|
|
emitter.birthRate = 20.0
|
|
emitter.lifetime = 2.0
|
|
emitter.velocity = 15.0
|
|
emitter.velocityRange = 10
|
|
emitter.scale = 0.15
|
|
emitter.scaleRange = 0.08
|
|
emitter.emissionRange = .pi / 4.0
|
|
emitter.setValue(3.0, forKey: "mass")
|
|
emitter.setValue(2.0, forKey: "massRange")
|
|
self.emitterLayer.emitterCells = [emitter]
|
|
|
|
let colors: [Any] = [
|
|
UIColor.white.withAlphaComponent(0.0).cgColor,
|
|
UIColor.white.withAlphaComponent(0.38).cgColor,
|
|
UIColor.white.withAlphaComponent(0.38).cgColor,
|
|
UIColor.white.withAlphaComponent(0.0).cgColor,
|
|
UIColor.white.withAlphaComponent(0.38).cgColor,
|
|
UIColor.white.withAlphaComponent(0.38).cgColor,
|
|
UIColor.white.withAlphaComponent(0.0).cgColor
|
|
]
|
|
let colorBehavior = CAEmitterCell.createEmitterBehavior(type: "colorOverLife")
|
|
colorBehavior.setValue(colors, forKey: "colors")
|
|
emitter.setValue([colorBehavior], forKey: "emitterBehaviors")
|
|
}
|
|
|
|
func update(size: CGSize, value: CGFloat) {
|
|
if self.emitterLayer.emitterCells == nil {
|
|
self.setupEmitter()
|
|
}
|
|
|
|
self.emitterLayer.setValue(20.0 + Float(value * 40.0), forKeyPath: "emitterCells.emitter.birthRate")
|
|
self.emitterLayer.setValue(15.0 + value * 75.0, forKeyPath: "emitterCells.emitter.velocity")
|
|
|
|
self.emitterLayer.frame = CGRect(origin: .zero, size: size)
|
|
self.emitterLayer.emitterPosition = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
|
|
self.emitterLayer.emitterSize = size
|
|
}
|
|
}
|