diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyICloudFilePicker.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyICloudFilePicker.swift index faf72e6d2b..1b6bc2022a 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyICloudFilePicker.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyICloudFilePicker.swift @@ -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?() } diff --git a/submodules/PremiumUI/Sources/BadgeLabelView.swift b/submodules/PremiumUI/Sources/BadgeLabelView.swift new file mode 100644 index 0000000000..af32cf3bce --- /dev/null +++ b/submodules/PremiumUI/Sources/BadgeLabelView.swift @@ -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) + } +} diff --git a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift index 870b2d7053..565994cc29 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift @@ -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) diff --git a/submodules/PremiumUI/Sources/ReactionsCarouselComponent.swift b/submodules/PremiumUI/Sources/ReactionsCarouselComponent.swift deleted file mode 100644 index 876e0c458c..0000000000 --- a/submodules/PremiumUI/Sources/ReactionsCarouselComponent.swift +++ /dev/null @@ -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, 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, 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() - - 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() - 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) { - 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) - } - } -} diff --git a/submodules/PremiumUI/Sources/RollingCountLabel.swift b/submodules/PremiumUI/Sources/RollingCountLabel.swift deleted file mode 100644 index 8bb1b143f7..0000000000 --- a/submodules/PremiumUI/Sources/RollingCountLabel.swift +++ /dev/null @@ -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 - } - } -} diff --git a/submodules/StatisticsUI/Sources/ChannelStatsController.swift b/submodules/StatisticsUI/Sources/ChannelStatsController.swift index 73cc9d19a0..e48c0c1fcf 100644 --- a/submodules/StatisticsUI/Sources/ChannelStatsController.swift +++ b/submodules/StatisticsUI/Sources/ChannelStatsController.swift @@ -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)? = 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)? = 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 + 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)? diff --git a/submodules/StatisticsUI/Sources/StatsMessageItem.swift b/submodules/StatisticsUI/Sources/StatsMessageItem.swift index b76ff08fa2..4987f30446 100644 --- a/submodules/StatisticsUI/Sources/StatsMessageItem.swift +++ b/submodules/StatisticsUI/Sources/StatsMessageItem.swift @@ -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())) diff --git a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift index 447d8f4d5f..976c1e491b 100644 --- a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift +++ b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift @@ -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 } diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Stats.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Stats.imageset/Contents.json new file mode 100644 index 0000000000..af15589a7c --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Stats.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Stats.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Stats.imageset/Stats.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Stats.imageset/Stats.pdf new file mode 100644 index 0000000000..f3c377a840 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Stats.imageset/Stats.pdf @@ -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 \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index a64936ef13..bb9ed3f17f 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -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) }