Various improvements

This commit is contained in:
Ilya Laktyushin 2023-09-19 01:16:45 +04:00
parent 8b417a3d9c
commit 5be0000735
11 changed files with 426 additions and 857 deletions

View File

@ -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?()
}

View 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)
}
}

View File

@ -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)

View File

@ -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)
}
}
}

View File

@ -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
}
}
}

View File

@ -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)?

View File

@ -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()))

View File

@ -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
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "Stats.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View 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

View File

@ -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)
}