mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Various improvements
This commit is contained in:
parent
8b417a3d9c
commit
5be0000735
@ -84,7 +84,7 @@ public func legacyICloudFilePicker(theme: PresentationTheme, mode: LegacyICloudF
|
|||||||
} else {
|
} else {
|
||||||
controller = DocumentPickerViewController(documentTypes: documentTypes, in: mode.documentPickerMode)
|
controller = DocumentPickerViewController(documentTypes: documentTypes, in: mode.documentPickerMode)
|
||||||
}
|
}
|
||||||
controller.forceDarkTheme = forceDarkTheme
|
controller.forceDarkTheme = forceDarkTheme || theme.overallDarkAppearance
|
||||||
controller.didDisappear = {
|
controller.didDisappear = {
|
||||||
dismissImpl?()
|
dismissImpl?()
|
||||||
}
|
}
|
||||||
|
148
submodules/PremiumUI/Sources/BadgeLabelView.swift
Normal file
148
submodules/PremiumUI/Sources/BadgeLabelView.swift
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import ComponentFlow
|
||||||
|
|
||||||
|
private let labelWidth: CGFloat = 16.0
|
||||||
|
private let labelHeight: CGFloat = 36.0
|
||||||
|
private let labelSize = CGSize(width: labelWidth, height: labelHeight)
|
||||||
|
private let font = Font.with(size: 24.0, design: .round, weight: .semibold, traits: [])
|
||||||
|
|
||||||
|
final class BadgeLabelView: UIView {
|
||||||
|
private class StackView: UIView {
|
||||||
|
var labels: [UILabel] = []
|
||||||
|
|
||||||
|
var currentValue: Int32 = 0
|
||||||
|
|
||||||
|
init() {
|
||||||
|
super.init(frame: CGRect(origin: .zero, size: labelSize))
|
||||||
|
|
||||||
|
var height: CGFloat = -labelHeight
|
||||||
|
for i in -1 ..< 10 {
|
||||||
|
let label = UILabel()
|
||||||
|
if i == -1 {
|
||||||
|
label.text = "9"
|
||||||
|
} else {
|
||||||
|
label.text = "\(i)"
|
||||||
|
}
|
||||||
|
label.textColor = .white
|
||||||
|
label.font = font
|
||||||
|
label.textAlignment = .center
|
||||||
|
label.frame = CGRect(x: 0, y: height, width: labelWidth, height: labelHeight)
|
||||||
|
self.addSubview(label)
|
||||||
|
self.labels.append(label)
|
||||||
|
|
||||||
|
height += labelHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(value: Int32, isFirst: Bool, isLast: Bool, transition: Transition) {
|
||||||
|
let previousValue = self.currentValue
|
||||||
|
self.currentValue = value
|
||||||
|
|
||||||
|
self.labels[1].alpha = isFirst && !isLast ? 0.0 : 1.0
|
||||||
|
|
||||||
|
if previousValue == 9 && value < 9 {
|
||||||
|
self.bounds = CGRect(
|
||||||
|
origin: CGPoint(
|
||||||
|
x: 0.0,
|
||||||
|
y: -1.0 * labelSize.height
|
||||||
|
),
|
||||||
|
size: labelSize
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let bounds = CGRect(
|
||||||
|
origin: CGPoint(
|
||||||
|
x: 0.0,
|
||||||
|
y: CGFloat(value) * labelSize.height
|
||||||
|
),
|
||||||
|
size: labelSize
|
||||||
|
)
|
||||||
|
transition.setBounds(view: self, bounds: bounds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var itemViews: [Int: StackView] = [:]
|
||||||
|
private var staticLabel = UILabel()
|
||||||
|
|
||||||
|
init() {
|
||||||
|
super.init(frame: .zero)
|
||||||
|
|
||||||
|
self.clipsToBounds = true
|
||||||
|
self.isUserInteractionEnabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(value: String, transition: Transition) -> CGSize {
|
||||||
|
if value.contains(" ") {
|
||||||
|
for (_, view) in self.itemViews {
|
||||||
|
view.isHidden = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.staticLabel.superview == nil {
|
||||||
|
self.staticLabel.textColor = .white
|
||||||
|
self.staticLabel.font = font
|
||||||
|
|
||||||
|
self.addSubview(self.staticLabel)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.staticLabel.text = value
|
||||||
|
let size = self.staticLabel.sizeThatFits(CGSize(width: 100.0, height: 100.0))
|
||||||
|
self.staticLabel.frame = CGRect(origin: .zero, size: CGSize(width: size.width, height: labelHeight))
|
||||||
|
|
||||||
|
return CGSize(width: ceil(self.staticLabel.bounds.width), height: ceil(self.staticLabel.bounds.height))
|
||||||
|
}
|
||||||
|
|
||||||
|
let string = value
|
||||||
|
let stringArray = Array(string.map { String($0) }.reversed())
|
||||||
|
|
||||||
|
let totalWidth = CGFloat(stringArray.count) * labelWidth
|
||||||
|
|
||||||
|
var validIds: [Int] = []
|
||||||
|
for i in 0 ..< stringArray.count {
|
||||||
|
validIds.append(i)
|
||||||
|
|
||||||
|
let itemView: StackView
|
||||||
|
var itemTransition = transition
|
||||||
|
if let current = self.itemViews[i] {
|
||||||
|
itemView = current
|
||||||
|
} else {
|
||||||
|
itemTransition = transition.withAnimation(.none)
|
||||||
|
itemView = StackView()
|
||||||
|
self.itemViews[i] = itemView
|
||||||
|
self.addSubview(itemView)
|
||||||
|
}
|
||||||
|
|
||||||
|
let digit = Int32(stringArray[i]) ?? 0
|
||||||
|
itemView.update(value: digit, isFirst: i == stringArray.count - 1, isLast: i == 0, transition: transition)
|
||||||
|
|
||||||
|
itemTransition.setFrame(
|
||||||
|
view: itemView,
|
||||||
|
frame: CGRect(x: totalWidth - labelWidth * CGFloat(i + 1), y: 0.0, width: labelWidth, height: labelHeight)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var removeIds: [Int] = []
|
||||||
|
for (id, itemView) in self.itemViews {
|
||||||
|
if !validIds.contains(id) {
|
||||||
|
removeIds.append(id)
|
||||||
|
|
||||||
|
transition.setAlpha(view: itemView, alpha: 0.0, completion: { _ in
|
||||||
|
itemView.removeFromSuperview()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for id in removeIds {
|
||||||
|
self.itemViews.removeValue(forKey: id)
|
||||||
|
}
|
||||||
|
return CGSize(width: totalWidth, height: labelHeight)
|
||||||
|
}
|
||||||
|
}
|
@ -39,13 +39,17 @@ func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private func generateBadgePath(rectSize: CGSize, tailPosition: CGFloat = 0.5) -> UIBezierPath {
|
private func generateBadgePath(rectSize: CGSize, tailPosition: CGFloat? = 0.5) -> UIBezierPath {
|
||||||
let cornerRadius: CGFloat = rectSize.height / 2.0
|
let cornerRadius: CGFloat = rectSize.height / 2.0
|
||||||
let tailWidth: CGFloat = 20.0
|
let tailWidth: CGFloat = 20.0
|
||||||
let tailHeight: CGFloat = 9.0
|
let tailHeight: CGFloat = 9.0
|
||||||
let tailRadius: CGFloat = 4.0
|
let tailRadius: CGFloat = 4.0
|
||||||
|
|
||||||
let rect = CGRect(origin: CGPoint(x: 0.0, y: tailHeight), size: rectSize)
|
let rect = CGRect(origin: CGPoint(x: 0.0, y: tailHeight), size: rectSize)
|
||||||
|
|
||||||
|
guard let tailPosition else {
|
||||||
|
return UIBezierPath(cgPath: CGPath(roundedRect: rect, cornerWidth: cornerRadius, cornerHeight: cornerRadius, transform: nil))
|
||||||
|
}
|
||||||
|
|
||||||
let path = UIBezierPath()
|
let path = UIBezierPath()
|
||||||
|
|
||||||
@ -278,8 +282,8 @@ public class PremiumLimitDisplayComponent: Component {
|
|||||||
|
|
||||||
private let badgeForeground: SimpleLayer
|
private let badgeForeground: SimpleLayer
|
||||||
private let badgeIcon: UIImageView
|
private let badgeIcon: UIImageView
|
||||||
private let badgeCountLabel: RollingLabel
|
private let badgeLabel: BadgeLabelView
|
||||||
private let countMaskView = UIImageView()
|
private let badgeLabelMaskView = UIImageView()
|
||||||
|
|
||||||
private let hapticFeedback = HapticFeedback()
|
private let hapticFeedback = HapticFeedback()
|
||||||
|
|
||||||
@ -311,11 +315,9 @@ public class PremiumLimitDisplayComponent: Component {
|
|||||||
self.badgeIcon = UIImageView()
|
self.badgeIcon = UIImageView()
|
||||||
self.badgeIcon.contentMode = .center
|
self.badgeIcon.contentMode = .center
|
||||||
|
|
||||||
self.badgeCountLabel = RollingLabel()
|
self.badgeLabel = BadgeLabelView()
|
||||||
self.badgeCountLabel.font = Font.with(size: 24.0, design: .round, weight: .semibold, traits: [])
|
let _ = self.badgeLabel.update(value: "0", transition: .immediate)
|
||||||
self.badgeCountLabel.textColor = .white
|
self.badgeLabel.mask = self.badgeLabelMaskView
|
||||||
self.badgeCountLabel.configure(with: "0")
|
|
||||||
self.badgeCountLabel.mask = self.countMaskView
|
|
||||||
|
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
|
||||||
@ -327,10 +329,10 @@ public class PremiumLimitDisplayComponent: Component {
|
|||||||
self.addSubview(self.badgeView)
|
self.addSubview(self.badgeView)
|
||||||
self.badgeView.layer.addSublayer(self.badgeForeground)
|
self.badgeView.layer.addSublayer(self.badgeForeground)
|
||||||
self.badgeView.addSubview(self.badgeIcon)
|
self.badgeView.addSubview(self.badgeIcon)
|
||||||
self.badgeView.addSubview(self.badgeCountLabel)
|
self.badgeView.addSubview(self.badgeLabel)
|
||||||
|
|
||||||
self.countMaskView.contentMode = .scaleToFill
|
self.badgeLabelMaskView.contentMode = .scaleToFill
|
||||||
self.countMaskView.image = generateImage(CGSize(width: 2.0, height: 48.0), rotatedContext: { size, context in
|
self.badgeLabelMaskView.image = generateImage(CGSize(width: 2.0, height: 36.0), rotatedContext: { size, context in
|
||||||
let bounds = CGRect(origin: .zero, size: size)
|
let bounds = CGRect(origin: .zero, size: size)
|
||||||
context.clear(bounds)
|
context.clear(bounds)
|
||||||
|
|
||||||
@ -339,9 +341,8 @@ public class PremiumLimitDisplayComponent: Component {
|
|||||||
UIColor(rgb: 0xffffff).cgColor,
|
UIColor(rgb: 0xffffff).cgColor,
|
||||||
UIColor(rgb: 0xffffff).cgColor,
|
UIColor(rgb: 0xffffff).cgColor,
|
||||||
UIColor(rgb: 0xffffff, alpha: 0.0).cgColor,
|
UIColor(rgb: 0xffffff, alpha: 0.0).cgColor,
|
||||||
UIColor(rgb: 0xffffff, alpha: 0.0).cgColor
|
|
||||||
]
|
]
|
||||||
var locations: [CGFloat] = [0.0, 0.11, 0.46, 0.57, 1.0]
|
var locations: [CGFloat] = [0.0, 0.24, 0.76, 1.0]
|
||||||
let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray as CFArray, locations: &locations)!
|
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())
|
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
|
||||||
@ -355,11 +356,18 @@ public class PremiumLimitDisplayComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var didPlayAppearanceAnimation = false
|
private var didPlayAppearanceAnimation = false
|
||||||
func playAppearanceAnimation(component: PremiumLimitDisplayComponent, availableSize: CGSize, from: CGFloat? = nil) {
|
func playAppearanceAnimation(component: PremiumLimitDisplayComponent, badgeFullSize: CGSize, from: CGFloat? = nil) {
|
||||||
if from == nil {
|
if from == nil {
|
||||||
self.badgeView.layer.animateScale(from: 0.1, to: 1.0, duration: 0.4, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
|
self.badgeView.layer.animateScale(from: 0.1, to: 1.0, duration: 0.4, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let rotationAngle: CGFloat
|
||||||
|
if badgeFullSize.width > 100.0 {
|
||||||
|
rotationAngle = 0.2
|
||||||
|
} else {
|
||||||
|
rotationAngle = 0.26
|
||||||
|
}
|
||||||
|
|
||||||
let positionAnimation = CABasicAnimation(keyPath: "position.x")
|
let positionAnimation = CABasicAnimation(keyPath: "position.x")
|
||||||
positionAnimation.fromValue = NSValue(cgPoint: CGPoint(x: from ?? 0.0, y: 0.0))
|
positionAnimation.fromValue = NSValue(cgPoint: CGPoint(x: from ?? 0.0, y: 0.0))
|
||||||
positionAnimation.toValue = NSValue(cgPoint: self.badgeView.center)
|
positionAnimation.toValue = NSValue(cgPoint: self.badgeView.center)
|
||||||
@ -370,7 +378,7 @@ public class PremiumLimitDisplayComponent: Component {
|
|||||||
|
|
||||||
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
|
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
|
||||||
rotateAnimation.fromValue = 0.0 as NSNumber
|
rotateAnimation.fromValue = 0.0 as NSNumber
|
||||||
rotateAnimation.toValue = -0.26 as NSNumber
|
rotateAnimation.toValue = -rotationAngle as NSNumber
|
||||||
rotateAnimation.duration = 0.15
|
rotateAnimation.duration = 0.15
|
||||||
rotateAnimation.fillMode = .forwards
|
rotateAnimation.fillMode = .forwards
|
||||||
rotateAnimation.timingFunction = CAMediaTimingFunction(name: .easeOut)
|
rotateAnimation.timingFunction = CAMediaTimingFunction(name: .easeOut)
|
||||||
@ -379,7 +387,7 @@ public class PremiumLimitDisplayComponent: Component {
|
|||||||
|
|
||||||
Queue.mainQueue().after(0.5, {
|
Queue.mainQueue().after(0.5, {
|
||||||
let bounceAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
|
let bounceAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
|
||||||
bounceAnimation.fromValue = -0.26 as NSNumber
|
bounceAnimation.fromValue = -rotationAngle as NSNumber
|
||||||
bounceAnimation.toValue = 0.04 as NSNumber
|
bounceAnimation.toValue = 0.04 as NSNumber
|
||||||
bounceAnimation.duration = 0.2
|
bounceAnimation.duration = 0.2
|
||||||
bounceAnimation.fillMode = .forwards
|
bounceAnimation.fillMode = .forwards
|
||||||
@ -410,7 +418,13 @@ public class PremiumLimitDisplayComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let badgeText = component.badgeText {
|
if let badgeText = component.badgeText {
|
||||||
self.badgeCountLabel.configure(with: badgeText, increment: from != nil, duration: from != nil ? 0.3 : 0.5)
|
let transition: Transition = .easeInOut(duration: from != nil ? 0.3 : 0.5)
|
||||||
|
var frameTransition = transition
|
||||||
|
if from == nil {
|
||||||
|
frameTransition = frameTransition.withAnimation(.none)
|
||||||
|
}
|
||||||
|
let badgeLabelSize = self.badgeLabel.update(value: badgeText, transition: transition)
|
||||||
|
frameTransition.setFrame(view: self.badgeLabel, frame: CGRect(origin: CGPoint(x: 14.0 + floorToScreenPixels((badgeFullSize.width - badgeLabelSize.width) / 2.0), y: 5.0), size: badgeLabelSize))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -643,9 +657,7 @@ public class PremiumLimitDisplayComponent: Component {
|
|||||||
|
|
||||||
if badgePosition > 1.0 - 0.15 {
|
if badgePosition > 1.0 - 0.15 {
|
||||||
progressTransition.setAnchorPoint(layer: self.badgeView.layer, anchorPoint: CGPoint(x: 1.0, y: 1.0))
|
progressTransition.setAnchorPoint(layer: self.badgeView.layer, anchorPoint: CGPoint(x: 1.0, y: 1.0))
|
||||||
progressTransition.setShapeLayerPath(layer: self.badgeShapeLayer, path: generateBadgePath(rectSize: badgeSize, tailPosition: 1.0).cgPath)
|
progressTransition.setShapeLayerPath(layer: self.badgeShapeLayer, path: generateBadgePath(rectSize: badgeSize, tailPosition: component.isPremiumDisabled ? nil : 1.0).cgPath)
|
||||||
|
|
||||||
// self.badgeMaskArrowView.isHidden = component.isPremiumDisabled
|
|
||||||
|
|
||||||
if let _ = self.badgeView.layer.animation(forKey: "appearance1") {
|
if let _ = self.badgeView.layer.animation(forKey: "appearance1") {
|
||||||
|
|
||||||
@ -654,10 +666,8 @@ public class PremiumLimitDisplayComponent: Component {
|
|||||||
}
|
}
|
||||||
} else if badgePosition < 0.15 {
|
} else if badgePosition < 0.15 {
|
||||||
progressTransition.setAnchorPoint(layer: self.badgeView.layer, anchorPoint: CGPoint(x: 0.0, y: 1.0))
|
progressTransition.setAnchorPoint(layer: self.badgeView.layer, anchorPoint: CGPoint(x: 0.0, y: 1.0))
|
||||||
progressTransition.setShapeLayerPath(layer: self.badgeShapeLayer, path: generateBadgePath(rectSize: badgeSize, tailPosition: 0.0).cgPath)
|
progressTransition.setShapeLayerPath(layer: self.badgeShapeLayer, path: generateBadgePath(rectSize: badgeSize, tailPosition: component.isPremiumDisabled ? nil : 0.0).cgPath)
|
||||||
|
|
||||||
// self.badgeMaskArrowView.isHidden = component.isPremiumDisabled
|
|
||||||
|
|
||||||
if let _ = self.badgeView.layer.animation(forKey: "appearance1") {
|
if let _ = self.badgeView.layer.animation(forKey: "appearance1") {
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
@ -665,10 +675,8 @@ public class PremiumLimitDisplayComponent: Component {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
progressTransition.setAnchorPoint(layer: self.badgeView.layer, anchorPoint: CGPoint(x: 0.5, y: 1.0))
|
progressTransition.setAnchorPoint(layer: self.badgeView.layer, anchorPoint: CGPoint(x: 0.5, y: 1.0))
|
||||||
progressTransition.setShapeLayerPath(layer: self.badgeShapeLayer, path: generateBadgePath(rectSize: badgeSize, tailPosition: 0.5).cgPath)
|
progressTransition.setShapeLayerPath(layer: self.badgeShapeLayer, path: generateBadgePath(rectSize: badgeSize, tailPosition: component.isPremiumDisabled ? nil : 0.5).cgPath)
|
||||||
|
|
||||||
// self.badgeMaskArrowView.isHidden = component.isPremiumDisabled
|
|
||||||
|
|
||||||
if let _ = self.badgeView.layer.animation(forKey: "appearance1") {
|
if let _ = self.badgeView.layer.animation(forKey: "appearance1") {
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
@ -681,8 +689,7 @@ public class PremiumLimitDisplayComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.badgeIcon.frame = CGRect(x: 10.0, y: 9.0, width: 30.0, height: 30.0)
|
self.badgeIcon.frame = CGRect(x: 10.0, y: 9.0, width: 30.0, height: 30.0)
|
||||||
self.badgeCountLabel.frame = CGRect(x: badgeFullSize.width - countWidth - 11.0, y: 10.0, width: countWidth, height: 48.0)
|
self.badgeLabelMaskView.frame = CGRect(x: 0.0, y: 0.0, width: 100.0, height: 36.0)
|
||||||
self.countMaskView.frame = CGRect(x: 0.0, y: 0.0, width: countWidth, height: 48.0)
|
|
||||||
|
|
||||||
if component.isPremiumDisabled {
|
if component.isPremiumDisabled {
|
||||||
if !self.didPlayAppearanceAnimation {
|
if !self.didPlayAppearanceAnimation {
|
||||||
@ -690,7 +697,8 @@ public class PremiumLimitDisplayComponent: Component {
|
|||||||
|
|
||||||
self.badgeView.alpha = 1.0
|
self.badgeView.alpha = 1.0
|
||||||
if let badgeText = component.badgeText {
|
if let badgeText = component.badgeText {
|
||||||
self.badgeCountLabel.configure(with: badgeText, duration: 0.3)
|
let badgeLabelSize = self.badgeLabel.update(value: badgeText, transition: .immediate)
|
||||||
|
transition.setFrame(view: self.badgeLabel, frame: CGRect(origin: CGPoint(x: 14.0 + floorToScreenPixels((badgeFullSize.width - badgeLabelSize.width) / 2.0), y: 5.0), size: badgeLabelSize))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if !self.didPlayAppearanceAnimation || !transition.animation.isImmediate {
|
} else if !self.didPlayAppearanceAnimation || !transition.animation.isImmediate {
|
||||||
@ -699,13 +707,14 @@ public class PremiumLimitDisplayComponent: Component {
|
|||||||
if component.badgePosition < 0.1 {
|
if component.badgePosition < 0.1 {
|
||||||
self.badgeView.alpha = 1.0
|
self.badgeView.alpha = 1.0
|
||||||
if let badgeText = component.badgeText {
|
if let badgeText = component.badgeText {
|
||||||
self.badgeCountLabel.configure(with: badgeText, duration: 0.001)
|
let badgeLabelSize = self.badgeLabel.update(value: badgeText, transition: .immediate)
|
||||||
|
transition.setFrame(view: self.badgeLabel, frame: CGRect(origin: CGPoint(x: 14.0 + floorToScreenPixels((badgeFullSize.width - badgeLabelSize.width) / 2.0), y: 5.0), size: badgeLabelSize))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.playAppearanceAnimation(component: component, availableSize: size)
|
self.playAppearanceAnimation(component: component, badgeFullSize: badgeFullSize)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.playAppearanceAnimation(component: component, availableSize: size, from: currentBadgeX)
|
self.playAppearanceAnimation(component: component, badgeFullSize: badgeFullSize, from: currentBadgeX)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -800,8 +809,9 @@ private final class LimitSheetContent: CombinedComponent {
|
|||||||
let action: () -> Bool
|
let action: () -> Bool
|
||||||
let dismiss: () -> Void
|
let dismiss: () -> Void
|
||||||
let openPeer: (EnginePeer) -> Void
|
let openPeer: (EnginePeer) -> Void
|
||||||
|
let openStats: (() -> Void)?
|
||||||
|
|
||||||
init(context: AccountContext, subject: PremiumLimitScreen.Subject, count: Int32, cancel: @escaping () -> Void, action: @escaping () -> Bool, dismiss: @escaping () -> Void, openPeer: @escaping (EnginePeer) -> Void) {
|
init(context: AccountContext, subject: PremiumLimitScreen.Subject, count: Int32, cancel: @escaping () -> Void, action: @escaping () -> Bool, dismiss: @escaping () -> Void, openPeer: @escaping (EnginePeer) -> Void, openStats: (() -> Void)?) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.subject = subject
|
self.subject = subject
|
||||||
self.count = count
|
self.count = count
|
||||||
@ -809,6 +819,7 @@ private final class LimitSheetContent: CombinedComponent {
|
|||||||
self.action = action
|
self.action = action
|
||||||
self.dismiss = dismiss
|
self.dismiss = dismiss
|
||||||
self.openPeer = openPeer
|
self.openPeer = openPeer
|
||||||
|
self.openStats = openStats
|
||||||
}
|
}
|
||||||
|
|
||||||
static func ==(lhs: LimitSheetContent, rhs: LimitSheetContent) -> Bool {
|
static func ==(lhs: LimitSheetContent, rhs: LimitSheetContent) -> Bool {
|
||||||
@ -878,6 +889,7 @@ private final class LimitSheetContent: CombinedComponent {
|
|||||||
let linkButton = Child(SolidRoundedButtonComponent.self)
|
let linkButton = Child(SolidRoundedButtonComponent.self)
|
||||||
let button = Child(SolidRoundedButtonComponent.self)
|
let button = Child(SolidRoundedButtonComponent.self)
|
||||||
let peerShortcut = Child(Button.self)
|
let peerShortcut = Child(Button.self)
|
||||||
|
let statsButton = Child(Button.self)
|
||||||
|
|
||||||
return { context in
|
return { context in
|
||||||
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
|
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
|
||||||
@ -1072,7 +1084,7 @@ private final class LimitSheetContent: CombinedComponent {
|
|||||||
defaultValue = component.count == 4 ? dataSizeString(limit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator)) : ""
|
defaultValue = component.count == 4 ? dataSizeString(limit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator)) : ""
|
||||||
premiumValue = component.count != 4 ? dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator)) : ""
|
premiumValue = component.count != 4 ? dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator)) : ""
|
||||||
badgePosition = component.count == 4 ? 1.0 : 0.5
|
badgePosition = component.count == 4 ? 1.0 : 0.5
|
||||||
badgeGraphPosition = badgePosition
|
badgeGraphPosition = 0.5
|
||||||
titleText = strings.Premium_FileTooLarge
|
titleText = strings.Premium_FileTooLarge
|
||||||
|
|
||||||
if isPremiumDisabled {
|
if isPremiumDisabled {
|
||||||
@ -1165,9 +1177,34 @@ private final class LimitSheetContent: CombinedComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
availableSize: CGSize(width: context.availableSize.width - 32.0, height: context.availableSize.height),
|
||||||
|
transition: .immediate
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let _ = link, let openStats = component.openStats {
|
||||||
|
let _ = openStats
|
||||||
|
let statsButton = statsButton.update(
|
||||||
|
component: Button(
|
||||||
|
content: AnyComponent(
|
||||||
|
BundleIconComponent(
|
||||||
|
name: "Premium/Stats",
|
||||||
|
tintColor: environment.theme.list.itemAccentColor
|
||||||
|
)
|
||||||
|
),
|
||||||
|
action: {
|
||||||
|
component.dismiss()
|
||||||
|
Queue.mainQueue().after(0.35) {
|
||||||
|
openStats()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
).minSize(CGSize(width: 44.0, height: 44.0)),
|
||||||
availableSize: context.availableSize,
|
availableSize: context.availableSize,
|
||||||
transition: .immediate
|
transition: .immediate
|
||||||
)
|
)
|
||||||
|
context.add(statsButton
|
||||||
|
.position(CGPoint(x: 31.0, y: 28.0))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if boosted && state.boosted != boosted {
|
if boosted && state.boosted != boosted {
|
||||||
@ -1528,14 +1565,16 @@ private final class LimitSheetComponent: CombinedComponent {
|
|||||||
let cancel: () -> Void
|
let cancel: () -> Void
|
||||||
let action: () -> Bool
|
let action: () -> Bool
|
||||||
let openPeer: (EnginePeer) -> Void
|
let openPeer: (EnginePeer) -> Void
|
||||||
|
let openStats: (() -> Void)?
|
||||||
|
|
||||||
init(context: AccountContext, subject: PremiumLimitScreen.Subject, count: Int32, cancel: @escaping () -> Void, action: @escaping () -> Bool, openPeer: @escaping (EnginePeer) -> Void) {
|
init(context: AccountContext, subject: PremiumLimitScreen.Subject, count: Int32, cancel: @escaping () -> Void, action: @escaping () -> Bool, openPeer: @escaping (EnginePeer) -> Void, openStats: (() -> Void)?) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.subject = subject
|
self.subject = subject
|
||||||
self.count = count
|
self.count = count
|
||||||
self.cancel = cancel
|
self.cancel = cancel
|
||||||
self.action = action
|
self.action = action
|
||||||
self.openPeer = openPeer
|
self.openPeer = openPeer
|
||||||
|
self.openStats = openStats
|
||||||
}
|
}
|
||||||
|
|
||||||
static func ==(lhs: LimitSheetComponent, rhs: LimitSheetComponent) -> Bool {
|
static func ==(lhs: LimitSheetComponent, rhs: LimitSheetComponent) -> Bool {
|
||||||
@ -1545,6 +1584,9 @@ private final class LimitSheetComponent: CombinedComponent {
|
|||||||
if lhs.subject != rhs.subject {
|
if lhs.subject != rhs.subject {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.count != rhs.count {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -1573,7 +1615,8 @@ private final class LimitSheetComponent: CombinedComponent {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
openPeer: context.component.openPeer
|
openPeer: context.component.openPeer,
|
||||||
|
openStats: context.component.openStats
|
||||||
)),
|
)),
|
||||||
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
|
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
|
||||||
animateOut: animateOut
|
animateOut: animateOut
|
||||||
@ -1636,14 +1679,14 @@ public class PremiumLimitScreen: ViewControllerComponentContainer {
|
|||||||
|
|
||||||
private let hapticFeedback = HapticFeedback()
|
private let hapticFeedback = HapticFeedback()
|
||||||
|
|
||||||
public init(context: AccountContext, subject: PremiumLimitScreen.Subject, count: Int32, forceDark: Bool = false, cancel: @escaping () -> Void = {}, action: @escaping () -> Bool, openPeer: @escaping (EnginePeer) -> Void = { _ in }) {
|
public init(context: AccountContext, subject: PremiumLimitScreen.Subject, count: Int32, forceDark: Bool = false, cancel: @escaping () -> Void = {}, action: @escaping () -> Bool, openPeer: @escaping (EnginePeer) -> Void = { _ in }, openStats: (() -> Void)? = nil) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.openPeer = openPeer
|
self.openPeer = openPeer
|
||||||
|
|
||||||
var actionImpl: (() -> Bool)?
|
var actionImpl: (() -> Bool)?
|
||||||
super.init(context: context, component: LimitSheetComponent(context: context, subject: subject, count: count, cancel: {}, action: {
|
super.init(context: context, component: LimitSheetComponent(context: context, subject: subject, count: count, cancel: {}, action: {
|
||||||
return actionImpl?() ?? true
|
return actionImpl?() ?? true
|
||||||
}, openPeer: openPeer), navigationBarAppearance: .none, statusBarStyle: .ignore, theme: forceDark ? .dark : .default)
|
}, openPeer: openPeer, openStats: openStats), navigationBarAppearance: .none, statusBarStyle: .ignore, theme: forceDark ? .dark : .default)
|
||||||
|
|
||||||
self.navigationPresentation = .flatModal
|
self.navigationPresentation = .flatModal
|
||||||
|
|
||||||
@ -1673,7 +1716,7 @@ public class PremiumLimitScreen: ViewControllerComponentContainer {
|
|||||||
public func updateSubject(_ subject: Subject, count: Int32) {
|
public func updateSubject(_ subject: Subject, count: Int32) {
|
||||||
let component = LimitSheetComponent(context: self.context, subject: subject, count: count, cancel: {}, action: {
|
let component = LimitSheetComponent(context: self.context, subject: subject, count: count, cancel: {}, action: {
|
||||||
return true
|
return true
|
||||||
}, openPeer: self.openPeer)
|
}, openPeer: self.openPeer, openStats: nil)
|
||||||
self.updateComponent(component: AnyComponent(component), transition: .easeInOut(duration: 0.2))
|
self.updateComponent(component: AnyComponent(component), transition: .easeInOut(duration: 0.2))
|
||||||
|
|
||||||
self.hapticFeedback.impact()
|
self.hapticFeedback.impact()
|
||||||
@ -1750,7 +1793,7 @@ private final class PeerShortcutComponent: Component {
|
|||||||
)
|
)
|
||||||
),
|
),
|
||||||
environment: {},
|
environment: {},
|
||||||
containerSize: availableSize
|
containerSize: CGSize(width: availableSize.width - 50.0, height: availableSize.height)
|
||||||
)
|
)
|
||||||
|
|
||||||
let size = CGSize(width: 30.0 + textSize.width + 20.0, height: 32.0)
|
let size = CGSize(width: 30.0 + textSize.width + 20.0, height: 32.0)
|
||||||
|
@ -1,591 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import UIKit
|
|
||||||
import Display
|
|
||||||
import SwiftSignalKit
|
|
||||||
import AsyncDisplayKit
|
|
||||||
import ComponentFlow
|
|
||||||
import TelegramCore
|
|
||||||
import AccountContext
|
|
||||||
import ReactionSelectionNode
|
|
||||||
import TelegramPresentationData
|
|
||||||
import AccountContext
|
|
||||||
import AnimationCache
|
|
||||||
import Postbox
|
|
||||||
import MultiAnimationRenderer
|
|
||||||
|
|
||||||
final class ReactionsCarouselComponent: Component {
|
|
||||||
public typealias EnvironmentType = DemoPageEnvironment
|
|
||||||
|
|
||||||
let context: AccountContext
|
|
||||||
let theme: PresentationTheme
|
|
||||||
let reactions: [AvailableReactions.Reaction]
|
|
||||||
|
|
||||||
public init(
|
|
||||||
context: AccountContext,
|
|
||||||
theme: PresentationTheme,
|
|
||||||
reactions: [AvailableReactions.Reaction]
|
|
||||||
) {
|
|
||||||
self.context = context
|
|
||||||
self.theme = theme
|
|
||||||
self.reactions = reactions
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func ==(lhs: ReactionsCarouselComponent, rhs: ReactionsCarouselComponent) -> Bool {
|
|
||||||
if lhs.context !== rhs.context {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if lhs.theme !== rhs.theme {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if lhs.reactions != rhs.reactions {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
public final class View: UIView {
|
|
||||||
private var component: ReactionsCarouselComponent?
|
|
||||||
private var node: ReactionCarouselNode?
|
|
||||||
|
|
||||||
private var isVisible = false
|
|
||||||
|
|
||||||
public func update(component: ReactionsCarouselComponent, availableSize: CGSize, environment: Environment<DemoPageEnvironment>, transition: Transition) -> CGSize {
|
|
||||||
let isDisplaying = environment[DemoPageEnvironment.self].isDisplaying
|
|
||||||
|
|
||||||
if self.node == nil && !component.reactions.isEmpty {
|
|
||||||
let node = ReactionCarouselNode(
|
|
||||||
context: component.context,
|
|
||||||
theme: component.theme,
|
|
||||||
reactions: component.reactions
|
|
||||||
)
|
|
||||||
self.node = node
|
|
||||||
self.addSubnode(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.component = component
|
|
||||||
|
|
||||||
if let node = self.node {
|
|
||||||
node.frame = CGRect(origin: CGPoint(x: 0.0, y: -20.0), size: availableSize)
|
|
||||||
node.updateLayout(size: availableSize, transition: .immediate)
|
|
||||||
}
|
|
||||||
|
|
||||||
if isDisplaying && !self.isVisible {
|
|
||||||
var fast = false
|
|
||||||
if let _ = transition.userData(DemoAnimateInTransition.self) {
|
|
||||||
fast = true
|
|
||||||
}
|
|
||||||
self.node?.setVisible(true, fast: fast)
|
|
||||||
} else if !isDisplaying && self.isVisible {
|
|
||||||
self.node?.setVisible(false)
|
|
||||||
}
|
|
||||||
self.isVisible = isDisplaying
|
|
||||||
|
|
||||||
return availableSize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func makeView() -> View {
|
|
||||||
return View()
|
|
||||||
}
|
|
||||||
|
|
||||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<DemoPageEnvironment>, transition: Transition) -> CGSize {
|
|
||||||
return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private let itemSize = CGSize(width: 110.0, height: 110.0)
|
|
||||||
|
|
||||||
private let order = ["😍","👌","🥴","🐳","🥱","🕊","🤡"]
|
|
||||||
|
|
||||||
private class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate {
|
|
||||||
private let context: AccountContext
|
|
||||||
private let theme: PresentationTheme
|
|
||||||
private let reactions: [AvailableReactions.Reaction]
|
|
||||||
private var itemContainerNodes: [ASDisplayNode] = []
|
|
||||||
private var itemNodes: [ReactionNode] = []
|
|
||||||
private let scrollNode: ASScrollNode
|
|
||||||
private let tapNode: ASDisplayNode
|
|
||||||
|
|
||||||
private var standaloneReactionAnimation: StandaloneReactionAnimation?
|
|
||||||
private var animator: DisplayLinkAnimator?
|
|
||||||
private var currentPosition: CGFloat = 0.0
|
|
||||||
private var currentIndex: Int = 0
|
|
||||||
|
|
||||||
private var validLayout: CGSize?
|
|
||||||
|
|
||||||
private var playingIndices = Set<Int>()
|
|
||||||
|
|
||||||
private let positionDelta: Double
|
|
||||||
|
|
||||||
private var previousInteractionTimestamp: Double = 0.0
|
|
||||||
private var timer: SwiftSignalKit.Timer?
|
|
||||||
|
|
||||||
private let animationCache: AnimationCache
|
|
||||||
private let animationRenderer: MultiAnimationRenderer
|
|
||||||
|
|
||||||
init(context: AccountContext, theme: PresentationTheme, reactions: [AvailableReactions.Reaction]) {
|
|
||||||
self.context = context
|
|
||||||
self.theme = theme
|
|
||||||
|
|
||||||
self.animationCache = context.animationCache
|
|
||||||
self.animationRenderer = context.animationRenderer
|
|
||||||
|
|
||||||
var reactionMap: [MessageReaction.Reaction: AvailableReactions.Reaction] = [:]
|
|
||||||
for reaction in reactions {
|
|
||||||
reactionMap[reaction.value] = reaction
|
|
||||||
}
|
|
||||||
|
|
||||||
var addedReactions = Set<MessageReaction.Reaction>()
|
|
||||||
var sortedReactions: [AvailableReactions.Reaction] = []
|
|
||||||
for emoji in order {
|
|
||||||
if let reaction = reactionMap[.builtin(emoji)] {
|
|
||||||
sortedReactions.append(reaction)
|
|
||||||
addedReactions.insert(.builtin(emoji))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for reaction in reactions {
|
|
||||||
if !addedReactions.contains(reaction.value) {
|
|
||||||
sortedReactions.append(reaction)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.reactions = sortedReactions
|
|
||||||
|
|
||||||
self.scrollNode = ASScrollNode()
|
|
||||||
self.tapNode = ASDisplayNode()
|
|
||||||
|
|
||||||
self.positionDelta = 1.0 / CGFloat(self.reactions.count)
|
|
||||||
|
|
||||||
super.init()
|
|
||||||
|
|
||||||
self.addSubnode(self.scrollNode)
|
|
||||||
self.scrollNode.addSubnode(self.tapNode)
|
|
||||||
|
|
||||||
self.setup()
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
self.timer?.invalidate()
|
|
||||||
}
|
|
||||||
|
|
||||||
override func didLoad() {
|
|
||||||
super.didLoad()
|
|
||||||
|
|
||||||
self.scrollNode.view.delegate = self
|
|
||||||
self.scrollNode.view.showsHorizontalScrollIndicator = false
|
|
||||||
self.scrollNode.view.canCancelContentTouches = true
|
|
||||||
|
|
||||||
self.tapNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.reactionTapped(_:))))
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func reactionTapped(_ gestureRecognizer: UITapGestureRecognizer) {
|
|
||||||
self.previousInteractionTimestamp = CACurrentMediaTime() + 1.0
|
|
||||||
|
|
||||||
if let animator = self.animator {
|
|
||||||
animator.invalidate()
|
|
||||||
self.animator = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
guard self.scrollStartPosition == nil else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let point = gestureRecognizer.location(in: self.view)
|
|
||||||
guard let index = self.itemContainerNodes.firstIndex(where: { $0.frame.contains(point) }) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
self.scrollTo(index, playReaction: true, immediately: true, duration: 0.85)
|
|
||||||
self.hapticFeedback.impact(.light)
|
|
||||||
}
|
|
||||||
|
|
||||||
func setVisible(_ visible: Bool, fast: Bool = false) {
|
|
||||||
if visible {
|
|
||||||
self.animateIn(fast: fast)
|
|
||||||
} else {
|
|
||||||
self.animator?.invalidate()
|
|
||||||
self.animator = nil
|
|
||||||
|
|
||||||
self.scrollTo(0, playReaction: false, immediately: false, duration: 0.0, clockwise: false)
|
|
||||||
self.timer?.invalidate()
|
|
||||||
self.timer = nil
|
|
||||||
|
|
||||||
self.playingIndices.removeAll()
|
|
||||||
self.standaloneReactionAnimation?.removeFromSupernode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func animateIn(fast: Bool) {
|
|
||||||
let duration: Double = fast ? 1.4 : 2.2
|
|
||||||
let delay: Double = fast ? 0.5 : 0.8
|
|
||||||
self.scrollTo(1, playReaction: false, immediately: true, duration: duration, damping: 0.75, clockwise: true)
|
|
||||||
Queue.mainQueue().after(delay, {
|
|
||||||
self.playReaction(index: 1)
|
|
||||||
})
|
|
||||||
|
|
||||||
if self.timer == nil {
|
|
||||||
self.previousInteractionTimestamp = CACurrentMediaTime()
|
|
||||||
self.timer = SwiftSignalKit.Timer(timeout: 0.2, repeat: true, completion: { [weak self] in
|
|
||||||
if let strongSelf = self {
|
|
||||||
let currentTimestamp = CACurrentMediaTime()
|
|
||||||
if currentTimestamp > strongSelf.previousInteractionTimestamp + 2.0 {
|
|
||||||
var nextIndex = strongSelf.currentIndex - 1
|
|
||||||
if nextIndex < 0 {
|
|
||||||
nextIndex = strongSelf.reactions.count + nextIndex
|
|
||||||
}
|
|
||||||
strongSelf.scrollTo(nextIndex, playReaction: true, immediately: true, duration: 0.85, clockwise: true)
|
|
||||||
strongSelf.previousInteractionTimestamp = currentTimestamp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, queue: Queue.mainQueue())
|
|
||||||
self.timer?.start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func animateOut() {
|
|
||||||
if let standaloneReactionAnimation = self.standaloneReactionAnimation {
|
|
||||||
standaloneReactionAnimation.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func springCurveFunc(_ t: Double, zeta: Double) -> Double {
|
|
||||||
let v0 = 0.0
|
|
||||||
let omega = 20.285
|
|
||||||
|
|
||||||
let y: Double
|
|
||||||
if abs(zeta - 1.0) < 1e-8 {
|
|
||||||
let c1 = -1.0
|
|
||||||
let c2 = v0 - omega
|
|
||||||
y = (c1 + c2 * t) * exp(-omega * t)
|
|
||||||
} else if zeta > 1 {
|
|
||||||
let s1 = omega * (-zeta + sqrt(zeta * zeta - 1))
|
|
||||||
let s2 = omega * (-zeta - sqrt(zeta * zeta - 1))
|
|
||||||
let c1 = (-s2 - v0) / (s2 - s1)
|
|
||||||
let c2 = (s1 + v0) / (s2 - s1)
|
|
||||||
y = c1 * exp(s1 * t) + c2 * exp(s2 * t)
|
|
||||||
} else {
|
|
||||||
let a = -omega * zeta
|
|
||||||
let b = omega * sqrt(1 - zeta * zeta)
|
|
||||||
let c2 = (v0 + a) / b
|
|
||||||
let theta = atan(c2)
|
|
||||||
// Alternatively y = (-cos(b * t) + c2 * sin(b * t)) * exp(a * t)
|
|
||||||
y = sqrt(1 + c2 * c2) * exp(a * t) * cos(b * t + theta + Double.pi)
|
|
||||||
}
|
|
||||||
|
|
||||||
return y + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
func scrollTo(_ index: Int, playReaction: Bool, immediately: Bool, duration: Double, damping: Double = 0.6, clockwise: Bool? = nil) {
|
|
||||||
guard index >= 0 && index < self.itemNodes.count else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self.currentIndex = index
|
|
||||||
let delta = self.positionDelta
|
|
||||||
|
|
||||||
let startPosition = self.currentPosition
|
|
||||||
let newPosition = delta * CGFloat(index)
|
|
||||||
var change = newPosition - startPosition
|
|
||||||
if let clockwise = clockwise {
|
|
||||||
if clockwise {
|
|
||||||
if change > 0.0 {
|
|
||||||
change = change - 1.0
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if change < 0.0 {
|
|
||||||
change = 1.0 + change
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if change > 0.5 {
|
|
||||||
change = change - 1.0
|
|
||||||
} else if change < -0.5 {
|
|
||||||
change = 1.0 + change
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if immediately {
|
|
||||||
self.playReaction(index: index)
|
|
||||||
}
|
|
||||||
|
|
||||||
if duration.isZero {
|
|
||||||
self.currentPosition = newPosition
|
|
||||||
if let size = self.validLayout {
|
|
||||||
self.updateLayout(size: size, transition: .immediate)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self.animator = DisplayLinkAnimator(duration: duration * UIView.animationDurationFactor(), from: 0.0, to: 1.0, update: { [weak self] t in
|
|
||||||
var t = t
|
|
||||||
if duration <= 0.2 {
|
|
||||||
t = listViewAnimationCurveSystem(t)
|
|
||||||
} else {
|
|
||||||
t = self?.springCurveFunc(t, zeta: damping) ?? 0.0
|
|
||||||
}
|
|
||||||
var updatedPosition = startPosition + change * t
|
|
||||||
while updatedPosition >= 1.0 {
|
|
||||||
updatedPosition -= 1.0
|
|
||||||
}
|
|
||||||
while updatedPosition < 0.0 {
|
|
||||||
updatedPosition += 1.0
|
|
||||||
}
|
|
||||||
self?.currentPosition = updatedPosition
|
|
||||||
if let size = self?.validLayout {
|
|
||||||
self?.updateLayout(size: size, transition: .immediate)
|
|
||||||
}
|
|
||||||
}, completion: { [weak self] in
|
|
||||||
self?.animator = nil
|
|
||||||
if playReaction && !immediately {
|
|
||||||
self?.playReaction(index: nil)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func setup() {
|
|
||||||
for reaction in self.reactions {
|
|
||||||
guard let centerAnimation = reaction.centerAnimation else {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
guard let aroundAnimation = reaction.aroundAnimation else {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
let containerNode = ASDisplayNode()
|
|
||||||
|
|
||||||
let itemNode = ReactionNode(context: self.context, theme: self.theme, item: ReactionItem(
|
|
||||||
reaction: ReactionItem.Reaction(rawValue: reaction.value),
|
|
||||||
appearAnimation: reaction.appearAnimation,
|
|
||||||
stillAnimation: reaction.selectAnimation,
|
|
||||||
listAnimation: centerAnimation,
|
|
||||||
largeListAnimation: reaction.activateAnimation,
|
|
||||||
applicationAnimation: aroundAnimation,
|
|
||||||
largeApplicationAnimation: reaction.effectAnimation,
|
|
||||||
isCustom: false
|
|
||||||
), animationCache: self.animationCache, animationRenderer: self.animationRenderer, loopIdle: false, hasAppearAnimation: false, useDirectRendering: false)
|
|
||||||
containerNode.isUserInteractionEnabled = false
|
|
||||||
containerNode.addSubnode(itemNode)
|
|
||||||
self.addSubnode(containerNode)
|
|
||||||
|
|
||||||
self.itemContainerNodes.append(containerNode)
|
|
||||||
self.itemNodes.append(itemNode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var ignoreContentOffsetChange = false
|
|
||||||
private func resetScrollPosition() {
|
|
||||||
self.scrollStartPosition = nil
|
|
||||||
self.ignoreContentOffsetChange = true
|
|
||||||
self.scrollNode.view.contentOffset = CGPoint(x: 5000.0 - self.scrollNode.frame.width * 0.5, y: 0.0)
|
|
||||||
self.ignoreContentOffsetChange = false
|
|
||||||
}
|
|
||||||
|
|
||||||
func playReaction(index: Int?) {
|
|
||||||
let index = index ?? max(0, Int(round(self.currentPosition / self.positionDelta)) % self.itemNodes.count)
|
|
||||||
|
|
||||||
guard !self.playingIndices.contains(index) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if let current = self.standaloneReactionAnimation, let dismiss = current.currentDismissAnimation {
|
|
||||||
dismiss()
|
|
||||||
current.currentDismissAnimation = nil
|
|
||||||
self.playingIndices.removeAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
let reaction = self.reactions[index]
|
|
||||||
let targetContainerNode = self.itemContainerNodes[index]
|
|
||||||
let targetView = self.itemNodes[index].view
|
|
||||||
|
|
||||||
guard let centerAnimation = reaction.centerAnimation else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard let aroundAnimation = reaction.aroundAnimation else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
self.playingIndices.insert(index)
|
|
||||||
|
|
||||||
targetContainerNode.view.superview?.bringSubviewToFront(targetContainerNode.view)
|
|
||||||
|
|
||||||
let standaloneReactionAnimation = StandaloneReactionAnimation(genericReactionEffect: nil, useDirectRendering: true)
|
|
||||||
self.standaloneReactionAnimation = standaloneReactionAnimation
|
|
||||||
|
|
||||||
targetContainerNode.addSubnode(standaloneReactionAnimation)
|
|
||||||
standaloneReactionAnimation.frame = targetContainerNode.bounds
|
|
||||||
standaloneReactionAnimation.animateReactionSelection(
|
|
||||||
context: self.context, theme: self.theme, animationCache: self.animationCache, reaction: ReactionItem(
|
|
||||||
reaction: ReactionItem.Reaction(rawValue: reaction.value),
|
|
||||||
appearAnimation: reaction.appearAnimation,
|
|
||||||
stillAnimation: reaction.selectAnimation,
|
|
||||||
listAnimation: centerAnimation,
|
|
||||||
largeListAnimation: reaction.activateAnimation,
|
|
||||||
applicationAnimation: aroundAnimation,
|
|
||||||
largeApplicationAnimation: reaction.effectAnimation,
|
|
||||||
isCustom: false
|
|
||||||
),
|
|
||||||
avatarPeers: [],
|
|
||||||
playHaptic: false,
|
|
||||||
isLarge: true,
|
|
||||||
forceSmallEffectAnimation: true,
|
|
||||||
targetView: targetView,
|
|
||||||
addStandaloneReactionAnimation: nil,
|
|
||||||
currentItemNode: self.itemNodes[index],
|
|
||||||
completion: { [weak standaloneReactionAnimation, weak self] in
|
|
||||||
standaloneReactionAnimation?.removeFromSupernode()
|
|
||||||
if self?.standaloneReactionAnimation === standaloneReactionAnimation {
|
|
||||||
self?.standaloneReactionAnimation = nil
|
|
||||||
self?.playingIndices.remove(index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var scrollStartPosition: (contentOffset: CGFloat, position: CGFloat, inverse: Bool)?
|
|
||||||
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
|
||||||
var inverse = false
|
|
||||||
let tapLocation = scrollView.panGestureRecognizer.location(in: scrollView)
|
|
||||||
if tapLocation.y < scrollView.frame.height / 2.0 {
|
|
||||||
inverse = true
|
|
||||||
}
|
|
||||||
if let scrollStartPosition = self.scrollStartPosition {
|
|
||||||
self.scrollStartPosition = (scrollStartPosition.contentOffset, scrollStartPosition.position, inverse)
|
|
||||||
} else {
|
|
||||||
self.scrollStartPosition = (scrollView.contentOffset.x, self.currentPosition, inverse)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private let hapticFeedback = HapticFeedback()
|
|
||||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
||||||
if scrollView.isTracking {
|
|
||||||
self.previousInteractionTimestamp = CACurrentMediaTime() + 1.0
|
|
||||||
}
|
|
||||||
|
|
||||||
if let animator = self.animator {
|
|
||||||
animator.invalidate()
|
|
||||||
self.animator = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
guard !self.ignoreContentOffsetChange, let (startContentOffset, startPosition, inverse) = self.scrollStartPosition else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let delta = scrollView.contentOffset.x - startContentOffset
|
|
||||||
var positionDelta = delta * -0.001
|
|
||||||
if inverse {
|
|
||||||
positionDelta *= -1.0
|
|
||||||
}
|
|
||||||
var updatedPosition = startPosition + positionDelta
|
|
||||||
while updatedPosition >= 1.0 {
|
|
||||||
updatedPosition -= 1.0
|
|
||||||
}
|
|
||||||
while updatedPosition < 0.0 {
|
|
||||||
updatedPosition += 1.0
|
|
||||||
}
|
|
||||||
self.currentPosition = updatedPosition
|
|
||||||
|
|
||||||
let indexDelta = self.positionDelta
|
|
||||||
let index = max(0, Int(round(self.currentPosition / indexDelta)) % self.itemNodes.count)
|
|
||||||
if index != self.currentIndex {
|
|
||||||
self.currentIndex = index
|
|
||||||
if self.scrollNode.view.isTracking || self.scrollNode.view.isDecelerating {
|
|
||||||
self.hapticFeedback.tap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let size = self.validLayout {
|
|
||||||
self.ignoreContentOffsetChange = true
|
|
||||||
self.updateLayout(size: size, transition: .immediate)
|
|
||||||
self.ignoreContentOffsetChange = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
|
|
||||||
guard let (startContentOffset, _, _) = self.scrollStartPosition, abs(velocity.x) > 0.0 else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let delta = self.positionDelta
|
|
||||||
let scrollDelta = targetContentOffset.pointee.x - startContentOffset
|
|
||||||
let positionDelta = scrollDelta * -0.001
|
|
||||||
let positionCounts = round(positionDelta / delta)
|
|
||||||
let adjustedPositionDelta = delta * positionCounts
|
|
||||||
let adjustedScrollDelta = adjustedPositionDelta * -1000.0
|
|
||||||
|
|
||||||
targetContentOffset.pointee = CGPoint(x: startContentOffset + adjustedScrollDelta, y: 0.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
|
||||||
if !decelerate {
|
|
||||||
self.previousInteractionTimestamp = CACurrentMediaTime() + 1.0
|
|
||||||
|
|
||||||
self.resetScrollPosition()
|
|
||||||
|
|
||||||
let delta = self.positionDelta
|
|
||||||
let index = max(0, Int(round(self.currentPosition / delta)) % self.itemNodes.count)
|
|
||||||
self.scrollTo(index, playReaction: true, immediately: true, duration: 0.2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
|
||||||
self.previousInteractionTimestamp = CACurrentMediaTime() + 1.0
|
|
||||||
|
|
||||||
self.resetScrollPosition()
|
|
||||||
self.playReaction(index: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
|
|
||||||
self.validLayout = size
|
|
||||||
|
|
||||||
self.scrollNode.frame = CGRect(origin: CGPoint(), size: size)
|
|
||||||
if self.scrollNode.view.contentSize.width.isZero {
|
|
||||||
self.scrollNode.view.contentSize = CGSize(width: 10000000.0, height: size.height)
|
|
||||||
self.tapNode.frame = CGRect(origin: CGPoint(), size: self.scrollNode.view.contentSize)
|
|
||||||
self.resetScrollPosition()
|
|
||||||
}
|
|
||||||
|
|
||||||
let delta = self.positionDelta
|
|
||||||
|
|
||||||
let areaSize = CGSize(width: floor(size.width * 0.7), height: size.height * 0.44)
|
|
||||||
|
|
||||||
for i in 0 ..< self.itemNodes.count {
|
|
||||||
let itemNode = self.itemNodes[i]
|
|
||||||
let containerNode = self.itemContainerNodes[i]
|
|
||||||
|
|
||||||
var angle = CGFloat.pi * 0.5 + CGFloat(i) * delta * CGFloat.pi * 2.0 - self.currentPosition * CGFloat.pi * 2.0
|
|
||||||
if angle < 0.0 {
|
|
||||||
angle = CGFloat.pi * 2.0 + angle
|
|
||||||
}
|
|
||||||
if angle > CGFloat.pi * 2.0 {
|
|
||||||
angle = angle - CGFloat.pi * 2.0
|
|
||||||
}
|
|
||||||
|
|
||||||
func calculateRelativeAngle(_ angle: CGFloat) -> CGFloat {
|
|
||||||
var relativeAngle = angle - CGFloat.pi * 0.5
|
|
||||||
if relativeAngle > CGFloat.pi {
|
|
||||||
relativeAngle = (2.0 * CGFloat.pi - relativeAngle) * -1.0
|
|
||||||
}
|
|
||||||
return relativeAngle
|
|
||||||
}
|
|
||||||
|
|
||||||
let rotatedAngle = angle - CGFloat.pi / 2.0
|
|
||||||
|
|
||||||
var updatedAngle = rotatedAngle + 0.5 * sin(rotatedAngle)
|
|
||||||
updatedAngle = updatedAngle + CGFloat.pi / 2.0
|
|
||||||
|
|
||||||
let relativeAngle = calculateRelativeAngle(updatedAngle)
|
|
||||||
let distance = abs(relativeAngle) / CGFloat.pi
|
|
||||||
|
|
||||||
let point = CGPoint(
|
|
||||||
x: cos(updatedAngle),
|
|
||||||
y: sin(updatedAngle)
|
|
||||||
)
|
|
||||||
|
|
||||||
let itemFrame = CGRect(origin: CGPoint(x: size.width * 0.5 + point.x * areaSize.width * 0.5 - itemSize.width * 0.5, y: size.height * 0.5 + point.y * areaSize.height * 0.5 - itemSize.height * 0.5), size: itemSize)
|
|
||||||
containerNode.bounds = CGRect(origin: CGPoint(), size: itemFrame.size)
|
|
||||||
containerNode.position = CGPoint(x: itemFrame.midX, y: itemFrame.midY)
|
|
||||||
transition.updateTransformScale(node: containerNode, scale: 1.0 - distance * 0.8)
|
|
||||||
|
|
||||||
itemNode.frame = CGRect(origin: CGPoint(), size: itemFrame.size)
|
|
||||||
itemNode.updateLayout(size: itemFrame.size, isExpanded: false, largeExpanded: false, isPreviewing: false, transition: transition)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,197 +0,0 @@
|
|||||||
import UIKit
|
|
||||||
import Display
|
|
||||||
|
|
||||||
private extension UILabel {
|
|
||||||
func textWidth() -> CGFloat {
|
|
||||||
return UILabel.textWidth(label: self)
|
|
||||||
}
|
|
||||||
|
|
||||||
class func textWidth(label: UILabel) -> CGFloat {
|
|
||||||
return textWidth(label: label, text: label.text!)
|
|
||||||
}
|
|
||||||
|
|
||||||
class func textWidth(label: UILabel, text: String) -> CGFloat {
|
|
||||||
return textWidth(font: label.font, text: text)
|
|
||||||
}
|
|
||||||
|
|
||||||
class func textWidth(font: UIFont, text: String) -> CGFloat {
|
|
||||||
let myText = text as NSString
|
|
||||||
|
|
||||||
let rect = CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
|
|
||||||
let labelSize = myText.boundingRect(with: rect, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: font], context: nil)
|
|
||||||
return ceil(labelSize.width)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
open class RollingLabel: UILabel {
|
|
||||||
private var fullText = ""
|
|
||||||
|
|
||||||
private var suffix: String = ""
|
|
||||||
open var showSymbol = false
|
|
||||||
private var scrollLayers: [CAScrollLayer] = []
|
|
||||||
private var scrollLabels: [UILabel] = []
|
|
||||||
private let durationOffset = 0.2
|
|
||||||
private let textsNotAnimated = [","]
|
|
||||||
|
|
||||||
public func setSuffix(suffix: String) {
|
|
||||||
self.suffix = suffix
|
|
||||||
}
|
|
||||||
|
|
||||||
func configure(with string: String, increment: Bool = false, duration: Double = 0.0) {
|
|
||||||
self.fullText = string
|
|
||||||
|
|
||||||
self.clean()
|
|
||||||
self.setupSubviews()
|
|
||||||
|
|
||||||
self.text = " "
|
|
||||||
self.animate(increment: increment, duration: duration)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func clean() {
|
|
||||||
self.text = nil
|
|
||||||
self.subviews.forEach { $0.removeFromSuperview() }
|
|
||||||
self.layer.sublayers?.forEach { $0.removeFromSuperlayer() }
|
|
||||||
self.scrollLayers.removeAll()
|
|
||||||
self.scrollLabels.removeAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func setupSubviews() {
|
|
||||||
let stringArray = fullText.map { String($0) }
|
|
||||||
var x: CGFloat = 0
|
|
||||||
let y: CGFloat = 0
|
|
||||||
if self.textAlignment == .center {
|
|
||||||
if showSymbol {
|
|
||||||
self.text = "\(fullText) \(suffix)"
|
|
||||||
} else {
|
|
||||||
self.text = fullText
|
|
||||||
}
|
|
||||||
let w = UILabel.textWidth(font: self.font, text: self.text ?? "")
|
|
||||||
self.text = ""
|
|
||||||
x = -(w / 2)
|
|
||||||
} else if self.textAlignment == .right {
|
|
||||||
if showSymbol {
|
|
||||||
self.text = "\(fullText) \(suffix) "
|
|
||||||
} else {
|
|
||||||
self.text = fullText
|
|
||||||
}
|
|
||||||
let w = UILabel.textWidth(font: self.font, text: self.text ?? "")
|
|
||||||
self.text = ""
|
|
||||||
x = -w
|
|
||||||
}
|
|
||||||
|
|
||||||
if showSymbol {
|
|
||||||
let wLabel = UILabel()
|
|
||||||
wLabel.frame.origin = CGPoint(x: x, y: y)
|
|
||||||
wLabel.textColor = textColor
|
|
||||||
wLabel.font = font
|
|
||||||
wLabel.text = "\(suffix) "
|
|
||||||
wLabel.textAlignment = .center
|
|
||||||
wLabel.sizeToFit()
|
|
||||||
self.addSubview(wLabel)
|
|
||||||
x += wLabel.bounds.width
|
|
||||||
}
|
|
||||||
|
|
||||||
stringArray.enumerated().forEach { index, text in
|
|
||||||
let nonDigits = CharacterSet.decimalDigits.inverted
|
|
||||||
if text.rangeOfCharacter(from: nonDigits) != nil {
|
|
||||||
let label = UILabel()
|
|
||||||
label.frame.origin = CGPoint(x: x, y: y - 1.0 - UIScreenPixel)
|
|
||||||
label.textColor = textColor
|
|
||||||
label.font = font
|
|
||||||
label.text = text
|
|
||||||
label.textAlignment = .center
|
|
||||||
label.sizeToFit()
|
|
||||||
self.addSubview(label)
|
|
||||||
|
|
||||||
x += label.bounds.width
|
|
||||||
} else {
|
|
||||||
let label = UILabel()
|
|
||||||
label.frame.origin = CGPoint(x: x, y: y)
|
|
||||||
label.textColor = textColor
|
|
||||||
label.font = font
|
|
||||||
label.text = "0"
|
|
||||||
label.textAlignment = .center
|
|
||||||
label.sizeToFit()
|
|
||||||
createScrollLayer(to: label, text: text, index: index)
|
|
||||||
|
|
||||||
x += label.bounds.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func createScrollLayer(to label: UILabel, text: String, index: Int) {
|
|
||||||
let scrollLayer = CAScrollLayer()
|
|
||||||
scrollLayer.frame = CGRect(x: label.frame.minX, y: label.frame.minY - 10.0, width: label.frame.width, height: label.frame.height * 3.0)
|
|
||||||
scrollLayers.append(scrollLayer)
|
|
||||||
self.layer.addSublayer(scrollLayer)
|
|
||||||
|
|
||||||
createContentForLayer(scrollLayer: scrollLayer, text: text, index: index)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func createContentForLayer(scrollLayer: CAScrollLayer, text: String, index: Int) {
|
|
||||||
var textsForScroll: [String] = []
|
|
||||||
|
|
||||||
let max: Int
|
|
||||||
var found = false
|
|
||||||
if let val = Int(text), index == 0 {
|
|
||||||
max = val
|
|
||||||
found = true
|
|
||||||
} else {
|
|
||||||
max = 9
|
|
||||||
}
|
|
||||||
|
|
||||||
for i in 0...max {
|
|
||||||
let str = String(i)
|
|
||||||
textsForScroll.append(str)
|
|
||||||
}
|
|
||||||
if !found && text != "9" {
|
|
||||||
textsForScroll.append(text)
|
|
||||||
}
|
|
||||||
|
|
||||||
var height: CGFloat = 0.0
|
|
||||||
for text in textsForScroll {
|
|
||||||
let label = UILabel()
|
|
||||||
label.text = text
|
|
||||||
label.textColor = textColor
|
|
||||||
label.font = font
|
|
||||||
label.textAlignment = .center
|
|
||||||
label.frame = CGRect(x: 0, y: height, width: scrollLayer.frame.width, height: scrollLayer.frame.height)
|
|
||||||
scrollLayer.addSublayer(label.layer)
|
|
||||||
scrollLabels.append(label)
|
|
||||||
|
|
||||||
height = label.frame.maxY
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func animate(ascending: Bool = true, increment: Bool, duration: Double) {
|
|
||||||
var offset: CFTimeInterval = 0.0
|
|
||||||
|
|
||||||
for scrollLayer in self.scrollLayers {
|
|
||||||
let maxY = scrollLayer.sublayers?.last?.frame.origin.y ?? 0.0
|
|
||||||
let height = scrollLayer.sublayers?.last?.frame.size.height ?? 0.0
|
|
||||||
|
|
||||||
let animation = CABasicAnimation(keyPath: "sublayerTransform.translation.y")
|
|
||||||
animation.duration = duration + offset
|
|
||||||
animation.timingFunction = CAMediaTimingFunction(name: .easeOut)
|
|
||||||
|
|
||||||
let verticalOffset = 20.0
|
|
||||||
if ascending {
|
|
||||||
if increment {
|
|
||||||
animation.fromValue = height + verticalOffset
|
|
||||||
} else {
|
|
||||||
animation.fromValue = maxY + verticalOffset
|
|
||||||
}
|
|
||||||
animation.toValue = 0
|
|
||||||
} else {
|
|
||||||
animation.fromValue = 0
|
|
||||||
animation.toValue = maxY + verticalOffset
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollLayer.scrollMode = .vertically
|
|
||||||
scrollLayer.add(animation, forKey: nil)
|
|
||||||
scrollLayer.scroll(to: CGPoint(x: 0, y: maxY + verticalOffset))
|
|
||||||
|
|
||||||
offset += self.durationOffset
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -473,7 +473,7 @@ private enum StatsEntry: ItemListNodeEntry {
|
|||||||
arguments.contextAction(message.id, node, gesture)
|
arguments.contextAction(message.id, node, gesture)
|
||||||
})
|
})
|
||||||
case let .booster(_, _, dateTimeFormat, peer, expires):
|
case let .booster(_, _, dateTimeFormat, peer, expires):
|
||||||
let expiresValue = stringForFullDate(timestamp: expires, strings: presentationData.strings, dateTimeFormat: dateTimeFormat)
|
let expiresValue = stringForMediumDate(timestamp: expires, strings: presentationData.strings, dateTimeFormat: dateTimeFormat)
|
||||||
return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: PresentationDateTimeFormat(), nameDisplayOrder: presentationData.nameDisplayOrder, context: arguments.context, peer: peer, presence: nil, text: .text(presentationData.strings.Stats_Boosts_ExpiresOn(expiresValue).string, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: peer.id != arguments.context.account.peerId, sectionId: self.section, action: {
|
return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: PresentationDateTimeFormat(), nameDisplayOrder: presentationData.nameDisplayOrder, context: arguments.context, peer: peer, presence: nil, text: .text(presentationData.strings.Stats_Boosts_ExpiresOn(expiresValue).string, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: peer.id != arguments.context.account.peerId, sectionId: self.section, action: {
|
||||||
arguments.openPeer(peer)
|
arguments.openPeer(peer)
|
||||||
}, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in })
|
}, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in })
|
||||||
@ -500,13 +500,13 @@ private enum StatsEntry: ItemListNodeEntry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum ChannelStatsSection {
|
||||||
|
case stats
|
||||||
|
case boosts
|
||||||
|
}
|
||||||
|
|
||||||
private struct ChannelStatsControllerState: Equatable {
|
private struct ChannelStatsControllerState: Equatable {
|
||||||
enum Section {
|
let section: ChannelStatsSection
|
||||||
case stats
|
|
||||||
case boosts
|
|
||||||
}
|
|
||||||
|
|
||||||
let section: Section
|
|
||||||
let boostersExpanded: Bool
|
let boostersExpanded: Bool
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
@ -514,7 +514,7 @@ private struct ChannelStatsControllerState: Equatable {
|
|||||||
self.boostersExpanded = false
|
self.boostersExpanded = false
|
||||||
}
|
}
|
||||||
|
|
||||||
init(section: Section, boostersExpanded: Bool) {
|
init(section: ChannelStatsSection, boostersExpanded: Bool) {
|
||||||
self.section = section
|
self.section = section
|
||||||
self.boostersExpanded = boostersExpanded
|
self.boostersExpanded = boostersExpanded
|
||||||
}
|
}
|
||||||
@ -529,7 +529,7 @@ private struct ChannelStatsControllerState: Equatable {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func withUpdatedSection(_ section: Section) -> ChannelStatsControllerState {
|
func withUpdatedSection(_ section: ChannelStatsSection) -> ChannelStatsControllerState {
|
||||||
return ChannelStatsControllerState(section: section, boostersExpanded: self.boostersExpanded)
|
return ChannelStatsControllerState(section: section, boostersExpanded: self.boostersExpanded)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -682,9 +682,9 @@ private func channelStatsControllerEntries(state: ChannelStatsControllerState, p
|
|||||||
return entries
|
return entries
|
||||||
}
|
}
|
||||||
|
|
||||||
public func channelStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: PeerId, statsDatacenterId: Int32?) -> ViewController {
|
public func channelStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: PeerId, section: ChannelStatsSection = .stats, boostStatus: ChannelBoostStatus? = nil, statsDatacenterId: Int32?) -> ViewController {
|
||||||
let statePromise = ValuePromise(ChannelStatsControllerState(), ignoreRepeated: true)
|
let statePromise = ValuePromise(ChannelStatsControllerState(section: section, boostersExpanded: false), ignoreRepeated: true)
|
||||||
let stateValue = Atomic(value: ChannelStatsControllerState())
|
let stateValue = Atomic(value: ChannelStatsControllerState(section: section, boostersExpanded: false))
|
||||||
let updateState: ((ChannelStatsControllerState) -> ChannelStatsControllerState) -> Void = { f in
|
let updateState: ((ChannelStatsControllerState) -> ChannelStatsControllerState) -> Void = { f in
|
||||||
statePromise.set(stateValue.modify { f($0) })
|
statePromise.set(stateValue.modify { f($0) })
|
||||||
}
|
}
|
||||||
@ -717,7 +717,12 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
|
|||||||
})
|
})
|
||||||
dataPromise.set(.single(nil) |> then(dataSignal))
|
dataPromise.set(.single(nil) |> then(dataSignal))
|
||||||
|
|
||||||
let boostData = context.engine.peers.getChannelBoostStatus(peerId: peerId)
|
let boostData: Signal<ChannelBoostStatus?, NoError>
|
||||||
|
if let boostStatus {
|
||||||
|
boostData = .single(boostStatus)
|
||||||
|
} else {
|
||||||
|
boostData = context.engine.peers.getChannelBoostStatus(peerId: peerId)
|
||||||
|
}
|
||||||
let boostersContext = ChannelBoostersContext(account: context.account, peerId: peerId)
|
let boostersContext = ChannelBoostersContext(account: context.account, peerId: peerId)
|
||||||
|
|
||||||
var presentImpl: ((ViewController) -> Void)?
|
var presentImpl: ((ViewController) -> Void)?
|
||||||
|
@ -288,7 +288,7 @@ public class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
|
|||||||
|
|
||||||
let labelFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 13.0 / 17.0))
|
let labelFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 13.0 / 17.0))
|
||||||
|
|
||||||
let label = stringForFullDate(timestamp: item.message.timestamp, strings: item.presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat)
|
let label = stringForMediumDate(timestamp: item.message.timestamp, strings: item.presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat)
|
||||||
|
|
||||||
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: label, font: labelFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - totalLeftInset - rightInset - additionalRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: label, font: labelFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - totalLeftInset - rightInset - additionalRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||||
|
|
||||||
|
@ -1138,34 +1138,33 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let context = component.context
|
|
||||||
let navigationController = controller.navigationController as? NavigationController
|
let navigationController = controller.navigationController as? NavigationController
|
||||||
|
|
||||||
switch error {
|
switch error {
|
||||||
case .generic:
|
case .generic:
|
||||||
controller.dismiss()
|
controller.dismiss()
|
||||||
case let .dialogFilterLimitExceeded(limit, _):
|
case let .dialogFilterLimitExceeded(limit, _):
|
||||||
let limitController = PremiumLimitScreen(context: component.context, subject: .folders, count: limit, action: { [weak navigationController] in
|
let limitController = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .folders, count: limit, forceDark: false, cancel: {}, action: { [weak navigationController] in
|
||||||
guard let navigationController else {
|
guard let navigationController else {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
navigationController.pushViewController(PremiumIntroScreen(context: context, source: .folders))
|
navigationController.pushViewController(PremiumIntroScreen(context: component.context, source: .folders))
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
controller.push(limitController)
|
controller.push(limitController)
|
||||||
controller.dismiss()
|
controller.dismiss()
|
||||||
case let .sharedFolderLimitExceeded(limit, _):
|
case let .sharedFolderLimitExceeded(limit, _):
|
||||||
let limitController = PremiumLimitScreen(context: component.context, subject: .membershipInSharedFolders, count: limit, action: { [weak navigationController] in
|
let limitController = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .membershipInSharedFolders, count: limit, forceDark: false, cancel: {}, action: { [weak navigationController] in
|
||||||
guard let navigationController else {
|
guard let navigationController else {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
navigationController.pushViewController(PremiumIntroScreen(context: context, source: .membershipInSharedFolders))
|
navigationController.pushViewController(PremiumIntroScreen(context: component.context, source: .membershipInSharedFolders))
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
controller.push(limitController)
|
controller.push(limitController)
|
||||||
controller.dismiss()
|
controller.dismiss()
|
||||||
case let .tooManyChannels(limit, _):
|
case let .tooManyChannels(limit, _):
|
||||||
let limitController = PremiumLimitScreen(context: component.context, subject: .chatsPerFolder, count: limit, action: { [weak navigationController] in
|
let limitController = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .chatsPerFolder, count: limit, forceDark: false, cancel: {}, action: { [weak navigationController] in
|
||||||
guard let navigationController else {
|
guard let navigationController else {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -1175,7 +1174,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
|
|||||||
controller.push(limitController)
|
controller.push(limitController)
|
||||||
controller.dismiss()
|
controller.dismiss()
|
||||||
case let .tooManyChannelsInAccount(limit, _):
|
case let .tooManyChannelsInAccount(limit, _):
|
||||||
let limitController = PremiumLimitScreen(context: component.context, subject: .channels, count: limit, action: { [weak navigationController] in
|
let limitController = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .channels, count: limit, forceDark: false, cancel: {}, action: { [weak navigationController] in
|
||||||
guard let navigationController else {
|
guard let navigationController else {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -1434,7 +1433,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
|
|||||||
|
|
||||||
return
|
return
|
||||||
case let .tooManyChannels(limit, _):
|
case let .tooManyChannels(limit, _):
|
||||||
let limitController = PremiumLimitScreen(context: component.context, subject: .chatsPerFolder, count: limit, action: { [weak navigationController] in
|
let limitController = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .chatsPerFolder, count: limit, forceDark: false, cancel: {}, action: { [weak navigationController] in
|
||||||
guard let navigationController else {
|
guard let navigationController else {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -1446,7 +1445,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
|
|||||||
|
|
||||||
return
|
return
|
||||||
case let .tooManyChannelsInAccount(limit, _):
|
case let .tooManyChannelsInAccount(limit, _):
|
||||||
let limitController = PremiumLimitScreen(context: component.context, subject: .channels, count: limit, action: { [weak navigationController] in
|
let limitController = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .channels, count: limit, forceDark: false, cancel: {}, action: { [weak navigationController] in
|
||||||
guard let navigationController else {
|
guard let navigationController else {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
12
submodules/TelegramUI/Images.xcassets/Premium/Stats.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Premium/Stats.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "Stats.pdf",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
146
submodules/TelegramUI/Images.xcassets/Premium/Stats.imageset/Stats.pdf
vendored
Normal file
146
submodules/TelegramUI/Images.xcassets/Premium/Stats.imageset/Stats.pdf
vendored
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
%PDF-1.7
|
||||||
|
|
||||||
|
1 0 obj
|
||||||
|
<< >>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
2 0 obj
|
||||||
|
<< /Length 3 0 R >>
|
||||||
|
stream
|
||||||
|
/DeviceRGB CS
|
||||||
|
/DeviceRGB cs
|
||||||
|
q
|
||||||
|
1.000000 0.000000 -0.000000 1.000000 3.335007 3.241150 cm
|
||||||
|
1.000000 1.000000 1.000000 scn
|
||||||
|
21.232433 20.412090 m
|
||||||
|
21.423946 20.725475 21.325150 21.134775 21.011765 21.326286 c
|
||||||
|
20.698380 21.517799 20.289082 21.419003 20.097569 21.105619 c
|
||||||
|
15.822404 14.109897 l
|
||||||
|
15.340820 13.321852 14.308747 13.078213 13.525581 13.567692 c
|
||||||
|
12.107020 14.454292 10.237064 14.004822 9.376399 12.570378 c
|
||||||
|
6.094776 7.100996 l
|
||||||
|
5.905817 6.786065 6.007938 6.377582 6.322869 6.188623 c
|
||||||
|
6.637800 5.999665 7.046283 6.101787 7.235241 6.416718 c
|
||||||
|
10.516866 11.886100 l
|
||||||
|
10.994746 12.682569 12.033031 12.932136 12.820683 12.439854 c
|
||||||
|
14.231167 11.558301 16.089935 11.997094 16.957268 13.416368 c
|
||||||
|
21.232433 20.412090 l
|
||||||
|
h
|
||||||
|
1.664999 20.093855 m
|
||||||
|
1.829460 20.093855 1.926293 20.093451 1.995498 20.089354 c
|
||||||
|
1.999595 20.020149 1.999999 19.923315 1.999999 19.758856 c
|
||||||
|
2.000000 14.923857 l
|
||||||
|
0.665000 14.923857 l
|
||||||
|
0.297731 14.923857 0.000000 14.626126 0.000000 14.258857 c
|
||||||
|
0.000000 13.891587 0.297731 13.593857 0.665000 13.593857 c
|
||||||
|
2.000000 13.593857 l
|
||||||
|
2.000000 8.758855 l
|
||||||
|
2.000000 8.729465 l
|
||||||
|
2.000054 8.423856 l
|
||||||
|
0.665000 8.423856 l
|
||||||
|
0.297731 8.423856 0.000000 8.126125 0.000000 7.758856 c
|
||||||
|
0.000000 7.391586 0.297731 7.093856 0.665000 7.093856 c
|
||||||
|
2.011757 7.093856 l
|
||||||
|
2.019459 6.782750 2.032205 6.499888 2.053299 6.241710 c
|
||||||
|
2.107750 5.575265 2.221116 5.014942 2.481206 4.504488 c
|
||||||
|
2.904487 3.673752 3.579896 2.998343 4.410632 2.575062 c
|
||||||
|
4.921087 2.314972 5.481410 2.201605 6.147855 2.147156 c
|
||||||
|
6.406034 2.126060 6.688895 2.113314 7.000000 2.105612 c
|
||||||
|
7.000000 0.758854 l
|
||||||
|
7.000000 0.391584 7.297730 0.093853 7.665000 0.093853 c
|
||||||
|
8.032269 0.093853 8.330000 0.391584 8.330000 0.758854 c
|
||||||
|
8.330000 2.093908 l
|
||||||
|
8.429698 2.093853 8.531552 2.093855 8.635623 2.093855 c
|
||||||
|
8.664999 2.093855 l
|
||||||
|
13.500005 2.093855 l
|
||||||
|
13.500000 0.758856 l
|
||||||
|
13.499998 0.391586 13.797728 0.093855 14.164997 0.093853 c
|
||||||
|
14.532267 0.093853 14.829999 0.391582 14.830000 0.758852 c
|
||||||
|
14.830005 2.093855 l
|
||||||
|
19.664999 2.093855 l
|
||||||
|
19.829456 2.093855 19.926289 2.093451 19.995493 2.089355 c
|
||||||
|
19.999588 2.020151 19.999998 1.923313 19.999998 1.758856 c
|
||||||
|
19.999998 0.758856 l
|
||||||
|
19.999998 0.391586 20.297729 0.093855 20.664999 0.093855 c
|
||||||
|
21.032269 0.093855 21.330000 0.391586 21.330000 0.758856 c
|
||||||
|
21.330000 1.758856 l
|
||||||
|
21.330002 1.779581 l
|
||||||
|
21.330023 1.936590 21.330044 2.091722 21.319275 2.223507 c
|
||||||
|
21.307400 2.368851 21.279184 2.543285 21.189398 2.719503 c
|
||||||
|
21.065722 2.962233 20.868376 3.159578 20.625647 3.283255 c
|
||||||
|
20.449429 3.373043 20.274994 3.401257 20.129650 3.413132 c
|
||||||
|
19.997866 3.423901 19.842733 3.423880 19.685724 3.423859 c
|
||||||
|
19.664999 3.423857 l
|
||||||
|
8.664999 3.423857 l
|
||||||
|
7.603928 3.423857 6.848118 3.424374 6.256159 3.472738 c
|
||||||
|
5.671963 3.520470 5.306152 3.611465 5.014439 3.760101 c
|
||||||
|
4.433959 4.055870 3.962014 4.527815 3.666245 5.108295 c
|
||||||
|
3.517610 5.400007 3.426613 5.765819 3.378882 6.350015 c
|
||||||
|
3.330517 6.941974 3.330000 7.697783 3.330000 8.758855 c
|
||||||
|
3.330000 14.258160 l
|
||||||
|
3.330000 14.258857 l
|
||||||
|
3.329999 14.259554 l
|
||||||
|
3.329999 19.758856 l
|
||||||
|
3.330001 19.779600 l
|
||||||
|
3.330022 19.936602 3.330043 20.091726 3.319276 20.223507 c
|
||||||
|
3.307400 20.368851 3.279186 20.543285 3.189398 20.719503 c
|
||||||
|
3.065721 20.962233 2.868376 21.159576 2.625647 21.283253 c
|
||||||
|
2.449428 21.373041 2.274994 21.401257 2.129650 21.413132 c
|
||||||
|
1.997869 21.423899 1.842742 21.423878 1.685738 21.423857 c
|
||||||
|
1.664999 21.423855 l
|
||||||
|
0.664999 21.423855 l
|
||||||
|
0.297732 21.423855 0.000000 21.126123 0.000000 20.758856 c
|
||||||
|
0.000000 20.391586 0.297732 20.093855 0.665001 20.093855 c
|
||||||
|
1.664999 20.093855 l
|
||||||
|
h
|
||||||
|
f*
|
||||||
|
n
|
||||||
|
Q
|
||||||
|
|
||||||
|
endstream
|
||||||
|
endobj
|
||||||
|
|
||||||
|
3 0 obj
|
||||||
|
3459
|
||||||
|
endobj
|
||||||
|
|
||||||
|
4 0 obj
|
||||||
|
<< /Annots []
|
||||||
|
/Type /Page
|
||||||
|
/MediaBox [ 0.000000 0.000000 30.000000 30.000000 ]
|
||||||
|
/Resources 1 0 R
|
||||||
|
/Contents 2 0 R
|
||||||
|
/Parent 5 0 R
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
5 0 obj
|
||||||
|
<< /Kids [ 4 0 R ]
|
||||||
|
/Count 1
|
||||||
|
/Type /Pages
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
6 0 obj
|
||||||
|
<< /Pages 5 0 R
|
||||||
|
/Type /Catalog
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
xref
|
||||||
|
0 7
|
||||||
|
0000000000 65535 f
|
||||||
|
0000000010 00000 n
|
||||||
|
0000000034 00000 n
|
||||||
|
0000003549 00000 n
|
||||||
|
0000003572 00000 n
|
||||||
|
0000003745 00000 n
|
||||||
|
0000003819 00000 n
|
||||||
|
trailer
|
||||||
|
<< /ID [ (some) (id) ]
|
||||||
|
/Root 6 0 R
|
||||||
|
/Size 7
|
||||||
|
>>
|
||||||
|
startxref
|
||||||
|
3878
|
||||||
|
%%EOF
|
@ -6642,7 +6642,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
|||||||
self.controller?.push(PeerInfoStoryGridScreen(context: self.context, peerId: self.peerId, scope: .archive))
|
self.controller?.push(PeerInfoStoryGridScreen(context: self.context, peerId: self.peerId, scope: .archive))
|
||||||
}
|
}
|
||||||
|
|
||||||
private func openStats() {
|
private func openStats(boosts: Bool = false, boostStatus: ChannelBoostStatus? = nil) {
|
||||||
guard let controller = self.controller, let data = self.data, let peer = data.peer, let cachedData = data.cachedData else {
|
guard let controller = self.controller, let data = self.data, let peer = data.peer, let cachedData = data.cachedData else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -6657,7 +6657,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
|||||||
if let channel = peer as? TelegramChannel, case .group = channel.info {
|
if let channel = peer as? TelegramChannel, case .group = channel.info {
|
||||||
statsController = groupStatsController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: peer.id, statsDatacenterId: statsDatacenterId)
|
statsController = groupStatsController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: peer.id, statsDatacenterId: statsDatacenterId)
|
||||||
} else {
|
} else {
|
||||||
statsController = channelStatsController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: peer.id, statsDatacenterId: statsDatacenterId)
|
statsController = channelStatsController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: peer.id, section: boosts ? .boosts : .stats, boostStatus: boostStatus, statsDatacenterId: statsDatacenterId)
|
||||||
}
|
}
|
||||||
controller.push(statsController)
|
controller.push(statsController)
|
||||||
}
|
}
|
||||||
@ -8310,13 +8310,17 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
|||||||
if let previousController = navigationController.viewControllers.last as? ShareWithPeersScreen {
|
if let previousController = navigationController.viewControllers.last as? ShareWithPeersScreen {
|
||||||
previousController.dismiss()
|
previousController.dismiss()
|
||||||
}
|
}
|
||||||
let controller = self.context.sharedContext.makePremiumLimitController(context: self.context, subject: .storiesChannelBoost(peer: peer, isCurrent: true, level: Int32(status.level), currentLevelBoosts: Int32(status.currentLevelBoosts), nextLevelBoosts: status.nextLevelBoosts.flatMap(Int32.init), link: link, boosted: false), count: Int32(status.boosts), forceDark: false, cancel: {}, action: { [weak self] in
|
let controller = PremiumLimitScreen(context: self.context, subject: .storiesChannelBoost(peer: peer, isCurrent: true, level: Int32(status.level), currentLevelBoosts: Int32(status.currentLevelBoosts), nextLevelBoosts: status.nextLevelBoosts.flatMap(Int32.init), link: link, boosted: false), count: Int32(status.boosts), action: { [weak self] in
|
||||||
UIPasteboard.general.string = "https://\(link)"
|
UIPasteboard.general.string = "https://\(link)"
|
||||||
|
|
||||||
if let self {
|
if let self {
|
||||||
self.controller?.present(UndoOverlayController(presentationData: self.presentationData, content: .linkCopied(text: self.presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { _ in return false }), in: .current)
|
self.controller?.present(UndoOverlayController(presentationData: self.presentationData, content: .linkCopied(text: self.presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { _ in return false }), in: .current)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
}, openStats: { [weak self] in
|
||||||
|
if let self {
|
||||||
|
self.openStats(boosts: true, boostStatus: status)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
navigationController.pushViewController(controller)
|
navigationController.pushViewController(controller)
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user