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 {
|
||||
controller = DocumentPickerViewController(documentTypes: documentTypes, in: mode.documentPickerMode)
|
||||
}
|
||||
controller.forceDarkTheme = forceDarkTheme
|
||||
controller.forceDarkTheme = forceDarkTheme || theme.overallDarkAppearance
|
||||
controller.didDisappear = {
|
||||
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 tailWidth: CGFloat = 20.0
|
||||
let tailHeight: CGFloat = 9.0
|
||||
let tailRadius: CGFloat = 4.0
|
||||
|
||||
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()
|
||||
|
||||
@ -278,8 +282,8 @@ public class PremiumLimitDisplayComponent: Component {
|
||||
|
||||
private let badgeForeground: SimpleLayer
|
||||
private let badgeIcon: UIImageView
|
||||
private let badgeCountLabel: RollingLabel
|
||||
private let countMaskView = UIImageView()
|
||||
private let badgeLabel: BadgeLabelView
|
||||
private let badgeLabelMaskView = UIImageView()
|
||||
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
|
||||
@ -311,11 +315,9 @@ public class PremiumLimitDisplayComponent: Component {
|
||||
self.badgeIcon = UIImageView()
|
||||
self.badgeIcon.contentMode = .center
|
||||
|
||||
self.badgeCountLabel = RollingLabel()
|
||||
self.badgeCountLabel.font = Font.with(size: 24.0, design: .round, weight: .semibold, traits: [])
|
||||
self.badgeCountLabel.textColor = .white
|
||||
self.badgeCountLabel.configure(with: "0")
|
||||
self.badgeCountLabel.mask = self.countMaskView
|
||||
self.badgeLabel = BadgeLabelView()
|
||||
let _ = self.badgeLabel.update(value: "0", transition: .immediate)
|
||||
self.badgeLabel.mask = self.badgeLabelMaskView
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
@ -327,10 +329,10 @@ public class PremiumLimitDisplayComponent: Component {
|
||||
self.addSubview(self.badgeView)
|
||||
self.badgeView.layer.addSublayer(self.badgeForeground)
|
||||
self.badgeView.addSubview(self.badgeIcon)
|
||||
self.badgeView.addSubview(self.badgeCountLabel)
|
||||
self.badgeView.addSubview(self.badgeLabel)
|
||||
|
||||
self.countMaskView.contentMode = .scaleToFill
|
||||
self.countMaskView.image = generateImage(CGSize(width: 2.0, height: 48.0), rotatedContext: { size, context in
|
||||
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)
|
||||
|
||||
@ -339,9 +341,8 @@ public class PremiumLimitDisplayComponent: Component {
|
||||
UIColor(rgb: 0xffffff).cgColor,
|
||||
UIColor(rgb: 0xffffff).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)!
|
||||
|
||||
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
|
||||
func playAppearanceAnimation(component: PremiumLimitDisplayComponent, availableSize: CGSize, from: CGFloat? = nil) {
|
||||
func playAppearanceAnimation(component: PremiumLimitDisplayComponent, badgeFullSize: CGSize, from: CGFloat? = nil) {
|
||||
if from == nil {
|
||||
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")
|
||||
positionAnimation.fromValue = NSValue(cgPoint: CGPoint(x: from ?? 0.0, y: 0.0))
|
||||
positionAnimation.toValue = NSValue(cgPoint: self.badgeView.center)
|
||||
@ -370,7 +378,7 @@ public class PremiumLimitDisplayComponent: Component {
|
||||
|
||||
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
|
||||
rotateAnimation.fromValue = 0.0 as NSNumber
|
||||
rotateAnimation.toValue = -0.26 as NSNumber
|
||||
rotateAnimation.toValue = -rotationAngle as NSNumber
|
||||
rotateAnimation.duration = 0.15
|
||||
rotateAnimation.fillMode = .forwards
|
||||
rotateAnimation.timingFunction = CAMediaTimingFunction(name: .easeOut)
|
||||
@ -379,7 +387,7 @@ public class PremiumLimitDisplayComponent: Component {
|
||||
|
||||
Queue.mainQueue().after(0.5, {
|
||||
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.duration = 0.2
|
||||
bounceAnimation.fillMode = .forwards
|
||||
@ -410,7 +418,13 @@ public class PremiumLimitDisplayComponent: Component {
|
||||
}
|
||||
|
||||
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 {
|
||||
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)
|
||||
|
||||
// self.badgeMaskArrowView.isHidden = component.isPremiumDisabled
|
||||
progressTransition.setShapeLayerPath(layer: self.badgeShapeLayer, path: generateBadgePath(rectSize: badgeSize, tailPosition: component.isPremiumDisabled ? nil : 1.0).cgPath)
|
||||
|
||||
if let _ = self.badgeView.layer.animation(forKey: "appearance1") {
|
||||
|
||||
@ -654,10 +666,8 @@ public class PremiumLimitDisplayComponent: Component {
|
||||
}
|
||||
} else if badgePosition < 0.15 {
|
||||
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)
|
||||
|
||||
// self.badgeMaskArrowView.isHidden = component.isPremiumDisabled
|
||||
|
||||
progressTransition.setShapeLayerPath(layer: self.badgeShapeLayer, path: generateBadgePath(rectSize: badgeSize, tailPosition: component.isPremiumDisabled ? nil : 0.0).cgPath)
|
||||
|
||||
if let _ = self.badgeView.layer.animation(forKey: "appearance1") {
|
||||
|
||||
} else {
|
||||
@ -665,10 +675,8 @@ public class PremiumLimitDisplayComponent: Component {
|
||||
}
|
||||
} else {
|
||||
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)
|
||||
|
||||
// self.badgeMaskArrowView.isHidden = component.isPremiumDisabled
|
||||
|
||||
progressTransition.setShapeLayerPath(layer: self.badgeShapeLayer, path: generateBadgePath(rectSize: badgeSize, tailPosition: component.isPremiumDisabled ? nil : 0.5).cgPath)
|
||||
|
||||
if let _ = self.badgeView.layer.animation(forKey: "appearance1") {
|
||||
|
||||
} 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.badgeCountLabel.frame = CGRect(x: badgeFullSize.width - countWidth - 11.0, y: 10.0, width: countWidth, height: 48.0)
|
||||
self.countMaskView.frame = CGRect(x: 0.0, y: 0.0, width: countWidth, height: 48.0)
|
||||
self.badgeLabelMaskView.frame = CGRect(x: 0.0, y: 0.0, width: 100.0, height: 36.0)
|
||||
|
||||
if component.isPremiumDisabled {
|
||||
if !self.didPlayAppearanceAnimation {
|
||||
@ -690,7 +697,8 @@ public class PremiumLimitDisplayComponent: Component {
|
||||
|
||||
self.badgeView.alpha = 1.0
|
||||
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 {
|
||||
@ -699,13 +707,14 @@ public class PremiumLimitDisplayComponent: Component {
|
||||
if component.badgePosition < 0.1 {
|
||||
self.badgeView.alpha = 1.0
|
||||
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 {
|
||||
self.playAppearanceAnimation(component: component, availableSize: size)
|
||||
self.playAppearanceAnimation(component: component, badgeFullSize: badgeFullSize)
|
||||
}
|
||||
} 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 dismiss: () -> 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.subject = subject
|
||||
self.count = count
|
||||
@ -809,6 +819,7 @@ private final class LimitSheetContent: CombinedComponent {
|
||||
self.action = action
|
||||
self.dismiss = dismiss
|
||||
self.openPeer = openPeer
|
||||
self.openStats = openStats
|
||||
}
|
||||
|
||||
static func ==(lhs: LimitSheetContent, rhs: LimitSheetContent) -> Bool {
|
||||
@ -878,6 +889,7 @@ private final class LimitSheetContent: CombinedComponent {
|
||||
let linkButton = Child(SolidRoundedButtonComponent.self)
|
||||
let button = Child(SolidRoundedButtonComponent.self)
|
||||
let peerShortcut = Child(Button.self)
|
||||
let statsButton = Child(Button.self)
|
||||
|
||||
return { context in
|
||||
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)) : ""
|
||||
premiumValue = component.count != 4 ? dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator)) : ""
|
||||
badgePosition = component.count == 4 ? 1.0 : 0.5
|
||||
badgeGraphPosition = badgePosition
|
||||
badgeGraphPosition = 0.5
|
||||
titleText = strings.Premium_FileTooLarge
|
||||
|
||||
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,
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(statsButton
|
||||
.position(CGPoint(x: 31.0, y: 28.0))
|
||||
)
|
||||
}
|
||||
|
||||
if boosted && state.boosted != boosted {
|
||||
@ -1528,14 +1565,16 @@ private final class LimitSheetComponent: CombinedComponent {
|
||||
let cancel: () -> Void
|
||||
let action: () -> Bool
|
||||
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.subject = subject
|
||||
self.count = count
|
||||
self.cancel = cancel
|
||||
self.action = action
|
||||
self.openPeer = openPeer
|
||||
self.openStats = openStats
|
||||
}
|
||||
|
||||
static func ==(lhs: LimitSheetComponent, rhs: LimitSheetComponent) -> Bool {
|
||||
@ -1545,6 +1584,9 @@ private final class LimitSheetComponent: CombinedComponent {
|
||||
if lhs.subject != rhs.subject {
|
||||
return false
|
||||
}
|
||||
if lhs.count != rhs.count {
|
||||
return false
|
||||
}
|
||||
|
||||
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),
|
||||
animateOut: animateOut
|
||||
@ -1636,14 +1679,14 @@ public class PremiumLimitScreen: ViewControllerComponentContainer {
|
||||
|
||||
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.openPeer = openPeer
|
||||
|
||||
var actionImpl: (() -> Bool)?
|
||||
super.init(context: context, component: LimitSheetComponent(context: context, subject: subject, count: count, cancel: {}, action: {
|
||||
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
|
||||
|
||||
@ -1673,7 +1716,7 @@ public class PremiumLimitScreen: ViewControllerComponentContainer {
|
||||
public func updateSubject(_ subject: Subject, count: Int32) {
|
||||
let component = LimitSheetComponent(context: self.context, subject: subject, count: count, cancel: {}, action: {
|
||||
return true
|
||||
}, openPeer: self.openPeer)
|
||||
}, openPeer: self.openPeer, openStats: nil)
|
||||
self.updateComponent(component: AnyComponent(component), transition: .easeInOut(duration: 0.2))
|
||||
|
||||
self.hapticFeedback.impact()
|
||||
@ -1750,7 +1793,7 @@ private final class PeerShortcutComponent: Component {
|
||||
)
|
||||
),
|
||||
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)
|
||||
|
@ -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)
|
||||
})
|
||||
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: {
|
||||
arguments.openPeer(peer)
|
||||
}, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in })
|
||||
@ -500,13 +500,13 @@ private enum StatsEntry: ItemListNodeEntry {
|
||||
}
|
||||
}
|
||||
|
||||
public enum ChannelStatsSection {
|
||||
case stats
|
||||
case boosts
|
||||
}
|
||||
|
||||
private struct ChannelStatsControllerState: Equatable {
|
||||
enum Section {
|
||||
case stats
|
||||
case boosts
|
||||
}
|
||||
|
||||
let section: Section
|
||||
let section: ChannelStatsSection
|
||||
let boostersExpanded: Bool
|
||||
|
||||
init() {
|
||||
@ -514,7 +514,7 @@ private struct ChannelStatsControllerState: Equatable {
|
||||
self.boostersExpanded = false
|
||||
}
|
||||
|
||||
init(section: Section, boostersExpanded: Bool) {
|
||||
init(section: ChannelStatsSection, boostersExpanded: Bool) {
|
||||
self.section = section
|
||||
self.boostersExpanded = boostersExpanded
|
||||
}
|
||||
@ -529,7 +529,7 @@ private struct ChannelStatsControllerState: Equatable {
|
||||
return true
|
||||
}
|
||||
|
||||
func withUpdatedSection(_ section: Section) -> ChannelStatsControllerState {
|
||||
func withUpdatedSection(_ section: ChannelStatsSection) -> ChannelStatsControllerState {
|
||||
return ChannelStatsControllerState(section: section, boostersExpanded: self.boostersExpanded)
|
||||
}
|
||||
|
||||
@ -682,9 +682,9 @@ private func channelStatsControllerEntries(state: ChannelStatsControllerState, p
|
||||
return entries
|
||||
}
|
||||
|
||||
public func channelStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: PeerId, statsDatacenterId: Int32?) -> ViewController {
|
||||
let statePromise = ValuePromise(ChannelStatsControllerState(), ignoreRepeated: true)
|
||||
let stateValue = Atomic(value: ChannelStatsControllerState())
|
||||
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(section: section, boostersExpanded: false), ignoreRepeated: true)
|
||||
let stateValue = Atomic(value: ChannelStatsControllerState(section: section, boostersExpanded: false))
|
||||
let updateState: ((ChannelStatsControllerState) -> ChannelStatsControllerState) -> Void = { f in
|
||||
statePromise.set(stateValue.modify { f($0) })
|
||||
}
|
||||
@ -717,7 +717,12 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
|
||||
})
|
||||
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)
|
||||
|
||||
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 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()))
|
||||
|
||||
|
@ -1138,34 +1138,33 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
|
||||
return
|
||||
}
|
||||
|
||||
let context = component.context
|
||||
let navigationController = controller.navigationController as? NavigationController
|
||||
|
||||
switch error {
|
||||
case .generic:
|
||||
controller.dismiss()
|
||||
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 {
|
||||
return true
|
||||
}
|
||||
navigationController.pushViewController(PremiumIntroScreen(context: context, source: .folders))
|
||||
navigationController.pushViewController(PremiumIntroScreen(context: component.context, source: .folders))
|
||||
return true
|
||||
})
|
||||
controller.push(limitController)
|
||||
controller.dismiss()
|
||||
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 {
|
||||
return true
|
||||
}
|
||||
navigationController.pushViewController(PremiumIntroScreen(context: context, source: .membershipInSharedFolders))
|
||||
navigationController.pushViewController(PremiumIntroScreen(context: component.context, source: .membershipInSharedFolders))
|
||||
return true
|
||||
})
|
||||
controller.push(limitController)
|
||||
controller.dismiss()
|
||||
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 {
|
||||
return true
|
||||
}
|
||||
@ -1175,7 +1174,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
|
||||
controller.push(limitController)
|
||||
controller.dismiss()
|
||||
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 {
|
||||
return true
|
||||
}
|
||||
@ -1434,7 +1433,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
|
||||
|
||||
return
|
||||
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 {
|
||||
return true
|
||||
}
|
||||
@ -1446,7 +1445,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
|
||||
|
||||
return
|
||||
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 {
|
||||
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))
|
||||
}
|
||||
|
||||
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 {
|
||||
return
|
||||
}
|
||||
@ -6657,7 +6657,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
if let channel = peer as? TelegramChannel, case .group = channel.info {
|
||||
statsController = groupStatsController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: peer.id, statsDatacenterId: statsDatacenterId)
|
||||
} 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)
|
||||
}
|
||||
@ -8310,13 +8310,17 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
if let previousController = navigationController.viewControllers.last as? ShareWithPeersScreen {
|
||||
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)"
|
||||
|
||||
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)
|
||||
}
|
||||
return true
|
||||
}, openStats: { [weak self] in
|
||||
if let self {
|
||||
self.openStats(boosts: true, boostStatus: status)
|
||||
}
|
||||
})
|
||||
navigationController.pushViewController(controller)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user