Charts improvements

This commit is contained in:
Ilya Laktyushin 2020-03-21 03:19:05 +04:00
parent c5d39df2b3
commit d6f0a02fc7
59 changed files with 4448 additions and 4271 deletions

Binary file not shown.

View File

@ -5368,6 +5368,7 @@ Any member of this group will be able to see messages in the channel.";
"Stats.InteractionsTitle" = "INTERACTIONS";
"Stats.InstantViewInteractionsTitle" = "INSTANT VIEW INTERACTIONS";
"Stats.ViewsBySourceTitle" = "VIEWS BY SOURCE";
"Stats.ViewsByHoursTitle" = "VIEWS BY HOURS";
"Stats.FollowersBySourceTitle" = "FOLLOWERS BY SOURCE";
"Stats.LanguagesTitle" = "LANGUAGES";
"Stats.PostsTitle" = "RECENT POSTS";
@ -5386,6 +5387,9 @@ Any member of this group will be able to see messages in the channel.";
"Stats.MessageForwards_many" = "%@ forwards";
"Stats.MessageForwards_any" = "%@ forwards";
"Stats.LoadingTitle" = "Preparing stats";
"Stats.LoadingText" = "Please wait a few moments while\nwe generate your stats";
"InstantPage.Views_0" = "%@ views";
"InstantPage.Views_1" = "%@ view";
"InstantPage.Views_2" = "%@ views";

View File

@ -38,7 +38,7 @@ open class OverlayMediaItemNode: ASDisplayNode {
open func setShouldAcquireContext(_ value: Bool) {
}
open func preferredSizeForOverlayDisplay() -> CGSize {
open func preferredSizeForOverlayDisplay(boundingSize: CGSize) -> CGSize {
return CGSize(width: 50.0, height: 50.0)
}

View File

@ -1881,8 +1881,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
}
private func foldLineBreaks(_ text: String) -> String {
var lines = text.split { $0.isNewline }
var startedBothLines = false
let lines = text.split { $0.isNewline }
var result = ""
for line in lines {
if line.isEmpty {

View File

@ -67,7 +67,15 @@ public extension ChartsCollection {
}
switch type {
case .axix:
axixValuesToSetup = try column.dropFirst().map { Date(timeIntervalSince1970: try Convert.doubleFrom($0) / 1000) }
axixValuesToSetup = try column.dropFirst().map { value in
let numberValue = try Convert.doubleFrom(value)
if numberValue < 24.0 {
return Date(timeIntervalSince1970: numberValue)
} else {
return Date(timeIntervalSince1970: numberValue / 1000)
}
}
case .chart, .bar, .area, .step:
guard let colorString = colors[columnId],
let color = GColor(hexString: colorString) else {

View File

@ -245,8 +245,14 @@ class GeneralChartComponentController: ChartThemeContainer {
var labels: [LinesChartLabel] = []
for index in stride(from: chartsCollection.axisValues.count - 1, to: -1, by: -strideInterval).reversed() {
let date = chartsCollection.axisValues[index]
labels.append(LinesChartLabel(value: CGFloat(date.timeIntervalSince1970),
text: scaleType.dateFormatter.string(from: date)))
let timestamp = date.timeIntervalSince1970
if timestamp <= 24 {
labels.append(LinesChartLabel(value: CGFloat(timestamp),
text: "\(Int(timestamp)):00"))
} else {
labels.append(LinesChartLabel(value: CGFloat(timestamp),
text: scaleType.dateFormatter.string(from: date)))
}
}
prevoiusHorizontalStrideInterval = strideInterval
horizontalScalesRenderer.setup(labels: labels, animated: animated)
@ -318,8 +324,9 @@ class GeneralChartComponentController: ChartThemeContainer {
tapAction: { [weak self] in
self?.zoomInOnDateClosure?(closestDate) },
hideAction: { [weak self] in
self?.setDetailsChartVisibleClosure?(false, true)
})
})
return viewModel
}

View File

@ -157,8 +157,14 @@ public class BaseLinesChartController: BaseChartController {
var labels: [LinesChartLabel] = []
for index in stride(from: initialChartsCollection.axisValues.count - 1, to: -1, by: -strideInterval).reversed() {
let date = initialChartsCollection.axisValues[index]
labels.append(LinesChartLabel(value: CGFloat(date.timeIntervalSince1970),
text: scaleType.dateFormatter.string(from: date)))
let timestamp = date.timeIntervalSince1970
if timestamp <= 24 {
labels.append(LinesChartLabel(value: CGFloat(timestamp),
text: "\(Int(timestamp)):00"))
} else {
labels.append(LinesChartLabel(value: CGFloat(timestamp),
text: scaleType.dateFormatter.string(from: date)))
}
}
return (strideInterval, labels)
}
@ -179,14 +185,14 @@ public class BaseLinesChartController: BaseChartController {
}
func verticalLimitsLabels(verticalRange: ClosedRange<CGFloat>) -> (ClosedRange<CGFloat>, [LinesChartLabel]) {
let ditance = verticalRange.distance
let distance = verticalRange.distance
let chartHeight = chartFrame().height
guard ditance > 0, chartHeight > 0 else { return (BaseConstants.defaultRange, []) }
guard distance > 0, chartHeight > 0 else { return (BaseConstants.defaultRange, []) }
let approximateNumberOfChartValues = (chartHeight / BaseConstants.minimumAxisYLabelsDistance)
var numberOfOffsetsPerItem = ditance / approximateNumberOfChartValues
var numberOfOffsetsPerItem = distance / approximateNumberOfChartValues
var multiplier: CGFloat = 1.0
while numberOfOffsetsPerItem > 10 {
numberOfOffsetsPerItem /= 10

View File

@ -237,7 +237,7 @@ public class TwoAxisLinesChartController: BaseLinesChartController {
let chartHeight = chartFrame().height
let approximateNumberOfChartValues = (chartHeight / BaseConstants.minimumAxisYLabelsDistance)
var dividorsAndMultiplers: [(startValue: CGFloat, base: CGFloat, count: Int, maximumNumberOfDecimals: Int)] = graphControllers.enumerated().map { arg in
let dividorsAndMultiplers: [(startValue: CGFloat, base: CGFloat, count: Int, maximumNumberOfDecimals: Int)] = graphControllers.enumerated().map { arg in
let (index, controller) = arg
let verticalRange = LinesChartRenderer.LineData.verticalRange(lines: controller.chartLines,
calculatingRange: horizontalRange,

View File

@ -138,7 +138,7 @@ class PercentChartComponentController: GeneralChartComponentController {
let values: [ChartDetailsViewModel.Value] = chartsCollection.chartValues.enumerated().map { arg in
let (index, component) = arg
return ChartDetailsViewModel.Value(prefix: PercentConstants.percentValueFormatter.string(from: component.values[pointIndex] / total * 100),
return ChartDetailsViewModel.Value(prefix: total > 0 ? PercentConstants.percentValueFormatter.string(from: component.values[pointIndex] / total * 100) : "0%",
title: component.name,
value: BaseConstants.detailsNumberFormatter.string(from: component.values[pointIndex]),
color: component.color,
@ -151,16 +151,16 @@ class PercentChartComponentController: GeneralChartComponentController {
dateString = BaseConstants.headerMediumRangeFormatter.string(from: closestDate)
}
let viewModel = ChartDetailsViewModel(title: dateString,
showArrow: self.isZoomable && !self.isZoomed,
showArrow: total > 0 && self.isZoomable && !self.isZoomed,
showPrefixes: true,
values: values,
totalValue: nil,
tapAction: { [weak self] in
self?.hideDetailsView(animated: true)
self?.zoomInOnDateClosure?(closestDate) },
hideAction: { [weak self] in
self?.hideDetailsView(animated: true)
})
hideAction: { [weak self] in
self?.hideDetailsView(animated: true)
})
return viewModel
}

View File

@ -95,9 +95,11 @@ public class PercentPieChartController: BaseChartController {
totalVerticalRange: BaseConstants.defaultRange)
switchToChart(chartsCollection: percentController.chartsCollection, isZoomed: false, animated: false)
TimeInterval.animationDurationMultipler = 0.00001
self.didTapZoomIn(date: Date(timeIntervalSinceReferenceDate: 603849600.0), animated: false)
TimeInterval.animationDurationMultipler = 1
if let lastDate = initialChartsCollection.axisValues.last {
TimeInterval.animationDurationMultipler = 0.00001
self.didTapZoomIn(date: lastDate, animated: false)
TimeInterval.animationDurationMultipler = 1.0
}
}
func switchToChart(chartsCollection: ChartsCollection, isZoomed: Bool, animated: Bool) {

View File

@ -17,25 +17,25 @@ class BarsComponentController: GeneralChartComponentController {
let mainBarsRenderer: BarChartRenderer
let horizontalScalesRenderer: HorizontalScalesRenderer
let verticalScalesRenderer: VerticalScalesRenderer
let secondVerticalScalesRenderer: VerticalScalesRenderer?
let previewBarsChartRenderer: BarChartRenderer
private(set) var barsWidth: CGFloat = 1
private (set) var chartBars: BarChartRenderer.BarsData = .blank
private var step: Bool
init(isZoomed: Bool,
mainBarsRenderer: BarChartRenderer,
horizontalScalesRenderer: HorizontalScalesRenderer,
verticalScalesRenderer: VerticalScalesRenderer,
previewBarsChartRenderer: BarChartRenderer) {
previewBarsChartRenderer: BarChartRenderer,
step: Bool = false) {
self.mainBarsRenderer = mainBarsRenderer
self.horizontalScalesRenderer = horizontalScalesRenderer
self.verticalScalesRenderer = verticalScalesRenderer
self.previewBarsChartRenderer = previewBarsChartRenderer
self.secondVerticalScalesRenderer = VerticalScalesRenderer()
self.secondVerticalScalesRenderer?.isRightAligned = true
self.step = step
self.mainBarsRenderer.optimizationLevel = BaseConstants.barsChartOptimizationLevel
self.previewBarsChartRenderer.optimizationLevel = BaseConstants.barsChartOptimizationLevel
@ -44,7 +44,7 @@ class BarsComponentController: GeneralChartComponentController {
}
override func initialize(chartsCollection: ChartsCollection, initialDate: Date, totalHorizontalRange _: ClosedRange<CGFloat>, totalVerticalRange _: ClosedRange<CGFloat>) {
let (width, chartBars, totalHorizontalRange, totalVerticalRange) = BarChartRenderer.BarsData.initialComponents(chartsCollection: chartsCollection)
let (width, chartBars, totalHorizontalRange, totalVerticalRange) = BarChartRenderer.BarsData.initialComponents(chartsCollection: chartsCollection, separate: self.step)
self.chartBars = chartBars
self.barsWidth = width
@ -76,10 +76,10 @@ class BarsComponentController: GeneralChartComponentController {
mainBarsRenderer.bars = self.chartBars
previewBarsChartRenderer.bars = self.chartBars
previewBarsChartRenderer.setup(verticalRange: 0...117278, animated: animated)
previewBarsChartRenderer.setup(verticalRange: totalVerticalRange, animated: animated)
previewBarsChartRenderer.setup(horizontalRange: totalHorizontalRange, animated: animated)
setupMainChart(verticalRange: 0...117278, animated: animated)
setupMainChart(verticalRange: initialVerticalRange, animated: animated)
setupMainChart(horizontalRange: initialHorizontalRange, animated: animated)
updateChartVerticalRanges(horizontalRange: initialHorizontalRange, animated: animated)
@ -119,16 +119,12 @@ class BarsComponentController: GeneralChartComponentController {
horizontalScalesRenderer.setVisible(visible, animated: animated)
verticalScalesRenderer.setVisible(visible, animated: animated)
previewBarsChartRenderer.setVisible(visible, animated: animated)
secondVerticalScalesRenderer?.setVisible(visible, animated: animated)
}
func setupMainChart(horizontalRange: ClosedRange<CGFloat>, animated: Bool) {
mainBarsRenderer.setup(horizontalRange: horizontalRange, animated: animated)
horizontalScalesRenderer.setup(horizontalRange: horizontalRange, animated: animated)
verticalScalesRenderer.setup(horizontalRange: horizontalRange, animated: animated)
secondVerticalScalesRenderer?.setup(horizontalRange: horizontalRange, animated: animated)
}
var visibleBars: BarChartRenderer.BarsData {
@ -142,6 +138,7 @@ class BarsComponentController: GeneralChartComponentController {
func updateChartVerticalRanges(horizontalRange: ClosedRange<CGFloat>, animated: Bool) {
if let range = BarChartRenderer.BarsData.verticalRange(bars: visibleBars,
separate: self.step,
calculatingRange: horizontalRange,
addBounds: true) {
let (range, labels) = verticalLimitsLabels(verticalRange: range)
@ -151,31 +148,19 @@ class BarsComponentController: GeneralChartComponentController {
verticalScalesRenderer.setVisible(true, animated: animated)
setupMainChart(verticalRange: range, animated: animated)
let (secondRange, secondLabels) = verticalLimitsLabels(verticalRange: range)
if secondVerticalScalesRenderer?.verticalRange.end != secondRange {
secondVerticalScalesRenderer?.setup(verticalLimitsLabels: secondLabels, animated: animated)
}
secondVerticalScalesRenderer?.setVisible(true, animated: animated)
} else {
verticalScalesRenderer.setVisible(false, animated: animated)
secondVerticalScalesRenderer?.setVisible(false, animated: animated)
}
if let range = BarChartRenderer.BarsData.verticalRange(bars: visibleBars) {
if let range = BarChartRenderer.BarsData.verticalRange(bars: visibleBars, separate: self.step) {
previewBarsChartRenderer.setup(verticalRange: range, animated: animated)
}
}
func setupMainChart(verticalRange: ClosedRange<CGFloat>, animated: Bool) {
var verticalRange = verticalRange
if verticalRange.upperBound > 2000 && verticalRange.upperBound < 10000 {
verticalRange = 0...117278
}
mainBarsRenderer.setup(verticalRange: verticalRange, animated: animated)
horizontalScalesRenderer.setup(verticalRange: verticalRange, animated: animated)
verticalScalesRenderer.setup(verticalRange: verticalRange, animated: animated)
secondVerticalScalesRenderer?.setup(verticalRange: verticalRange, animated: animated)
}
public override func updateChartsVisibility(visibility: [Bool], animated: Bool) {
@ -198,12 +183,15 @@ class BarsComponentController: GeneralChartComponentController {
var viewModel = super.chartDetailsViewModel(closestDate: closestDate, pointIndex: pointIndex)
let visibleChartValues = self.visibleChartValues
let totalSumm: CGFloat = visibleChartValues.map { CGFloat($0.values[pointIndex]) }.reduce(0, +)
viewModel.totalValue = ChartDetailsViewModel.Value(prefix: nil,
title: "Total",
value: BaseConstants.detailsNumberFormatter.string(from: totalSumm),
color: .white,
visible: visibleChartValues.count > 1)
if !self.step {
viewModel.totalValue = ChartDetailsViewModel.Value(prefix: nil,
title: "Total",
value: BaseConstants.detailsNumberFormatter.string(from: totalSumm),
color: .white,
visible: visibleChartValues.count > 1)
} else {
viewModel.title = ""
}
return viewModel
}
@ -235,10 +223,6 @@ class BarsComponentController: GeneralChartComponentController {
verticalScalesRenderer.horizontalLinesColor = theme.barChartStrongLinesColor
mainBarsRenderer.update(backgroundColor: theme.chartBackgroundColor, animated: false)
previewBarsChartRenderer.update(backgroundColor: theme.chartBackgroundColor, animated: false)
secondVerticalScalesRenderer?.labelsColor = theme.chartLabelsColor
secondVerticalScalesRenderer?.axisXColor = theme.barChartStrongLinesColor
secondVerticalScalesRenderer?.horizontalLinesColor = theme.barChartStrongLinesColor
}
override func updateChartRangeTitle(animated: Bool) {

View File

@ -1,5 +1,5 @@
//
// DailyBarsChartController.swift
// StackedBarsChartController.swift
// GraphTest
//
// Created by Andrei Salavei on 4/7/19.
@ -17,6 +17,12 @@ public class StepBarsChartController: BaseChartController {
let barsController: BarsComponentController
let zoomedBarsController: BarsComponentController
override public var isZoomable: Bool {
didSet {
barsController.isZoomable = self.isZoomable
}
}
override public init(chartsCollection: ChartsCollection) {
let horizontalScalesRenderer = HorizontalScalesRenderer()
let verticalScalesRenderer = VerticalScalesRenderer()
@ -24,13 +30,13 @@ public class StepBarsChartController: BaseChartController {
mainBarsRenderer: BarChartRenderer(step: true),
horizontalScalesRenderer: horizontalScalesRenderer,
verticalScalesRenderer: verticalScalesRenderer,
previewBarsChartRenderer: BarChartRenderer())
previewBarsChartRenderer: BarChartRenderer(step: true), step: true)
zoomedBarsController = BarsComponentController(isZoomed: true,
mainBarsRenderer: BarChartRenderer(step: true),
mainBarsRenderer: BarChartRenderer(),
horizontalScalesRenderer: horizontalScalesRenderer,
verticalScalesRenderer: verticalScalesRenderer,
previewBarsChartRenderer: BarChartRenderer())
previewBarsChartRenderer: BarChartRenderer(), step: true)
super.init(chartsCollection: chartsCollection)
[barsController, zoomedBarsController].forEach { controller in
@ -40,7 +46,7 @@ public class StepBarsChartController: BaseChartController {
self.didTapZoomIn(date: date)
}
controller.setChartTitleClosure = { [unowned self] (title, animated) in
self.setChartTitleClosure?(title, animated)
self.setChartTitleClosure?("", animated)
}
controller.setDetailsViewPositionClosure = { [unowned self] (position) in
self.setDetailsViewPositionClosure?(position)
@ -60,12 +66,18 @@ public class StepBarsChartController: BaseChartController {
}
}
public var hourly: Bool = false
public convenience init(chartsCollection: ChartsCollection, hourly: Bool) {
self.init(chartsCollection: chartsCollection)
self.hourly = hourly
}
public override var mainChartRenderers: [ChartViewRenderer] {
return [barsController.mainBarsRenderer,
zoomedBarsController.mainBarsRenderer,
barsController.horizontalScalesRenderer,
barsController.verticalScalesRenderer,
barsController.secondVerticalScalesRenderer!
// performanceRenderer
]
}
@ -90,57 +102,65 @@ public class StepBarsChartController: BaseChartController {
TimeInterval.setDefaultSuration(.osXDuration)
}
}
super.isZoomed = isZoomed
if isZoomed {
let toHorizontalRange = zoomedBarsController.initialHorizontalRange
let destinationHorizontalRange = (toHorizontalRange.lowerBound - barsController.barsWidth)...(toHorizontalRange.upperBound - barsController.barsWidth)
// let initialChartVerticalRange = lineProportionAnimationRange()
// let visibleVerticalRange = BarChartRenderer.BarsData.verticalRange(bars: zoomedBarsController.visibleBars,
// calculatingRange: zoomedBarsController.initialHorizontalRange) ?? BaseConstants.defaultRange
zoomedBarsController.mainBarsRenderer.setup(verticalRange: 0...117278, animated: false)
zoomedBarsController.setupMainChart(horizontalRange: barsController.currentHorizontalMainChartRange, animated: false)
let verticalVisibleRange = barsController.currentVerticalMainChartRange
let initialVerticalRange = verticalVisibleRange.lowerBound...(verticalVisibleRange.upperBound + verticalVisibleRange.distance * 10)
zoomedBarsController.mainBarsRenderer.setup(horizontalRange: barsController.currentHorizontalMainChartRange, animated: false)
zoomedBarsController.previewBarsChartRenderer.setup(horizontalRange: barsController.currentPreviewHorizontalRange, animated: false)
// zoomedBarsController.mainLinesRenderer.setup(verticalRange: initialChartVerticalRange, animated: false)
// zoomedBarsController.previewLinesChartRenderer.setup(verticalRange: initialChartVerticalRange, animated: false)
zoomedBarsController.mainBarsRenderer.setVisible(false, animated: false)
zoomedBarsController.previewBarsChartRenderer.setVisible(false, animated: false)
zoomedBarsController.mainBarsRenderer.setup(verticalRange: initialVerticalRange, animated: false)
zoomedBarsController.previewBarsChartRenderer.setup(verticalRange: initialVerticalRange, animated: false)
zoomedBarsController.mainBarsRenderer.setVisible(true, animated: false)
zoomedBarsController.previewBarsChartRenderer.setVisible(true, animated: false)
barsController.setupMainChart(horizontalRange: destinationHorizontalRange, animated: animated)
barsController.previewBarsChartRenderer.setup(horizontalRange: zoomedBarsController.totalHorizontalRange, animated: animated)
barsController.mainBarsRenderer.setVisible(false, animated: animated)
barsController.previewBarsChartRenderer.setVisible(false, animated: animated)
zoomedBarsController.willAppear(animated: animated)
barsController.willDisappear(animated: animated)
zoomedBarsController.updateChartsVisibility(visibility: zoomedBarsController.chartBars.components.map { _ in true }, animated: false)
zoomedBarsController.updateChartsVisibility(visibility: barsController.chartVisibility, animated: false)
zoomedBarsController.mainBarsRenderer.setup(verticalRange: zoomedBarsController.currentVerticalMainChartRange, animated: animated, timeFunction: .easeOut)
zoomedBarsController.previewBarsChartRenderer.setup(verticalRange: zoomedBarsController.currentPreviewVerticalRange, animated: animated, timeFunction: .easeOut)
} else {
if !zoomedBarsController.chartsCollection.isBlank {
barsController.hideDetailsView(animated: false)
barsController.chartVisibility = zoomedBarsController.chartVisibility
let visibleVerticalRange = BarChartRenderer.BarsData.verticalRange(bars: barsController.visibleBars,
separate: true,
calculatingRange: barsController.initialHorizontalRange) ?? BaseConstants.defaultRange
barsController.mainBarsRenderer.setup(verticalRange: visibleVerticalRange, animated: false)
let toHorizontalRange = barsController.initialHorizontalRange
// let destinationChartVerticalRange = lineProportionAnimationRange()
let verticalVisibleRange = barsController.initialVerticalRange
let targetVerticalRange = verticalVisibleRange.lowerBound...(verticalVisibleRange.upperBound + verticalVisibleRange.distance * 10)
zoomedBarsController.setupMainChart(horizontalRange: toHorizontalRange, animated: animated)
// zoomedBarsController.mainLinesRenderer.setup(verticalRange: destinationChartVerticalRange, animated: animated)
// zoomedBarsController.previewLinesChartRenderer.setup(verticalRange: destinationChartVerticalRange, animated: animated)
zoomedBarsController.mainBarsRenderer.setup(verticalRange: targetVerticalRange, animated: animated, timeFunction: .easeIn)
zoomedBarsController.previewBarsChartRenderer.setup(verticalRange: targetVerticalRange, animated: animated, timeFunction: .easeIn)
zoomedBarsController.previewBarsChartRenderer.setup(horizontalRange: barsController.totalHorizontalRange, animated: animated)
zoomedBarsController.mainBarsRenderer.setVisible(false, animated: animated)
zoomedBarsController.previewBarsChartRenderer.setVisible(false, animated: animated)
DispatchQueue.main.asyncAfter(deadline: .now() + .defaultDuration) { [weak self] in
self?.zoomedBarsController.mainBarsRenderer.setVisible(false, animated: false)
self?.zoomedBarsController.previewBarsChartRenderer.setVisible(false, animated: false)
}
}
barsController.willAppear(animated: animated)
zoomedBarsController.willDisappear(animated: animated)
if !zoomedBarsController.chartsCollection.isBlank {
barsController.updateChartsVisibility(visibility: zoomedBarsController.chartVisibility, animated: false)
}
}
self.setBackButtonVisibilityClosure?(isZoomed, animated)
self.refreshChartToolsClosure?(animated)
}
public override func updateChartsVisibility(visibility: [Bool], animated: Bool) {
@ -166,13 +186,12 @@ public class StepBarsChartController: BaseChartController {
public override var actualChartsCollection: ChartsCollection {
let collection = isZoomed ? zoomedBarsController.chartsCollection : barsController.chartsCollection
if collection.isBlank {
return self.initialChartsCollection
}
return collection
}
public override func chartInteractionDidBegin(point: CGPoint) {
if isZoomed {
zoomedBarsController.chartInteractionDidBegin(point: point)
@ -189,6 +208,10 @@ public class StepBarsChartController: BaseChartController {
}
}
public override var drawChartVisibity: Bool {
return true
}
public override var currentChartHorizontalRangeFraction: ClosedRange<CGFloat> {
if isZoomed {
return zoomedBarsController.currentChartHorizontalRangeFraction
@ -221,15 +244,6 @@ public class StepBarsChartController: BaseChartController {
})
}
// func lineProportionAnimationRange() -> ClosedRange<CGFloat> {
// let visibleLines = self.barsController.chartVisibility.enumerated().compactMap { $0.element ? self.zoomedBarsController.chartLines[$0.offset] : nil }
// let linesRange = LinesChartRenderer.LineData.verticalRange(lines: visibleLines) ?? BaseConstants.defaultRange
// let barsRange = BarChartRenderer.BarsData.verticalRange(bars: self.barsController.visibleBars,
// calculatingRange: self.zoomedBarsController.totalHorizontalRange) ?? BaseConstants.defaultRange
// let range = 0...(linesRange.upperBound / barsRange.distance * self.barsController.currentVerticalMainChartRange.distance)
// return range
// }
public override func didTapZoomOut() {
cancelChartInteraction()
switchToChart(chartsCollection: barsController.chartsCollection, isZoomed: false, animated: true)
@ -243,17 +257,10 @@ public class StepBarsChartController: BaseChartController {
}
}
override public func apply(theme: ChartTheme, animated: Bool) {
public override func apply(theme: ChartTheme, animated: Bool) {
super.apply(theme: theme, animated: animated)
zoomedBarsController.apply(theme: theme, animated: animated)
barsController.apply(theme: theme, animated: animated)
}
public override var drawChartVisibity: Bool {
return true
}
}
//TODO: Убрать Performance полоски сверзу чартов (Не забыть)
//TODO: Добавить ховеры на кнопки

View File

@ -1,383 +0,0 @@
import Foundation
#if os(macOS)
import Cocoa
#else
import UIKit
#endif
public class StepBarsChartController2: BaseChartController {
class GraphController {
let mainBarsRenderer: BarChartRenderer
let verticalScalesRenderer = VerticalScalesRenderer()
let lineBulletsRenderer = LineBulletsRenderer()
let previewBarsRenderer: BarChartRenderer
var chartBars: BarChartRenderer.BarsData = .blank
var barsWidth: CGFloat = 1
var totalVerticalRange: ClosedRange<CGFloat> = BaseConstants.defaultRange
init(isZoomed: Bool,
mainBarsRenderer: BarChartRenderer,
previewBarsRenderer: BarChartRenderer) {
self.mainBarsRenderer = mainBarsRenderer
self.previewBarsRenderer = previewBarsRenderer
self.mainBarsRenderer.optimizationLevel = BaseConstants.barsChartOptimizationLevel
self.previewBarsRenderer.optimizationLevel = BaseConstants.barsChartOptimizationLevel
}
}
private var graphControllers: [GraphController] = []
private let horizontalScalesRenderer = HorizontalScalesRenderer()
private let verticalLineRenderer = VerticalLinesRenderer()
var chartVisibility: [Bool] = []
var zoomChartVisibility: [Bool] = []
private let initialChartCollection: ChartsCollection
var initialChartRange: ClosedRange<CGFloat> = BaseConstants.defaultRange
var zoomedChartRange: ClosedRange<CGFloat> = BaseConstants.defaultRange
var totalHorizontalRange: ClosedRange<CGFloat> = BaseConstants.defaultRange
var lastChartInteractionPoint: CGPoint = .zero
var isChartInteractionBegun: Bool = false
override public init(chartsCollection: ChartsCollection) {
self.initialChartCollection = chartsCollection
self.graphControllers = chartsCollection.chartValues.map { _ in GraphController(isZoomed: false, mainBarsRenderer: BarChartRenderer(step: true), previewBarsRenderer: BarChartRenderer(step: true))
}
super.init(chartsCollection: chartsCollection)
self.chartVisibility = Array(repeating: true, count: chartsCollection.chartValues.count)
self.zoomChartVisibility = self.chartVisibility
// self.graphControllers.map({ $0.barsController }).forEach { controller in
// controller.chartFrame = { [unowned self] in self.chartFrame() }
// controller.cartViewBounds = { [unowned self] in self.cartViewBounds() }
// controller.zoomInOnDateClosure = { [unowned self] date in
// self.didTapZoomIn(date: date)
// }
// controller.setChartTitleClosure = { [unowned self] (title, animated) in
// self.setChartTitleClosure?(title, animated)
// }
// controller.setDetailsViewPositionClosure = { [unowned self] (position) in
// self.setDetailsViewPositionClosure?(position)
// }
// controller.setDetailsChartVisibleClosure = { [unowned self] (visible, animated) in
// self.setDetailsChartVisibleClosure?(visible, animated)
// }
// controller.setDetailsViewModel = { [unowned self] (viewModel, animated) in
// self.setDetailsViewModel?(viewModel, animated)
// }
// controller.updatePreviewRangeClosure = { [unowned self] (fraction, animated) in
// self.chartRangeUpdatedClosure?(fraction, animated)
// }
// controller.chartRangePagingClosure = { [unowned self] (isEnabled, pageSize) in
// self.setChartRangePagingEnabled(isEnabled: isEnabled, minimumSelectionSize: pageSize)
// }
// }
}
public override var mainChartRenderers: [ChartViewRenderer] {
var renderers: [ChartViewRenderer] = []
self.graphControllers.forEach { controller in
renderers.append(controller.mainBarsRenderer)
}
renderers.append(self.horizontalScalesRenderer)
self.graphControllers.forEach { controller in
renderers.append(controller.verticalScalesRenderer)
renderers.append(controller.lineBulletsRenderer)
}
renderers.append(self.verticalLineRenderer)
return renderers
}
public override var navigationRenderers: [ChartViewRenderer] {
return graphControllers.map { $0.previewBarsRenderer }
}
public override func initializeChart() {
if let first = initialChartCollection.axisValues.first?.timeIntervalSince1970,
let last = initialChartCollection.axisValues.last?.timeIntervalSince1970 {
initialChartRange = CGFloat(max(first, last - BaseConstants.defaultRangePresetLength))...CGFloat(last)
}
setupChartCollection(chartsCollection: initialChartCollection, animated: false, isZoomed: false)
}
public override func updateChartsVisibility(visibility: [Bool], animated: Bool) {
self.chartVisibility = visibility
self.zoomChartVisibility = visibility
let firstIndex = visibility.firstIndex(where: { $0 })
for (index, isVisible) in visibility.enumerated() {
let graph = graphControllers[index]
for graphIndex in graph.chartBars.components.indices {
graph.mainBarsRenderer.setComponentVisible(isVisible, at: graphIndex, animated: animated)
graph.previewBarsRenderer.setComponentVisible(isVisible, at: graphIndex, animated: animated)
graph.lineBulletsRenderer.setLineVisible(isVisible, at: graphIndex, animated: animated)
}
graph.verticalScalesRenderer.setVisible(isVisible, animated: animated)
if let firstIndex = firstIndex {
graph.verticalScalesRenderer.setHorizontalLinesVisible(index == firstIndex, animated: animated)
}
}
// updateVerticalLimitsAndRange(horizontalRange: currentHorizontalRange, animated: true)
if isChartInteractionBegun {
chartInteractionDidBegin(point: lastChartInteractionPoint)
}
}
private func findClosestDateTo(dateToFind: Date) -> (Date, Int)? {
guard self.initialChartCollection.axisValues.count > 0 else { return nil }
var closestDate = self.initialChartCollection.axisValues[0]
var minIndex = 0
for (index, date) in self.initialChartCollection.axisValues.enumerated() {
if abs(dateToFind.timeIntervalSince(date)) < abs(dateToFind.timeIntervalSince(closestDate)) {
closestDate = date
minIndex = index
}
}
return (closestDate, minIndex)
}
public override func chartInteractionDidBegin(point: CGPoint) {
let horizontalRange = currentHorizontalRange
let chartFrame = self.chartFrame()
guard chartFrame.width > 0 else { return }
let dateToFind = Date(timeIntervalSince1970: TimeInterval(horizontalRange.distance * point.x + horizontalRange.lowerBound))
guard let (closestDate, minIndex) = findClosestDateTo(dateToFind: dateToFind) else { return }
let chartInteractionWasBegin = isChartInteractionBegun
super.chartInteractionDidBegin(point: point)
// for graphController in graphControllers {
// graphController.lineBulletsRenderer.bullets = graphController.chartBars.components.map { component in
// LineBulletsRenderer.Bullet(coordinate: component.values[minIndex], color: component.color)
// }
// graphController.lineBulletsRenderer.isEnabled = true
// }
let chartValue: CGFloat = CGFloat(closestDate.timeIntervalSince1970)
let detailsViewPosition = (chartValue - horizontalRange.lowerBound) / horizontalRange.distance * chartFrame.width + chartFrame.minX
self.setDetailsViewModel?(chartDetailsViewModel(closestDate: closestDate, pointIndex: minIndex), chartInteractionWasBegin)
self.setDetailsChartVisibleClosure?(true, true)
self.setDetailsViewPositionClosure?(detailsViewPosition)
self.verticalLineRenderer.values = [chartValue]
}
// func chartDetailsViewModel(closestDate: Date, pointIndex: Int) -> ChartDetailsViewModel {
// var viewModel = super.chartDetailsViewModel(closestDate: closestDate, pointIndex: pointIndex)
// let visibleChartValues = self.visibleChartValues
// let totalSumm: CGFloat = visibleChartValues.map { CGFloat($0.values[pointIndex]) }.reduce(0, +)
//
// viewModel.totalValue = ChartDetailsViewModel.Value(prefix: nil,
// title: "Total",
// value: BaseConstants.detailsNumberFormatter.string(from: totalSumm),
// color: .white,
// visible: visibleChartValues.count > 1)
// return viewModel
// }
//
func chartDetailsViewModel(closestDate: Date, pointIndex: Int) -> ChartDetailsViewModel {
let values: [ChartDetailsViewModel.Value] = initialChartCollection.chartValues.enumerated().map { arg in
let (index, component) = arg
return ChartDetailsViewModel.Value(prefix: nil,
title: component.name,
value: BaseConstants.detailsNumberFormatter.string(from: NSNumber(value: component.values[pointIndex])) ?? "",
color: component.color,
visible: chartVisibility[index])
}
let dateString: String
if isZoomed {
dateString = BaseConstants.timeDateFormatter.string(from: closestDate)
} else {
dateString = BaseConstants.headerMediumRangeFormatter.string(from: closestDate)
}
let viewModel = ChartDetailsViewModel(title: dateString,
showArrow: self.isZoomable && !self.isZoomed,
showPrefixes: false,
values: values,
totalValue: nil,
tapAction: { [weak self] in },
hideAction: { [weak self] in
self?.setDetailsChartVisibleClosure?(false, true)
})
return viewModel
}
public override func chartInteractionDidEnd() {
self.isChartInteractionBegun = false
}
public override var currentHorizontalRange: ClosedRange<CGFloat> {
return graphControllers.first?.mainBarsRenderer.horizontalRange.end ?? BaseConstants.defaultRange
}
public override var currentChartHorizontalRangeFraction: ClosedRange<CGFloat> {
let lowerPercent = (currentHorizontalRange.lowerBound - totalHorizontalRange.lowerBound) / totalHorizontalRange.distance
let upperPercent = (currentHorizontalRange.upperBound - totalHorizontalRange.lowerBound) / totalHorizontalRange.distance
return lowerPercent...upperPercent
}
public override func cancelChartInteraction() {
super.cancelChartInteraction()
self.graphControllers.forEach { controller in
controller.lineBulletsRenderer.isEnabled = false
}
self.setDetailsChartVisibleClosure?(false, true)
self.verticalLineRenderer.values = []
}
func setupChartCollection(chartsCollection: ChartsCollection, animated: Bool, isZoomed: Bool) {
for (index, controller) in self.graphControllers.enumerated() {
let chart = chartsCollection.chartValues[index]
let points = chart.values.enumerated().map({ (arg) -> CGPoint in
return CGPoint(x: chartsCollection.axisValues[arg.offset].timeIntervalSince1970,
y: arg.element)
})
let (width, chartBars, totalHorizontalRange, totalVerticalRange) = BarChartRenderer.BarsData.initialComponents(chartsCollection: chartsCollection)
controller.chartBars = chartBars
controller.barsWidth = width
controller.verticalScalesRenderer.labelsColor = chart.color
controller.totalVerticalRange = totalVerticalRange
self.totalHorizontalRange = totalHorizontalRange
// controller.lineBulletsRenderer.bullets = chartBars.components.map { LineBulletsRenderer.Bullet(coordinate: $0.values.first ?? .zero,
// color: $0.color) }
controller.previewBarsRenderer.setup(horizontalRange: self.totalHorizontalRange, animated: animated)
controller.previewBarsRenderer.setup(verticalRange: controller.totalVerticalRange, animated: animated)
controller.mainBarsRenderer.bars = chartBars
controller.previewBarsRenderer.bars = chartBars
controller.verticalScalesRenderer.setHorizontalLinesVisible((index == 0), animated: animated)
controller.verticalScalesRenderer.isRightAligned = (index != 0)
}
let chartRange: ClosedRange<CGFloat>
if isZoomed {
chartRange = zoomedChartRange
} else {
chartRange = initialChartRange
}
// updateHorizontalLimits(horizontalRange: chartRange, animated: animated)
updateMainChartHorizontalRange(range: chartRange, animated: animated)
updateMainChartVerticalRange(range: chartRange, animated: animated)
// updateVerticalLimitsAndRange(horizontalRange: chartRange, animated: animated)
self.chartRangeUpdatedClosure?(currentChartHorizontalRangeFraction, animated)
}
// func setupChartCollection(chartsCollection: ChartsCollection, animated: Bool, isZoomed: Bool) {
// if animated {
// TimeInterval.setDefaultSuration(.expandAnimationDuration)
// DispatchQueue.main.asyncAfter(deadline: .now() + .expandAnimationDuration) {
// TimeInterval.setDefaultSuration(.osXDuration)
// }
// }
//
// self.initialChartsCollection = chartsCollection
// self.isZoomed = isZoomed
//
// self.setBackButtonVisibilityClosure?(isZoomed, animated)
//
// self.graphControllers.forEach { controller in
// controller.barsController.willAppear(animated: animated)
// }
//
// self.refreshChartToolsClosure?(animated)
// }
public override func didTapZoomIn(date: Date) {
guard isZoomed == false else { return }
cancelChartInteraction()
self.getDetailsData?(date, { updatedCollection in
if let updatedCollection = updatedCollection {
self.initialChartRange = self.currentHorizontalRange
if let startDate = updatedCollection.axisValues.first,
let endDate = updatedCollection.axisValues.last {
self.zoomedChartRange = CGFloat(max(date.timeIntervalSince1970, startDate.timeIntervalSince1970))...CGFloat(min(date.timeIntervalSince1970 + .day - .hour, endDate.timeIntervalSince1970))
} else {
self.zoomedChartRange = CGFloat(date.timeIntervalSince1970)...CGFloat(date.timeIntervalSince1970 + .day - 1)
}
self.setupChartCollection(chartsCollection: updatedCollection, animated: true, isZoomed: true)
}
})
}
public override func didTapZoomOut() {
cancelChartInteraction()
self.setupChartCollection(chartsCollection: self.initialChartCollection, animated: true, isZoomed: false)
}
public override func updateChartRange(_ rangeFraction: ClosedRange<CGFloat>, animated: Bool) {
cancelChartInteraction()
let horizontalRange = ClosedRange(uncheckedBounds:
(lower: totalHorizontalRange.lowerBound + rangeFraction.lowerBound * totalHorizontalRange.distance,
upper: totalHorizontalRange.lowerBound + rangeFraction.upperBound * totalHorizontalRange.distance))
zoomedChartRange = horizontalRange
// updateChartRangeTitle(animated: true)
updateMainChartHorizontalRange(range: horizontalRange, animated: false)
// updateHorizontalLimits(horizontalRange: horizontalRange, animated: true)
// updateVerticalLimitsAndRange(horizontalRange: horizontalRange, animated: true)
// barsController.chartRangeFractionDidUpdated(rangeFraction)
//
// let totalHorizontalRange = barsController.totalHorizontalRange
// let horizontalRange = ClosedRange(uncheckedBounds:
// (lower: totalHorizontalRange.lowerBound + rangeFraction.lowerBound * totalHorizontalRange.distance,
// upper: totalHorizontalRange.lowerBound + rangeFraction.upperBound * totalHorizontalRange.distance))
//
// updateMainChartHorizontalRange(range: horizontalRange, animated: false)
}
func updateMainChartHorizontalRange(range: ClosedRange<CGFloat>, animated: Bool) {
self.graphControllers.forEach { controller in
controller.mainBarsRenderer.setup(horizontalRange: range, animated: animated)
// controller.horizontalScalesRenderer.setup(horizontalRange: range, animated: animated)
controller.verticalScalesRenderer.setup(horizontalRange: range, animated: animated)
controller.lineBulletsRenderer.setup(horizontalRange: range, animated: animated)
}
self.horizontalScalesRenderer.setup(horizontalRange: range, animated: animated)
self.verticalLineRenderer.setup(horizontalRange: range, animated: animated)
}
func updateMainChartVerticalRange(range: ClosedRange<CGFloat>, animated: Bool) {
self.verticalLineRenderer.setup(verticalRange: range, animated: animated)
self.graphControllers.forEach { controller in
controller.lineBulletsRenderer.setup(verticalRange: range, animated: animated)
}
}
override public func apply(theme: ChartTheme, animated: Bool) {
super.apply(theme: theme, animated: animated)
self.graphControllers.forEach { controller in
controller.verticalScalesRenderer.horizontalLinesColor = theme.chartHelperLinesColor
controller.lineBulletsRenderer.setInnerColor(theme.chartBackgroundColor, animated: animated)
controller.verticalScalesRenderer.axisXColor = theme.chartStrongLinesColor
}
verticalLineRenderer.linesColor = theme.chartStrongLinesColor
}
public override var drawChartVisibity: Bool {
return true
}
}
//TODO: Убрать Performance полоски сверзу чартов (Не забыть)
//TODO: Добавить ховеры на кнопки

View File

@ -0,0 +1,309 @@
import Foundation
#if os(macOS)
import Cocoa
#else
import UIKit
#endif
private enum Constants {
static let verticalBaseAnchors: [CGFloat] = [8, 5, 4, 2.5, 2, 1]
}
public class TwoAxisStepBarsChartController: BaseLinesChartController {
class GraphController {
let mainBarsRenderer = BarChartRenderer(step: true)
let verticalScalesRenderer = VerticalScalesRenderer()
let lineBulletsRenderer = LineBulletsRenderer()
let previewBarsRenderer = BarChartRenderer(step: true, lineWidth: 1.0)
var chartBars: BarChartRenderer.BarsData = .blank
var barsWidth: CGFloat = 1
var totalVerticalRange: ClosedRange<CGFloat> = BaseConstants.defaultRange
init() {
self.lineBulletsRenderer.isEnabled = false
self.mainBarsRenderer.optimizationLevel = BaseConstants.barsChartOptimizationLevel
self.previewBarsRenderer.optimizationLevel = BaseConstants.barsChartOptimizationLevel
}
func updateMainChartVerticalRange(range: ClosedRange<CGFloat>, animated: Bool) {
mainBarsRenderer.setup(verticalRange: range, animated: animated)
verticalScalesRenderer.setup(verticalRange: range, animated: animated)
lineBulletsRenderer.setup(verticalRange: range, animated: animated)
}
}
private var graphControllers: [GraphController] = []
private let verticalLineRenderer = VerticalLinesRenderer()
private let horizontalScalesRenderer = HorizontalScalesRenderer()
var totalHorizontalRange: ClosedRange<CGFloat> = BaseConstants.defaultRange
private let initialChartCollection: ChartsCollection
private var prevoiusHorizontalStrideInterval: Int = 1
override public init(chartsCollection: ChartsCollection) {
self.initialChartCollection = chartsCollection
graphControllers = chartsCollection.chartValues.map { _ in GraphController() }
super.init(chartsCollection: chartsCollection)
self.zoomChartVisibility = chartVisibility
}
override func setupChartCollection(chartsCollection: ChartsCollection, animated: Bool, isZoomed: Bool) {
super.setupChartCollection(chartsCollection: chartsCollection, animated: animated, isZoomed: isZoomed)
for (index, controller) in self.graphControllers.enumerated() {
let chart = chartsCollection.chartValues[index]
let initialComponents = [BarChartRenderer.BarsData.Component(color: chart.color,
values: chart.values.map { CGFloat($0) })]
let (width, chartBars, totalHorizontalRange, totalVerticalRange) = BarChartRenderer.BarsData.initialComponents(chartsCollection: chartsCollection, separate: true, initialComponents: initialComponents)
controller.chartBars = chartBars
controller.verticalScalesRenderer.labelsColor = chart.color
controller.barsWidth = width
controller.totalVerticalRange = totalVerticalRange
self.totalHorizontalRange = totalHorizontalRange
var bullets: [LineBulletsRenderer.Bullet] = []
if let component = chartBars.components.first {
for i in 0 ..< chartBars.locations.count {
let location = chartBars.locations[i]
let value = component.values[i]
bullets.append(LineBulletsRenderer.Bullet(coordinate: CGPoint(x: location, y: value), color: component.color))
}
}
controller.lineBulletsRenderer.bullets = bullets
controller.previewBarsRenderer.setup(horizontalRange: self.totalHorizontalRange, animated: animated)
controller.previewBarsRenderer.setup(verticalRange: controller.totalVerticalRange, animated: animated)
controller.mainBarsRenderer.bars = chartBars
controller.previewBarsRenderer.bars = chartBars
controller.verticalScalesRenderer.setHorizontalLinesVisible((index == 0), animated: animated)
controller.verticalScalesRenderer.isRightAligned = (index != 0)
}
self.prevoiusHorizontalStrideInterval = -1
let chartRange: ClosedRange<CGFloat>
if isZoomed {
chartRange = zoomedChartRange
} else {
chartRange = initialChartRange
}
updateHorizontalLimits(horizontalRange: chartRange, animated: animated)
updateMainChartHorizontalRange(range: chartRange, animated: animated)
updateVerticalLimitsAndRange(horizontalRange: chartRange, animated: animated)
self.chartRangeUpdatedClosure?(currentChartHorizontalRangeFraction, animated)
}
public override func initializeChart() {
if let first = initialChartCollection.axisValues.first?.timeIntervalSince1970,
let last = initialChartCollection.axisValues.last?.timeIntervalSince1970 {
initialChartRange = CGFloat(max(first, last - BaseConstants.defaultRangePresetLength))...CGFloat(last)
}
setupChartCollection(chartsCollection: initialChartCollection, animated: false, isZoomed: false)
}
public override var mainChartRenderers: [ChartViewRenderer] {
return graphControllers.map { $0.mainBarsRenderer } +
graphControllers.flatMap { [$0.verticalScalesRenderer, $0.lineBulletsRenderer] } +
[horizontalScalesRenderer, verticalLineRenderer,
// performanceRenderer
]
}
public override var navigationRenderers: [ChartViewRenderer] {
return graphControllers.map { $0.previewBarsRenderer }
}
public override func updateChartsVisibility(visibility: [Bool], animated: Bool) {
chartVisibility = visibility
zoomChartVisibility = visibility
let firstIndex = visibility.firstIndex(where: { $0 })
for (index, isVisible) in visibility.enumerated() {
let graph = graphControllers[index]
graph.mainBarsRenderer.setVisible(isVisible, animated: animated)
graph.previewBarsRenderer.setVisible(isVisible, animated: animated)
graph.lineBulletsRenderer.setLineVisible(isVisible, at: 0, animated: animated)
graph.verticalScalesRenderer.setVisible(isVisible, animated: animated)
if let firstIndex = firstIndex {
graph.verticalScalesRenderer.setHorizontalLinesVisible(index == firstIndex, animated: animated)
}
}
updateVerticalLimitsAndRange(horizontalRange: currentHorizontalRange, animated: true)
if isChartInteractionBegun {
chartInteractionDidBegin(point: lastChartInteractionPoint)
}
}
public override func chartInteractionDidBegin(point: CGPoint) {
let horizontalRange = currentHorizontalRange
let chartFrame = self.chartFrame()
guard chartFrame.width > 0 else { return }
let dateToFind = Date(timeIntervalSince1970: TimeInterval(horizontalRange.distance * point.x + horizontalRange.lowerBound))
guard let (closestDate, minIndex) = findClosestDateTo(dateToFind: dateToFind) else { return }
let chartInteractionWasBegin = isChartInteractionBegun
super.chartInteractionDidBegin(point: point)
for graphController in graphControllers {
var bullets: [LineBulletsRenderer.Bullet] = []
if let component = graphController.chartBars.components.first {
let location = graphController.chartBars.locations[minIndex]
let value = component.values[minIndex]
bullets.append(LineBulletsRenderer.Bullet(coordinate: CGPoint(x: location, y: value), color: component.color))
}
graphController.lineBulletsRenderer.bullets = bullets
graphController.lineBulletsRenderer.isEnabled = true
}
let chartValue: CGFloat = CGFloat(closestDate.timeIntervalSince1970)
let detailsViewPosition = (chartValue - horizontalRange.lowerBound) / horizontalRange.distance * chartFrame.width + chartFrame.minX
self.setDetailsViewModel?(chartDetailsViewModel(closestDate: closestDate, pointIndex: minIndex), chartInteractionWasBegin)
self.setDetailsChartVisibleClosure?(true, true)
self.setDetailsViewPositionClosure?(detailsViewPosition)
self.verticalLineRenderer.values = [chartValue]
}
public override var currentChartHorizontalRangeFraction: ClosedRange<CGFloat> {
let lowerPercent = (currentHorizontalRange.lowerBound - totalHorizontalRange.lowerBound) / totalHorizontalRange.distance
let upperPercent = (currentHorizontalRange.upperBound - totalHorizontalRange.lowerBound) / totalHorizontalRange.distance
return lowerPercent...upperPercent
}
public override var currentHorizontalRange: ClosedRange<CGFloat> {
return graphControllers.first?.mainBarsRenderer.horizontalRange.end ?? BaseConstants.defaultRange
}
public override func cancelChartInteraction() {
super.cancelChartInteraction()
for graphController in graphControllers {
graphController.lineBulletsRenderer.isEnabled = false
}
self.setDetailsChartVisibleClosure?(false, true)
self.verticalLineRenderer.values = []
}
public override func didTapZoomOut() {
cancelChartInteraction()
self.setupChartCollection(chartsCollection: initialChartCollection, animated: true, isZoomed: false)
}
public override func updateChartRange(_ rangeFraction: ClosedRange<CGFloat>, animated: Bool = true) {
cancelChartInteraction()
let horizontalRange = ClosedRange(uncheckedBounds:
(lower: totalHorizontalRange.lowerBound + rangeFraction.lowerBound * totalHorizontalRange.distance,
upper: totalHorizontalRange.lowerBound + rangeFraction.upperBound * totalHorizontalRange.distance))
zoomedChartRange = horizontalRange
updateChartRangeTitle(animated: true)
updateMainChartHorizontalRange(range: horizontalRange, animated: false)
updateHorizontalLimits(horizontalRange: horizontalRange, animated: true)
updateVerticalLimitsAndRange(horizontalRange: horizontalRange, animated: true)
}
func updateMainChartHorizontalRange(range: ClosedRange<CGFloat>, animated: Bool) {
for controller in graphControllers {
controller.mainBarsRenderer.setup(horizontalRange: range, animated: animated)
controller.verticalScalesRenderer.setup(horizontalRange: range, animated: animated)
controller.lineBulletsRenderer.setup(horizontalRange: range, animated: animated)
}
horizontalScalesRenderer.setup(horizontalRange: range, animated: animated)
verticalLineRenderer.setup(horizontalRange: range, animated: animated)
}
func updateHorizontalLimits(horizontalRange: ClosedRange<CGFloat>, animated: Bool) {
if let (stride, labels) = horizontalLimitsLabels(horizontalRange: horizontalRange,
scaleType: isZoomed ? .minutes5 : .day,
prevoiusHorizontalStrideInterval: prevoiusHorizontalStrideInterval) {
self.horizontalScalesRenderer.setup(labels: labels, animated: animated)
self.prevoiusHorizontalStrideInterval = stride
}
}
func updateVerticalLimitsAndRange(horizontalRange: ClosedRange<CGFloat>, animated: Bool) {
let chartHeight = chartFrame().height
let approximateNumberOfChartValues = (chartHeight / BaseConstants.minimumAxisYLabelsDistance)
let dividorsAndMultiplers: [(startValue: CGFloat, base: CGFloat, count: Int, maximumNumberOfDecimals: Int)] = graphControllers.enumerated().map { arg in
let (index, controller) = arg
let verticalRange = BarChartRenderer.BarsData.verticalRange(bars: controller.chartBars, separate: true, calculatingRange: horizontalRange, addBounds: true) ?? controller.totalVerticalRange
var numberOfOffsetsPerItem = verticalRange.distance / approximateNumberOfChartValues
var multiplier: CGFloat = 1.0
while numberOfOffsetsPerItem > 10 {
numberOfOffsetsPerItem /= 10
multiplier *= 10
}
var dividor: CGFloat = 1.0
var maximumNumberOfDecimals = 2
while numberOfOffsetsPerItem < 1 {
numberOfOffsetsPerItem *= 10
dividor *= 10
maximumNumberOfDecimals += 1
}
let generalBase = Constants.verticalBaseAnchors.first { numberOfOffsetsPerItem > $0 } ?? BaseConstants.defaultVerticalBaseAnchor
let base = generalBase * multiplier / dividor
var verticalValue = (verticalRange.lowerBound / base).rounded(.down) * base
let startValue = verticalValue
var count = 0
if chartVisibility[index] {
while verticalValue < verticalRange.upperBound {
count += 1
verticalValue += base
}
}
return (startValue: startValue, base: base, count: count, maximumNumberOfDecimals: maximumNumberOfDecimals)
}
let totalCount = dividorsAndMultiplers.map { $0.count }.max() ?? 0
guard totalCount > 0 else { return }
let numberFormatter = BaseConstants.chartNumberFormatter
for (index, controller) in graphControllers.enumerated() {
let (startValue, base, _, maximumNumberOfDecimals) = dividorsAndMultiplers[index]
let updatedRange = startValue...(startValue + base * CGFloat(totalCount))
if controller.verticalScalesRenderer.verticalRange.end != updatedRange {
numberFormatter.maximumFractionDigits = maximumNumberOfDecimals
var verticalLabels: [LinesChartLabel] = []
for multipler in 0...(totalCount - 1) {
let verticalValue = startValue + base * CGFloat(multipler)
let text: String = numberFormatter.string(from: NSNumber(value: Double(verticalValue))) ?? ""
verticalLabels.append(LinesChartLabel(value: verticalValue, text: text))
}
controller.verticalScalesRenderer.setup(verticalLimitsLabels: verticalLabels, animated: animated)
controller.updateMainChartVerticalRange(range: updatedRange, animated: animated)
}
}
}
public override func apply(theme: ChartTheme, animated: Bool) {
horizontalScalesRenderer.labelsColor = theme.chartLabelsColor
verticalLineRenderer.linesColor = theme.chartStrongLinesColor
for controller in graphControllers {
controller.verticalScalesRenderer.horizontalLinesColor = theme.chartHelperLinesColor
controller.lineBulletsRenderer.setInnerColor(theme.chartBackgroundColor, animated: animated)
controller.verticalScalesRenderer.axisXColor = theme.chartStrongLinesColor
}
}
}

View File

@ -27,9 +27,11 @@ class BarChartRenderer: BaseChartRenderer {
}
private var step = false
private var lineWidth: CGFloat = 2.0
init(step: Bool = false) {
init(step: Bool = false, lineWidth: CGFloat = 2.0) {
self.step = step
self.lineWidth = lineWidth
super.init()
}
@ -158,14 +160,10 @@ class BarChartRenderer: BaseChartRenderer {
var leftX = transform(toChartCoordinateHorizontal: currentLocation - bars.barWidth, chartFrame: chartFrame)
var rightX: CGFloat = 0
let startPoint = CGPoint(x: leftX,
y: transform(toChartCoordinateVertical: verticalRange.current.lowerBound, chartFrame: chartFrame))
var backgourndPaths: [[CGPoint]] = bars.components.map { _ in Array() }
var backgroundPaths: [[CGPoint]] = bars.components.map { _ in Array() }
let itemsCount = ((bars.locations.count - barIndex) * 2) + 4
for path in backgourndPaths.indices {
backgourndPaths[path].reserveCapacity(itemsCount)
backgourndPaths[path].append(startPoint)
for path in backgroundPaths.indices {
backgroundPaths[path].reserveCapacity(itemsCount)
}
var maxValues: [CGFloat] = bars.components.map { _ in 0 }
@ -173,18 +171,13 @@ class BarChartRenderer: BaseChartRenderer {
currentLocation = bars.locations[barIndex]
rightX = transform(toChartCoordinateHorizontal: currentLocation, chartFrame: chartFrame)
var stackedValue: CGFloat = 0
var bottomY: CGFloat = transform(toChartCoordinateVertical: stackedValue, chartFrame: chartFrame)
let bottomY: CGFloat = transform(toChartCoordinateVertical: 0.0, chartFrame: chartFrame)
for (index, component) in bars.components.enumerated() {
let visibilityPercent = componentsAnimators[index].current
if visibilityPercent == 0 { continue }
var value = component.values[barIndex]
if value < 5000 {
value *= 40
}
let value = component.values[barIndex]
let height = value * visibilityPercent
// stackedValue += height
let topY = transform(toChartCoordinateVertical: height, chartFrame: chartFrame)
let componentHeight = (bottomY - topY)
maxValues[index] = max(maxValues[index], componentHeight)
@ -195,9 +188,8 @@ class BarChartRenderer: BaseChartRenderer {
height: componentHeight)
selectedPaths[index].append(rect)
}
backgourndPaths[index].append(CGPoint(x: leftX, y: topY))
backgourndPaths[index].append(CGPoint(x: rightX, y: topY))
// bottomY = topY
backgroundPaths[index].append(CGPoint(x: leftX, y: topY))
backgroundPaths[index].append(CGPoint(x: rightX, y: topY))
}
if currentLocation > range.upperBound {
break
@ -206,8 +198,6 @@ class BarChartRenderer: BaseChartRenderer {
barIndex += 1
}
let endPoint = CGPoint(x: transform(toChartCoordinateHorizontal: currentLocation, chartFrame: chartFrame).roundedUpToPixelGrid(),
y: transform(toChartCoordinateVertical: verticalRange.current.lowerBound, chartFrame: chartFrame))
let colorOffset = Double((1.0 - (1.0 - generalUnselectedAlpha) * selectedIndexAnimator.current) * chartsAlpha)
for (index, component) in bars.components.enumerated().reversed() {
@ -216,12 +206,12 @@ class BarChartRenderer: BaseChartRenderer {
}
context.saveGState()
context.setLineWidth(2.0)
context.setLineWidth(self.lineWidth)
context.setStrokeColor(GColor.valueBetween(start: backgroundColorAnimator.current.color,
end: component.color,
offset: colorOffset).cgColor)
context.beginPath()
context.addLines(between: backgourndPaths[index])
context.addLines(between: backgroundPaths[index])
context.strokePath()
context.restoreGState()
}
@ -236,11 +226,11 @@ class BarChartRenderer: BaseChartRenderer {
let startPoint = CGPoint(x: leftX,
y: transform(toChartCoordinateVertical: verticalRange.current.lowerBound, chartFrame: chartFrame))
var backgourndPaths: [[CGPoint]] = bars.components.map { _ in Array() }
var backgroundPaths: [[CGPoint]] = bars.components.map { _ in Array() }
let itemsCount = ((bars.locations.count - barIndex) * 2) + 4
for path in backgourndPaths.indices {
backgourndPaths[path].reserveCapacity(itemsCount)
backgourndPaths[path].append(startPoint)
for path in backgroundPaths.indices {
backgroundPaths[path].reserveCapacity(itemsCount)
backgroundPaths[path].append(startPoint)
}
var maxValues: [CGFloat] = bars.components.map { _ in 0 }
while barIndex < bars.locations.count {
@ -265,8 +255,8 @@ class BarChartRenderer: BaseChartRenderer {
height: componentHeight)
selectedPaths[index].append(rect)
}
backgourndPaths[index].append(CGPoint(x: leftX, y: topY))
backgourndPaths[index].append(CGPoint(x: rightX, y: topY))
backgroundPaths[index].append(CGPoint(x: leftX, y: topY))
backgroundPaths[index].append(CGPoint(x: rightX, y: topY))
bottomY = topY
}
if currentLocation > range.upperBound {
@ -285,13 +275,13 @@ class BarChartRenderer: BaseChartRenderer {
continue
}
context.saveGState()
backgourndPaths[index].append(endPoint)
backgroundPaths[index].append(endPoint)
context.setFillColor(GColor.valueBetween(start: backgroundColorAnimator.current.color,
end: component.color,
offset: colorOffset).cgColor)
context.beginPath()
context.addLines(between: backgourndPaths[index])
context.addLines(between: backgroundPaths[index])
context.closePath()
context.fillPath()
context.restoreGState()
@ -308,7 +298,7 @@ class BarChartRenderer: BaseChartRenderer {
}
extension BarChartRenderer.BarsData {
static func initialComponents(chartsCollection: ChartsCollection) ->
static func initialComponents(chartsCollection: ChartsCollection, separate: Bool = false, initialComponents: [BarChartRenderer.BarsData.Component]? = nil) ->
(width: CGFloat,
chartBars: BarChartRenderer.BarsData,
totalHorizontalRange: ClosedRange<CGFloat>,
@ -319,15 +309,13 @@ extension BarChartRenderer.BarsData {
} else {
width = 1
}
let components = chartsCollection.chartValues.map { BarChartRenderer.BarsData.Component(color: $0.color,
let components = initialComponents ?? chartsCollection.chartValues.map { BarChartRenderer.BarsData.Component(color: $0.color,
values: $0.values.map { CGFloat($0) }) }
let chartBars = BarChartRenderer.BarsData(barWidth: width,
locations: chartsCollection.axisValues.map { CGFloat($0.timeIntervalSince1970) },
components: components)
let totalVerticalRange = BarChartRenderer.BarsData.verticalRange(bars: chartBars) ?? 0...1
let totalVerticalRange = BarChartRenderer.BarsData.verticalRange(bars: chartBars, separate: separate) ?? 0...1
let totalHorizontalRange = BarChartRenderer.BarsData.visibleHorizontalRange(bars: chartBars, width: width) ?? 0...1
return (width: width, chartBars: chartBars, totalHorizontalRange: totalHorizontalRange, totalVerticalRange: totalVerticalRange)
}
@ -342,7 +330,7 @@ extension BarChartRenderer.BarsData {
return (firstPoint - width)...lastPoint
}
static func verticalRange(bars: BarChartRenderer.BarsData, calculatingRange: ClosedRange<CGFloat>? = nil, addBounds: Bool = false) -> ClosedRange<CGFloat>? {
static func verticalRange(bars: BarChartRenderer.BarsData, separate: Bool = false, calculatingRange: ClosedRange<CGFloat>? = nil, addBounds: Bool = false) -> ClosedRange<CGFloat>? {
guard bars.components.count > 0 else {
return nil
}
@ -353,11 +341,17 @@ extension BarChartRenderer.BarsData {
var vMax: CGFloat = bars.components[0].values[index]
while index < bars.locations.count {
var summ: CGFloat = 0
for component in bars.components {
summ += component.values[index]
if separate {
for component in bars.components {
vMax = max(vMax, component.values[index])
}
} else {
var summ: CGFloat = 0
for component in bars.components {
summ += component.values[index]
}
vMax = max(vMax, summ)
}
vMax = max(vMax, summ)
if bars.locations[index] > calculatingRange.upperBound {
break
@ -370,11 +364,18 @@ extension BarChartRenderer.BarsData {
var vMax: CGFloat = bars.components[0].values[index]
while index < bars.locations.count {
var summ: CGFloat = 0
for component in bars.components {
summ += component.values[index]
if separate {
for component in bars.components {
vMax = max(vMax, component.values[index])
}
} else {
var summ: CGFloat = 0
for component in bars.components {
summ += component.values[index]
}
vMax = max(vMax, summ)
}
vMax = max(vMax, summ)
index += 1
}
return 0...vMax

View File

@ -48,7 +48,7 @@ class LinesChartRenderer: BaseChartRenderer {
linesShapeAnimator.set(current: 1)
}
}
func setLineVisible(_ isVisible: Bool, at index: Int, animated: Bool) {
linesAlphaAnimators[index].animate(to: isVisible ? 1 : 0, duration: animated ? .defaultDuration : 0)
}
@ -62,9 +62,10 @@ class LinesChartRenderer: BaseChartRenderer {
for (index, toLine) in toLines.enumerated() {
let alpha = linesAlphaAnimators[index].current * chartsAlpha
if alpha == 0 { continue }
context.setStrokeColor(toLine.color.withAlphaComponent(alpha).cgColor)
context.setAlpha(alpha)
context.setStrokeColor(toLine.color.cgColor)
context.setLineWidth(lineWidth)
if linesShapeAnimator.isAnimating {
let animationOffset = linesShapeAnimator.current
@ -93,7 +94,7 @@ class LinesChartRenderer: BaseChartRenderer {
var previousToPoint: CGPoint
let startFromPoint: CGPoint?
let startToPoint: CGPoint?
if let validFrom = fromIndex {
previousFromPoint = convertFromPoint(fromPoints[max(0, validFrom - 1)])
startFromPoint = previousFromPoint
@ -110,7 +111,7 @@ class LinesChartRenderer: BaseChartRenderer {
}
var combinedPoints: [CGPoint] = []
func add(pointToDraw: CGPoint) {
if let startFromPoint = startFromPoint,
pointToDraw.x < startFromPoint.x {
@ -312,13 +313,7 @@ class LinesChartRenderer: BaseChartRenderer {
context.setLineCap(.round)
context.strokeLineSegments(between: lines)
} else {
let alpha = linesAlphaAnimators[index].current * chartsAlpha
if alpha == 0 { continue }
context.setStrokeColor(toLine.color.withAlphaComponent(alpha).cgColor)
context.setLineWidth(lineWidth)
} else {
if var index = toLine.points.firstIndex(where: { $0.x >= range.lowerBound }) {
var lines: [CGPoint] = []
index = max(0, index - 1)
@ -436,33 +431,8 @@ class LinesChartRenderer: BaseChartRenderer {
context.setLineCap(.round)
context.strokeLineSegments(between: lines)
}
// if var start = toLine.points.firstIndex(where: { $0.x > range.lowerBound }) {
// let alpha = linesAlphaAnimators[index].current * chartsAlpha
// if alpha == 0 { continue }
// context.setStrokeColor(toLine.color.withAlphaComponent(alpha).cgColor)
// context.setLineWidth(lineWidth)
//
// context.setLineCap(.round)
// start = max(0, start - 1)
// let startPoint = toLine.points[start]
// var lines: [CGPoint] = []
// var pointToDraw = CGPoint(x: transform(toChartCoordinateHorizontal: startPoint.x, chartFrame: chartFrame),
// y: transform(toChartCoordinateVertical: startPoint.y, chartFrame: chartFrame))
// for index in (start + 1)..<toLine.points.count {
// lines.append(pointToDraw)
// let point = toLine.points[index]
// pointToDraw = CGPoint(x: transform(toChartCoordinateHorizontal: point.x, chartFrame: chartFrame),
// y: transform(toChartCoordinateVertical: point.y, chartFrame: chartFrame))
// lines.append(pointToDraw)
// if point.x > range.upperBound {
// break
// }
// }
//
// context.strokeLineSegments(between: lines)
// }
}
context.setAlpha(1.0)
}
}
}

View File

@ -54,6 +54,21 @@ extension GColor {
case "lightblue":
self.init(hexString: "#5ac8fa")
return
case "seablue":
self.init(hexString: "#35afdc")
return
case "orange":
self.init(hexString: "#fd7a32")
return
case "violet":
self.init(hexString: "#9968f7")
return
case "emerald":
self.init(hexString: "#37cca3")
return
case "pink":
self.init(hexString: "#ff4f79")
return
default:
break
}

View File

@ -14,7 +14,7 @@ private let cornerRadius: CGFloat = 5
private let verticalMargins: CGFloat = 8
private var labelHeight: CGFloat = 18
private var margin: CGFloat = 10
private var prefixLabelWidth: CGFloat = 27
private var prefixLabelWidth: CGFloat = 29
private var textLabelWidth: CGFloat = 96
private var valueLabelWidth: CGFloat = 65
@ -56,9 +56,10 @@ class ChartDetailsView: UIControl {
func setup(viewModel: ChartDetailsViewModel, animated: Bool) {
self.viewModel = viewModel
titleLabel.setText(viewModel.title, animated: animated)
titleLabel.setText(viewModel.title, animated: false)
titleLabel.setVisible(!viewModel.title.isEmpty, animated: animated)
arrowView.setVisible(viewModel.showArrow, animated: animated)
arrowButton.isUserInteractionEnabled = viewModel.showArrow
let width: CGFloat = margin * 2 + (viewModel.showPrefixes ? (prefixLabelWidth + margin) : 0) + textLabelWidth + valueLabelWidth
var y: CGFloat = verticalMargins

View File

@ -13,6 +13,8 @@ public enum ChartType {
case pie
case bars
case step
case twoAxisStep
case hourlyStep
}
public extension ChartTheme {
@ -24,6 +26,54 @@ public extension ChartTheme {
}
}
public func createChartController(_ data: String, type: ChartType, getDetailsData: @escaping (Date, @escaping (String?) -> Void) -> Void) -> BaseChartController? {
var resultController: BaseChartController?
if let data = data.data(using: .utf8) {
ChartsDataManager.readChart(data: data, extraCopiesCount: 0, sync: true, success: { collection in
let controller: BaseChartController
switch type {
case .lines:
controller = GeneralLinesChartController(chartsCollection: collection)
controller.isZoomable = false
case .twoAxis:
controller = TwoAxisLinesChartController(chartsCollection: collection)
controller.isZoomable = false
case .pie:
controller = PercentPieChartController(chartsCollection: collection)
case .bars:
controller = StackedBarsChartController(chartsCollection: collection)
controller.isZoomable = false
case .step:
controller = StepBarsChartController(chartsCollection: collection)
case .twoAxisStep:
controller = TwoAxisStepBarsChartController(chartsCollection: collection)
case .hourlyStep:
controller = StepBarsChartController(chartsCollection: collection, hourly: true)
controller.isZoomable = false
}
controller.getDetailsData = { date, completion in
getDetailsData(date, { detailsData in
if let detailsData = detailsData, let data = detailsData.data(using: .utf8) {
ChartsDataManager.readChart(data: data, extraCopiesCount: 0, sync: true, success: { collection in
Queue.mainQueue().async {
completion(collection)
}
}) { error in
completion(nil)
}
} else {
completion(nil)
}
})
}
resultController = controller
}) { error in
}
}
return resultController
}
public final class ChartNode: ASDisplayNode {
private var chartView: ChartStackSection {
return self.view as! ChartStackSection
@ -36,64 +86,21 @@ public final class ChartNode: ASDisplayNode {
return ChartStackSection()
})
}
public override func didLoad() {
super.didLoad()
self.view.disablesInteractiveTransitionGestureRecognizer = true
}
public func setupTheme(_ theme: ChartTheme) {
self.chartView.apply(theme: ChartTheme.defaultDayTheme, animated: false)
}
public override func layout() {
super.layout()
self.chartView.setNeedsDisplay()
public func setup(controller: BaseChartController) {
var displayRange = true
if let controller = controller as? StepBarsChartController {
displayRange = !controller.hourly
}
self.chartView.setup(controller: controller, displayRange: displayRange)
}
public func setup(_ data: String, type: ChartType, getDetailsData: @escaping (Date, @escaping (String?) -> Void) -> Void) {
if let data = data.data(using: .utf8) {
ChartsDataManager.readChart(data: data, extraCopiesCount: 0, sync: true, success: { [weak self] collection in
let controller: BaseChartController
switch type {
case .lines:
controller = GeneralLinesChartController(chartsCollection: collection)
controller.isZoomable = false
case .twoAxis:
controller = TwoAxisLinesChartController(chartsCollection: collection)
controller.isZoomable = false
case .pie:
controller = PercentPieChartController(chartsCollection: collection)
case .bars:
controller = StackedBarsChartController(chartsCollection: collection)
controller.isZoomable = false
case .step:
controller = StepBarsChartController(chartsCollection: collection)
}
controller.getDetailsData = { date, completion in
getDetailsData(date, { detailsData in
if let detailsData = detailsData, let data = detailsData.data(using: .utf8) {
ChartsDataManager.readChart(data: data, extraCopiesCount: 0, sync: true, success: { collection in
Queue.mainQueue().async {
completion(collection)
}
}) { error in
completion(nil)
}
} else {
completion(nil)
}
})
}
if let strongSelf = self {
strongSelf.chartView.setup(controller: controller, title: "")
}
}) { error in
}
}
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return super.hitTest(point, with: event)
}
required public init?(coder aDecoder: NSCoder) {

View File

@ -35,6 +35,8 @@ class ChartStackSection: UIView, ChartThemeContainer {
var controller: BaseChartController?
var theme: ChartTheme?
var displayRange: Bool = true
init() {
sectionContainerView = UIView()
chartView = ChartView()
@ -157,13 +159,20 @@ class ChartStackSection: UIView, ChartThemeContainer {
self.titleLabel.frame = CGRect(origin: CGPoint(x: backButton.alpha > 0.0 ? 36.0 : 0.0, y: 5.0), size: CGSize(width: bounds.width, height: 28.0))
self.sectionContainerView.frame = CGRect(origin: CGPoint(), size: CGSize(width: bounds.width, height: 750.0))
self.chartView.frame = CGRect(origin: CGPoint(), size: CGSize(width: bounds.width, height: 250.0))
self.rangeView.isHidden = !self.displayRange
self.rangeView.frame = CGRect(origin: CGPoint(x: 0.0, y: 250.0), size: CGSize(width: bounds.width, height: 42.0))
self.visibilityView.frame = CGRect(origin: CGPoint(x: 0.0, y: 308.0), size: CGSize(width: bounds.width, height: 350.0))
self.visibilityView.frame = CGRect(origin: CGPoint(x: 0.0, y: self.displayRange ? 308.0 : 266.0), size: CGSize(width: bounds.width, height: 350.0))
self.backButton.frame = CGRect(x: 0.0, y: 0.0, width: 96.0, height: 38.0)
self.chartView.setNeedsDisplay()
}
func setup(controller: BaseChartController, title: String) {
func setup(controller: BaseChartController, displayRange: Bool = true) {
self.controller = controller
self.displayRange = displayRange
if let theme = self.theme {
controller.apply(theme: theme, animated: false)
}
@ -222,7 +231,10 @@ class ChartStackSection: UIView, ChartThemeContainer {
controller.initializeChart()
updateToolViews(animated: false)
rangeView.setRange(0.8...1.0, animated: false)
controller.updateChartRange(0.8...1.0, animated: false)
let range: ClosedRange<CGFloat> = displayRange ? 0.8 ... 1.0 : 0.0 ... 1.0
rangeView.setRange(range, animated: false)
controller.updateChartRange(range, animated: false)
self.setNeedsLayout()
}
}

View File

@ -75,7 +75,9 @@ class ChartView: UIControl {
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let point = touches.first?.location(in: self) {
if var point = touches.first?.location(in: self) {
point.x = max(0.0, min(self.frame.width, point.x))
point.y = max(0.0, min(self.frame.height, point.y))
let fractionPoint = CGPoint(x: (point.x - chartFrame.origin.x) / chartFrame.width,
y: (point.y - chartFrame.origin.y) / chartFrame.height)
userDidSelectCoordinateClosure?(fractionPoint)

View File

@ -17,9 +17,42 @@ private enum Constants {
static let insets = UIEdgeInsets(top: 0, left: 16, bottom: 16, right: 16)
}
struct ChartVisibilityItem {
public struct ChartVisibilityItem {
var title: String
var color: UIColor
public init(title: String, color: UIColor) {
self.title = title
self.color = color
}
}
public func calculateVisiblityHeight(width: CGFloat, items: [ChartVisibilityItem]) -> CGFloat {
let frames = generateItemsFrames(frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: CGFloat.greatestFiniteMagnitude)), items: items)
guard let lastFrame = frames.last else { return .zero }
return lastFrame.maxY + Constants.insets.bottom
}
private func generateItemsFrames(frame: CGRect, items: [ChartVisibilityItem]) -> [CGRect] {
var previousPoint = CGPoint(x: Constants.insets.left, y: Constants.insets.top)
var frames: [CGRect] = []
for item in items {
let labelSize = (item.title as NSString).size(withAttributes: [.font: ChartVisibilityItemView.textFont])
let width = (labelSize.width + Constants.labelTextApproxInsets).rounded(.up)
if previousPoint.x + width < (frame.width - Constants.insets.left - Constants.insets.right) {
frames.append(CGRect(origin: previousPoint, size: CGSize(width: width, height: Constants.itemHeight)))
} else if previousPoint.x <= Constants.insets.left {
frames.append(CGRect(origin: previousPoint, size: CGSize(width: width, height: Constants.itemHeight)))
} else {
previousPoint.y += Constants.itemHeight + Constants.itemSpacing
previousPoint.x = Constants.insets.left
frames.append(CGRect(origin: previousPoint, size: CGSize(width: width, height: Constants.itemHeight)))
}
previousPoint.x += width + Constants.itemSpacing
}
return frames
}
class ChartVisibilityView: UIView {
@ -80,29 +113,7 @@ class ChartVisibilityView: UIView {
}
private var selectionViews: [ChartVisibilityItemView] = []
private func generateItemsFrames(frame: CGRect) -> [CGRect] {
var previousPoint = CGPoint(x: Constants.insets.left, y: Constants.insets.top)
var frames: [CGRect] = []
for item in items {
let labelSize = (item.title as NSString).size(withAttributes: [.font: ChartVisibilityItemView.textFont])
let width = (labelSize.width + Constants.labelTextApproxInsets).rounded(.up)
if previousPoint.x + width < (frame.width - Constants.insets.left - Constants.insets.right) {
frames.append(CGRect(origin: previousPoint, size: CGSize(width: width, height: Constants.itemHeight)))
} else if previousPoint.x <= Constants.insets.left {
frames.append(CGRect(origin: previousPoint, size: CGSize(width: width, height: Constants.itemHeight)))
} else {
previousPoint.y += Constants.itemHeight + Constants.itemSpacing
previousPoint.x = Constants.insets.left
frames.append(CGRect(origin: previousPoint, size: CGSize(width: width, height: Constants.itemHeight)))
}
previousPoint.x += width + Constants.itemSpacing
}
return frames
}
var selectionCallbackClosure: (([Bool]) -> Void)?
func setItemSelected(_ selected: Bool, at index: Int, animated: Bool) {
@ -129,7 +140,7 @@ class ChartVisibilityView: UIView {
}
private func updateFrames() {
for (index, frame) in generateItemsFrames(frame: bounds).enumerated() {
for (index, frame) in generateItemsFrames(frame: bounds, items: self.items).enumerated() {
selectionViews[index].frame = frame
}
}
@ -140,7 +151,7 @@ class ChartVisibilityView: UIView {
size.height = 0
return size
}
let frames = generateItemsFrames(frame: UIScreen.main.bounds)
let frames = generateItemsFrames(frame: UIScreen.main.bounds, items: self.items)
guard let lastFrame = frames.last else { return .zero }
let size = CGSize(width: frame.width, height: lastFrame.maxY + Constants.insets.bottom)
return size

View File

@ -293,7 +293,11 @@ typedef enum
[_wrapperView addSubview:_fadeView];
}
CGFloat circleWrapperViewLength = 216.0f + 38.0f;
CGFloat minSide = MIN(_wrapperView.frame.size.width, _wrapperView.frame.size.height);
CGFloat diameter = minSide > 320.0f ? 240.0f : 216.0f;
CGFloat shadowSize = minSide > 320.0f ? 21.0f : 19.0f;
CGFloat circleWrapperViewLength = diameter + shadowSize * 2.0;
_circleWrapperView = [[UIView alloc] initWithFrame:(CGRect){
.origin.x = (_wrapperView.bounds.size.width - circleWrapperViewLength) / 2.0f,
.origin.y = _wrapperView.bounds.size.height + circleWrapperViewLength * 0.3f,
@ -309,7 +313,7 @@ typedef enum
_shadowView.frame = _circleWrapperView.bounds;
[_circleWrapperView addSubview:_shadowView];
_circleView = [[UIView alloc] initWithFrame:CGRectInset(_circleWrapperView.bounds, 19.0f, 19.0f)];
_circleView = [[UIView alloc] initWithFrame:CGRectInset(_circleWrapperView.bounds, shadowSize, shadowSize)];
_circleView.clipsToBounds = true;
_circleView.layer.cornerRadius = _circleView.frame.size.width / 2.0f;
[_circleWrapperView addSubview:_circleView];
@ -325,7 +329,7 @@ typedef enum
_placeholderView.accessibilityIgnoresInvertColors = true;
}
CGFloat ringViewLength = 234.0f;
CGFloat ringViewLength = minSide > 320.0f ? 260.0f : 234.0f;
_ringView = [[TGVideoMessageRingView alloc] initWithFrame:(CGRect){
.origin.x = (_circleWrapperView.bounds.size.width - ringViewLength) / 2.0f,
.origin.y = (_circleWrapperView.bounds.size.height - ringViewLength) / 2.0f,

View File

@ -0,0 +1,18 @@
load("//Config:buck_rule_macros.bzl", "static_library")
static_library(
name = "ManagedAnimationNode",
srcs = glob([
"Sources/**/*.swift",
]),
deps = [
"//submodules/Display:Display#shared",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit#shared",
"//submodules/AsyncDisplayKit:AsyncDisplayKit#shared",
"//submodules/rlottie:RLottieBinding",
],
frameworks = [
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
"$SDKROOT/System/Library/Frameworks/UIKit.framework",
],
)

View File

@ -0,0 +1,21 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ManagedAnimationNode",
module_name = "ManagedAnimationNode",
srcs = glob([
"Sources/**/*.swift",
]),
deps = [
"//submodules/Display:Display",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Postbox:Postbox",
"//submodules/GZip:GZip",
"//submodules/rlottie:RLottieBinding",
"//submodules/AppBundle:AppBundle",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
</plist>

View File

@ -0,0 +1,11 @@
#import <Cocoa/Cocoa.h>
//! Project version number for ManagedAnimationNode.
FOUNDATION_EXPORT double ManagedAnimationNodeVersionNumber;
//! Project version string for ManagedAnimationNode.
FOUNDATION_EXPORT const unsigned char ManagedAnimationNodeVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import <ManagedAnimationNode/PublicHeader.h>

View File

@ -0,0 +1,231 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import RLottieBinding
import AppBundle
import GZip
import SwiftSignalKit
public final class ManagedAnimationState {
public let item: ManagedAnimationItem
private let instance: LottieInstance
let frameCount: Int
let fps: Double
var relativeTime: Double = 0.0
public var frameIndex: Int?
private let renderContext: DrawingContext
public init?(displaySize: CGSize, item: ManagedAnimationItem, current: ManagedAnimationState?) {
let resolvedInstance: LottieInstance
let renderContext: DrawingContext
if let current = current {
resolvedInstance = current.instance
renderContext = current.renderContext
} else {
guard let path = item.source.path else {
return nil
}
guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else {
return nil
}
guard let unpackedData = TGGUnzipData(data, 5 * 1024 * 1024) else {
return nil
}
guard let instance = LottieInstance(data: unpackedData, cacheKey: item.source.cacheKey) else {
return nil
}
resolvedInstance = instance
renderContext = DrawingContext(size: displaySize, scale: UIScreenScale, premultiplied: true, clear: true)
}
self.item = item
self.instance = resolvedInstance
self.renderContext = renderContext
self.frameCount = Int(self.instance.frameCount)
self.fps = Double(self.instance.frameRate)
}
func draw() -> UIImage? {
self.instance.renderFrame(with: Int32(self.frameIndex ?? 0), into: self.renderContext.bytes.assumingMemoryBound(to: UInt8.self), width: Int32(self.renderContext.size.width * self.renderContext.scale), height: Int32(self.renderContext.size.height * self.renderContext.scale), bytesPerRow: Int32(self.renderContext.bytesPerRow))
return self.renderContext.generateImage()
}
}
public struct ManagedAnimationFrameRange: Equatable {
var startFrame: Int
var endFrame: Int
public init(startFrame: Int, endFrame: Int) {
self.startFrame = startFrame
self.endFrame = endFrame
}
}
public enum ManagedAnimationSource: Equatable {
case local(String)
case resource(MediaBox, MediaResource)
var cacheKey: String {
switch self {
case let .local(name):
return name
case let .resource(_, resource):
return resource.id.uniqueId
}
}
var path: String? {
switch self {
case let .local(name):
return getAppBundle().path(forResource: name, ofType: "tgs")
case let .resource(mediaBox, resource):
return mediaBox.completedResourcePath(resource)
}
}
public static func == (lhs: ManagedAnimationSource, rhs: ManagedAnimationSource) -> Bool {
switch lhs {
case let .local(lhsPath):
if case let .local(rhsPath) = rhs, lhsPath == rhsPath {
return true
} else {
return false
}
case let .resource(lhsMediaBox, lhsResource):
if case let .resource(rhsMediaBox, rhsResource) = rhs, lhsMediaBox === rhsMediaBox, lhsResource.isEqual(to: rhsResource) {
return true
} else {
return false
}
}
}
}
public struct ManagedAnimationItem: Equatable {
public let source: ManagedAnimationSource
var frames: ManagedAnimationFrameRange
var duration: Double
public init(source: ManagedAnimationSource, frames: ManagedAnimationFrameRange, duration: Double) {
self.source = source
self.frames = frames
self.duration = duration
}
}
open class ManagedAnimationNode: ASDisplayNode {
public let intrinsicSize: CGSize
private let imageNode: ASImageNode
private let displayLink: CADisplayLink
public var state: ManagedAnimationState?
public var trackStack: [ManagedAnimationItem] = []
public var didTryAdvancingState = false
public init(size: CGSize) {
self.intrinsicSize = size
self.imageNode = ASImageNode()
self.imageNode.displayWithoutProcessing = true
self.imageNode.displaysAsynchronously = false
self.imageNode.frame = CGRect(origin: CGPoint(), size: self.intrinsicSize)
final class DisplayLinkTarget: NSObject {
private let f: () -> Void
init(_ f: @escaping () -> Void) {
self.f = f
}
@objc func event() {
self.f()
}
}
var displayLinkUpdate: (() -> Void)?
self.displayLink = CADisplayLink(target: DisplayLinkTarget {
displayLinkUpdate?()
}, selector: #selector(DisplayLinkTarget.event))
super.init()
self.addSubnode(self.imageNode)
self.displayLink.add(to: RunLoop.main, forMode: .common)
displayLinkUpdate = { [weak self] in
self?.updateAnimation()
}
}
open func advanceState() {
guard !self.trackStack.isEmpty else {
return
}
let item = self.trackStack.removeFirst()
if let state = self.state, state.item.source == item.source {
self.state = ManagedAnimationState(displaySize: self.intrinsicSize, item: item, current: state)
} else {
self.state = ManagedAnimationState(displaySize: self.intrinsicSize, item: item, current: nil)
}
self.didTryAdvancingState = false
}
public func updateAnimation() {
if self.state == nil {
self.advanceState()
}
guard let state = self.state else {
return
}
let timestamp = CACurrentMediaTime()
let fps = state.fps
let frameRange = state.item.frames
let duration: Double = state.item.duration
var t = state.relativeTime / duration
t = max(0.0, t)
t = min(1.0, t)
//print("\(t) \(state.item.name)")
let frameOffset = Int(Double(frameRange.startFrame) * (1.0 - t) + Double(frameRange.endFrame) * t)
let lowerBound: Int = 0
let upperBound = state.frameCount - 1
let frameIndex = max(lowerBound, min(upperBound, frameOffset))
if state.frameIndex != frameIndex {
state.frameIndex = frameIndex
if let image = state.draw() {
self.imageNode.image = image
}
}
var animationAdvancement: Double = 1.0 / 60.0
animationAdvancement *= Double(min(2, self.trackStack.count + 1))
state.relativeTime += animationAdvancement
if state.relativeTime >= duration && !self.didTryAdvancingState {
self.didTryAdvancingState = true
self.advanceState()
}
}
public func trackTo(item: ManagedAnimationItem) {
self.trackStack.append(item)
self.didTryAdvancingState = false
self.updateAnimation()
}
}

View File

@ -40,6 +40,7 @@ static_library(
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
"//submodules/OverlayStatusController:OverlayStatusController",
"//submodules/rlottie:RLottieBinding",
"//submodules/ManagedAnimationNode:ManagedAnimationNode",
],
frameworks = [
"$SDKROOT/System/Library/Frameworks/Foundation.framework",

View File

@ -39,6 +39,7 @@ swift_library(
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
"//submodules/OverlayStatusController:OverlayStatusController",
"//submodules/rlottie:RLottieBinding",
"//submodules/ManagedAnimationNode:ManagedAnimationNode",
],
visibility = [
"//visibility:public",

View File

@ -1,340 +0,0 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import RLottieBinding
import AppBundle
import GZip
import SwiftSignalKit
private final class ManagedAnimationState {
let item: ManagedAnimationItem
private let instance: LottieInstance
let frameCount: Int
let fps: Double
var relativeTime: Double = 0.0
var frameIndex: Int?
private let renderContext: DrawingContext
init?(displaySize: CGSize, item: ManagedAnimationItem, current: ManagedAnimationState?) {
let resolvedInstance: LottieInstance
let renderContext: DrawingContext
if let current = current {
resolvedInstance = current.instance
renderContext = current.renderContext
} else {
guard let path = getAppBundle().path(forResource: item.name, ofType: "tgs") else {
return nil
}
guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else {
return nil
}
guard let unpackedData = TGGUnzipData(data, 5 * 1024 * 1024) else {
return nil
}
guard let instance = LottieInstance(data: unpackedData, cacheKey: item.name) else {
return nil
}
resolvedInstance = instance
renderContext = DrawingContext(size: displaySize, scale: UIScreenScale, premultiplied: true, clear: true)
}
self.item = item
self.instance = resolvedInstance
self.renderContext = renderContext
self.frameCount = Int(self.instance.frameCount)
self.fps = Double(self.instance.frameRate)
}
func draw() -> UIImage? {
self.instance.renderFrame(with: Int32(self.frameIndex ?? 0), into: self.renderContext.bytes.assumingMemoryBound(to: UInt8.self), width: Int32(self.renderContext.size.width * self.renderContext.scale), height: Int32(self.renderContext.size.height * self.renderContext.scale), bytesPerRow: Int32(self.renderContext.bytesPerRow))
return self.renderContext.generateImage()
}
}
struct ManagedAnimationFrameRange: Equatable {
var startFrame: Int
var endFrame: Int
}
struct ManagedAnimationItem: Equatable {
let name: String
var frames: ManagedAnimationFrameRange
var duration: Double
}
class ManagedAnimationNode: ASDisplayNode {
let intrinsicSize: CGSize
private let imageNode: ASImageNode
private let displayLink: CADisplayLink
fileprivate var state: ManagedAnimationState?
fileprivate var trackStack: [ManagedAnimationItem] = []
fileprivate var didTryAdvancingState = false
init(size: CGSize) {
self.intrinsicSize = size
self.imageNode = ASImageNode()
self.imageNode.displayWithoutProcessing = true
self.imageNode.displaysAsynchronously = false
self.imageNode.frame = CGRect(origin: CGPoint(), size: self.intrinsicSize)
final class DisplayLinkTarget: NSObject {
private let f: () -> Void
init(_ f: @escaping () -> Void) {
self.f = f
}
@objc func event() {
self.f()
}
}
var displayLinkUpdate: (() -> Void)?
self.displayLink = CADisplayLink(target: DisplayLinkTarget {
displayLinkUpdate?()
}, selector: #selector(DisplayLinkTarget.event))
super.init()
self.addSubnode(self.imageNode)
self.displayLink.add(to: RunLoop.main, forMode: .common)
displayLinkUpdate = { [weak self] in
self?.updateAnimation()
}
}
func advanceState() {
guard !self.trackStack.isEmpty else {
return
}
let item = self.trackStack.removeFirst()
if let state = self.state, state.item.name == item.name {
self.state = ManagedAnimationState(displaySize: self.intrinsicSize, item: item, current: state)
} else {
self.state = ManagedAnimationState(displaySize: self.intrinsicSize, item: item, current: nil)
}
self.didTryAdvancingState = false
}
fileprivate func updateAnimation() {
if self.state == nil {
self.advanceState()
}
guard let state = self.state else {
return
}
let timestamp = CACurrentMediaTime()
let fps = state.fps
let frameRange = state.item.frames
let duration: Double = state.item.duration
var t = state.relativeTime / duration
t = max(0.0, t)
t = min(1.0, t)
//print("\(t) \(state.item.name)")
let frameOffset = Int(Double(frameRange.startFrame) * (1.0 - t) + Double(frameRange.endFrame) * t)
let lowerBound: Int = 0
let upperBound = state.frameCount - 1
let frameIndex = max(lowerBound, min(upperBound, frameOffset))
if state.frameIndex != frameIndex {
state.frameIndex = frameIndex
if let image = state.draw() {
self.imageNode.image = image
}
}
var animationAdvancement: Double = 1.0 / 60.0
animationAdvancement *= Double(min(2, self.trackStack.count + 1))
state.relativeTime += animationAdvancement
if state.relativeTime >= duration && !self.didTryAdvancingState {
self.didTryAdvancingState = true
self.advanceState()
}
}
func trackTo(item: ManagedAnimationItem) {
self.trackStack.append(item)
self.didTryAdvancingState = false
self.updateAnimation()
}
}
enum ManagedMonkeyAnimationIdle: CaseIterable {
case blink
case ear
case still
}
enum ManagedMonkeyAnimationState: Equatable {
case idle(ManagedMonkeyAnimationIdle)
case eyesClosed
case peeking
case tracking(CGFloat)
}
final class ManagedMonkeyAnimationNode: ManagedAnimationNode {
private var monkeyState: ManagedMonkeyAnimationState = .idle(.blink)
private var timer: SwiftSignalKit.Timer?
init() {
super.init(size: CGSize(width: 136.0, height: 136.0))
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyIdle", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0), duration: 0.3))
}
deinit {
self.timer?.invalidate()
}
private func startIdleTimer() {
self.timer?.invalidate()
let timer = SwiftSignalKit.Timer(timeout: Double.random(in: 1.0 ..< 1.5), repeat: false, completion: { [weak self] in
guard let strongSelf = self else {
return
}
switch strongSelf.monkeyState {
case .idle:
if let idle = ManagedMonkeyAnimationIdle.allCases.randomElement() {
strongSelf.setState(.idle(idle))
}
default:
break
}
}, queue: .mainQueue())
self.timer = timer
timer.start()
}
override func advanceState() {
super.advanceState()
self.timer?.invalidate()
self.timer = nil
if self.trackStack.isEmpty, case .idle = self.monkeyState {
self.startIdleTimer()
}
}
private func enqueueIdle(_ idle: ManagedMonkeyAnimationIdle) {
switch idle {
case .still:
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyIdle", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0), duration: 0.3))
case .blink:
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyIdle1", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 30), duration: 0.3))
case .ear:
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyIdle2", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 30), duration: 0.3))
//self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyIdle", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 179), duration: 3.0))
}
}
func setState(_ monkeyState: ManagedMonkeyAnimationState) {
let previousState = self.monkeyState
self.monkeyState = monkeyState
self.timer?.invalidate()
self.timer = nil
func enqueueTracking(_ value: CGFloat) {
let lowerBound = 18
let upperBound = 160
let frameIndex = lowerBound + Int(value * CGFloat(upperBound - lowerBound))
if let state = self.state, state.item.name == "TwoFactorSetupMonkeyTracking" {
let item = ManagedAnimationItem(name: "TwoFactorSetupMonkeyTracking", frames: ManagedAnimationFrameRange(startFrame: state.frameIndex ?? 0, endFrame: frameIndex), duration: 0.3)
self.state = ManagedAnimationState(displaySize: self.intrinsicSize, item: item, current: state)
self.didTryAdvancingState = false
self.updateAnimation()
} else {
self.trackStack = self.trackStack.filter {
$0.name != "TwoFactorSetupMonkeyTracking"
}
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyTracking", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: frameIndex), duration: 0.3))
}
}
func enqueueClearTracking() {
if let state = self.state, state.item.name == "TwoFactorSetupMonkeyTracking" {
let item = ManagedAnimationItem(name: "TwoFactorSetupMonkeyTracking", frames: ManagedAnimationFrameRange(startFrame: state.frameIndex ?? 0, endFrame: 0), duration: 0.3)
self.state = ManagedAnimationState(displaySize: self.intrinsicSize, item: item, current: state)
self.didTryAdvancingState = false
self.updateAnimation()
}
}
switch previousState {
case let .idle(previousIdle):
switch monkeyState {
case let .idle(idle):
self.enqueueIdle(idle)
case .eyesClosed:
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyClose", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 41), duration: 0.3))
case .peeking:
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyCloseAndPeek", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 41), duration: 0.3))
case let .tracking(value):
enqueueTracking(value)
}
case .eyesClosed:
switch monkeyState {
case let .idle(idle):
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyClose", frames: ManagedAnimationFrameRange(startFrame: 41, endFrame: 0), duration: 0.3))
self.enqueueIdle(idle)
case .eyesClosed:
break
case .peeking:
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyPeek", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 14), duration: 0.3))
case let .tracking(value):
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyClose", frames: ManagedAnimationFrameRange(startFrame: 41, endFrame: 0), duration: 0.3))
enqueueTracking(value)
}
case .peeking:
switch monkeyState {
case let .idle(idle):
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyCloseAndPeek", frames: ManagedAnimationFrameRange(startFrame: 41, endFrame: 0), duration: 0.3))
self.enqueueIdle(idle)
case .eyesClosed:
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyPeek", frames: ManagedAnimationFrameRange(startFrame: 14, endFrame: 0), duration: 0.3))
case .peeking:
break
case let .tracking(value):
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyCloseAndPeek", frames: ManagedAnimationFrameRange(startFrame: 41, endFrame: 0), duration: 0.3))
enqueueTracking(value)
}
case let .tracking(currentValue):
switch monkeyState {
case let .idle(idle):
enqueueClearTracking()
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyIdle", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0), duration: 0.3))
self.enqueueIdle(idle)
case .eyesClosed:
enqueueClearTracking()
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyClose", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 41), duration: 0.3))
case .peeking:
enqueueClearTracking()
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyCloseAndPeek", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 41), duration: 0.3))
case let .tracking(value):
if abs(currentValue - value) > CGFloat.ulpOfOne {
enqueueTracking(value)
}
}
}
}
}

View File

@ -0,0 +1,170 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import RLottieBinding
import AppBundle
import GZip
import SwiftSignalKit
import ManagedAnimationNode
enum ManagedMonkeyAnimationIdle: CaseIterable {
case blink
case ear
case still
}
enum ManagedMonkeyAnimationState: Equatable {
case idle(ManagedMonkeyAnimationIdle)
case eyesClosed
case peeking
case tracking(CGFloat)
}
final class ManagedMonkeyAnimationNode: ManagedAnimationNode {
private var monkeyState: ManagedMonkeyAnimationState = .idle(.blink)
private var timer: SwiftSignalKit.Timer?
init() {
super.init(size: CGSize(width: 136.0, height: 136.0))
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyIdle"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0), duration: 0.3))
}
deinit {
self.timer?.invalidate()
}
private func startIdleTimer() {
self.timer?.invalidate()
let timer = SwiftSignalKit.Timer(timeout: Double.random(in: 1.0 ..< 1.5), repeat: false, completion: { [weak self] in
guard let strongSelf = self else {
return
}
switch strongSelf.monkeyState {
case .idle:
if let idle = ManagedMonkeyAnimationIdle.allCases.randomElement() {
strongSelf.setState(.idle(idle))
}
default:
break
}
}, queue: .mainQueue())
self.timer = timer
timer.start()
}
override func advanceState() {
super.advanceState()
self.timer?.invalidate()
self.timer = nil
if self.trackStack.isEmpty, case .idle = self.monkeyState {
self.startIdleTimer()
}
}
private func enqueueIdle(_ idle: ManagedMonkeyAnimationIdle) {
switch idle {
case .still:
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyIdle"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0), duration: 0.3))
case .blink:
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyIdle1"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 30), duration: 0.3))
case .ear:
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyIdle2"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 30), duration: 0.3))
//self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyIdle"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 179), duration: 3.0))
}
}
func setState(_ monkeyState: ManagedMonkeyAnimationState) {
let previousState = self.monkeyState
self.monkeyState = monkeyState
self.timer?.invalidate()
self.timer = nil
func enqueueTracking(_ value: CGFloat) {
let lowerBound = 18
let upperBound = 160
let frameIndex = lowerBound + Int(value * CGFloat(upperBound - lowerBound))
if let state = self.state, state.item.source == .local("TwoFactorSetupMonkeyTracking") {
let item = ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyTracking"), frames: ManagedAnimationFrameRange(startFrame: state.frameIndex ?? 0, endFrame: frameIndex), duration: 0.3)
self.state = ManagedAnimationState(displaySize: self.intrinsicSize, item: item, current: state)
self.didTryAdvancingState = false
self.updateAnimation()
} else {
self.trackStack = self.trackStack.filter {
$0.source != .local("TwoFactorSetupMonkeyTracking")
}
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyTracking"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: frameIndex), duration: 0.3))
}
}
func enqueueClearTracking() {
if let state = self.state, state.item.source == .local("TwoFactorSetupMonkeyTracking") {
let item = ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyTracking"), frames: ManagedAnimationFrameRange(startFrame: state.frameIndex ?? 0, endFrame: 0), duration: 0.3)
self.state = ManagedAnimationState(displaySize: self.intrinsicSize, item: item, current: state)
self.didTryAdvancingState = false
self.updateAnimation()
}
}
switch previousState {
case let .idle(previousIdle):
switch monkeyState {
case let .idle(idle):
self.enqueueIdle(idle)
case .eyesClosed:
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyClose"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 41), duration: 0.3))
case .peeking:
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyCloseAndPeek"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 41), duration: 0.3))
case let .tracking(value):
enqueueTracking(value)
}
case .eyesClosed:
switch monkeyState {
case let .idle(idle):
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyClose"), frames: ManagedAnimationFrameRange(startFrame: 41, endFrame: 0), duration: 0.3))
self.enqueueIdle(idle)
case .eyesClosed:
break
case .peeking:
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyPeek"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 14), duration: 0.3))
case let .tracking(value):
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyClose"), frames: ManagedAnimationFrameRange(startFrame: 41, endFrame: 0), duration: 0.3))
enqueueTracking(value)
}
case .peeking:
switch monkeyState {
case let .idle(idle):
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyCloseAndPeek"), frames: ManagedAnimationFrameRange(startFrame: 41, endFrame: 0), duration: 0.3))
self.enqueueIdle(idle)
case .eyesClosed:
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyPeek"), frames: ManagedAnimationFrameRange(startFrame: 14, endFrame: 0), duration: 0.3))
case .peeking:
break
case let .tracking(value):
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyCloseAndPeek"), frames: ManagedAnimationFrameRange(startFrame: 41, endFrame: 0), duration: 0.3))
enqueueTracking(value)
}
case let .tracking(currentValue):
switch monkeyState {
case let .idle(idle):
enqueueClearTracking()
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyIdle"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0), duration: 0.3))
self.enqueueIdle(idle)
case .eyesClosed:
enqueueClearTracking()
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyClose"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 41), duration: 0.3))
case .peeking:
enqueueClearTracking()
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyCloseAndPeek"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 41), duration: 0.3))
case let .tracking(value):
if abs(currentValue - value) > CGFloat.ulpOfOne {
enqueueTracking(value)
}
}
}
}
}

View File

@ -24,6 +24,7 @@ static_library(
"//submodules/PhotoResources:PhotoResources",
"//submodules/GraphCore:GraphCore",
"//submodules/GraphUI:GraphUI",
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
],
frameworks = [
"$SDKROOT/System/Library/Frameworks/Foundation.framework",

View File

@ -25,6 +25,7 @@ swift_library(
"//submodules/PhotoResources:PhotoResources",
"//submodules/GraphCore:GraphCore",
"//submodules/GraphUI:GraphUI",
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
],
visibility = [
"//visibility:public",

View File

@ -296,36 +296,36 @@ private enum StatsEntry: ItemListNodeEntry {
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! StatsControllerArguments
switch self {
case let .overviewHeader(theme, text, dates):
case let .overviewHeader(_, text, dates):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, accessoryText: ItemListSectionHeaderAccessoryText(value: dates, color: .generic), sectionId: self.section)
case let .growthTitle(theme, text),
let .followersTitle(theme, text),
let .notificationsTitle(theme, text),
let .viewsByHourTitle(theme, text),
let .viewsBySourceTitle(theme, text),
let .followersBySourceTitle(theme, text),
let .languagesTitle(theme, text),
let .postInteractionsTitle(theme, text),
let .postsTitle(theme, text),
let .instantPageInteractionsTitle(theme, text):
case let .growthTitle(_, text),
let .followersTitle(_, text),
let .notificationsTitle(_, text),
let .viewsByHourTitle(_, text),
let .viewsBySourceTitle(_, text),
let .followersBySourceTitle(_, text),
let .languagesTitle(_, text),
let .postInteractionsTitle(_, text),
let .postsTitle(_, text),
let .instantPageInteractionsTitle(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .overview(theme, stats):
case let .overview(_, stats):
return StatsOverviewItem(presentationData: presentationData, stats: stats, sectionId: self.section, style: .blocks)
case let .growthGraph(theme, strings, dateTimeFormat, graph, type):
case let .growthGraph(_, _, _, graph, type):
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, sectionId: self.section, style: .blocks)
case let .followersGraph(theme, strings, dateTimeFormat, graph, type):
case let .followersGraph(_, _, _, graph, type):
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, sectionId: self.section, style: .blocks)
case let .notificationsGraph(theme, strings, dateTimeFormat, graph, type):
case let .notificationsGraph(_, _, _, graph, type):
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, sectionId: self.section, style: .blocks)
case let .viewsByHourGraph(theme, strings, dateTimeFormat, graph, type):
case let .viewsByHourGraph(_, _, _, graph, type):
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, sectionId: self.section, style: .blocks)
case let .viewsBySourceGraph(theme, strings, dateTimeFormat, graph, type):
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, height: 160.0, sectionId: self.section, style: .blocks)
case let .followersBySourceGraph(theme, strings, dateTimeFormat, graph, type):
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, height: 160.0, sectionId: self.section, style: .blocks)
case let .languagesGraph(theme, strings, dateTimeFormat, graph, type):
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, height: 100.0, sectionId: self.section, style: .blocks)
case let .postInteractionsGraph(theme, strings, dateTimeFormat, graph, type):
case let .viewsBySourceGraph(_, _, _, graph, type):
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, sectionId: self.section, style: .blocks)
case let .followersBySourceGraph(_, _, _, graph, type):
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, sectionId: self.section, style: .blocks)
case let .languagesGraph(_, _, _, graph, type):
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, sectionId: self.section, style: .blocks)
case let .postInteractionsGraph(_, _, _, graph, type):
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, getDetailsData: { date, completion in
let _ = arguments.loadDetailedGraph(graph, Int64(date.timeIntervalSince1970) * 1000).start(next: { graph in
if let graph = graph, case let .Loaded(_, data) = graph {
@ -333,18 +333,18 @@ private enum StatsEntry: ItemListNodeEntry {
}
})
}, sectionId: self.section, style: .blocks)
case let .post(index, theme, strings, dateTimeFormat, message, interactions):
case let .instantPageInteractionsGraph(_, _, _, graph, type):
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, getDetailsData: { date, completion in
let _ = arguments.loadDetailedGraph(graph, Int64(date.timeIntervalSince1970) * 1000).start(next: { graph in
if let graph = graph, case let .Loaded(_, data) = graph {
completion(data)
}
})
}, sectionId: self.section, style: .blocks)
case let .post(_, _, _, _, message, interactions):
return StatsMessageItem(context: arguments.context, presentationData: presentationData, message: message, views: interactions.views, forwards: interactions.forwards, sectionId: self.section, style: .blocks, action: {
arguments.openMessage(message.id)
})
case let .instantPageInteractionsGraph(theme, strings, dateTimeFormat, graph, type):
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, getDetailsData: { date, completion in
let _ = arguments.loadDetailedGraph(graph, Int64(date.timeIntervalSince1970) * 1000).start(next: { graph in
if let graph = graph, case let .Loaded(_, data) = graph {
completion(data)
}
})
}, sectionId: self.section, style: .blocks)
}
}
}
@ -368,32 +368,37 @@ private func statsControllerEntries(data: ChannelStats?, messages: [Message]?, i
entries.append(.followersTitle(presentationData.theme, presentationData.strings.Stats_FollowersTitle))
entries.append(.followersGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.followersGraph, .lines))
}
if !data.muteGraph.isEmpty {
entries.append(.notificationsTitle(presentationData.theme, presentationData.strings.Stats_NotificationsTitle))
entries.append(.notificationsGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.muteGraph, .lines))
}
if !data.topHoursGraph.isEmpty {
entries.append(.viewsByHourTitle(presentationData.theme, presentationData.strings.Stats_ViewsByHoursTitle))
entries.append(.viewsByHourGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.topHoursGraph, .hourlyStep))
}
if !data.viewsBySourceGraph.isEmpty {
entries.append(.viewsBySourceTitle(presentationData.theme, presentationData.strings.Stats_ViewsBySourceTitle))
entries.append(.viewsBySourceGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.viewsBySourceGraph, .bars))
}
if !data.newFollowersBySourceGraph.isEmpty {
entries.append(.followersBySourceTitle(presentationData.theme, presentationData.strings.Stats_FollowersBySourceTitle))
entries.append(.followersBySourceGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.newFollowersBySourceGraph, .bars))
}
if !data.languagesGraph.isEmpty {
entries.append(.languagesTitle(presentationData.theme, presentationData.strings.Stats_LanguagesTitle))
entries.append(.languagesGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.languagesGraph, .pie))
}
if !data.interactionsGraph.isEmpty {
entries.append(.postInteractionsTitle(presentationData.theme, presentationData.strings.Stats_InteractionsTitle))
entries.append(.postInteractionsGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.interactionsGraph, .step))
entries.append(.postInteractionsGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.interactionsGraph, .twoAxisStep))
}
if let messages = messages, !messages.isEmpty, let interactions = interactions, !interactions.isEmpty {
entries.append(.postsTitle(presentationData.theme, presentationData.strings.Stats_PostsTitle))
var index: Int32 = 0
@ -404,7 +409,7 @@ private func statsControllerEntries(data: ChannelStats?, messages: [Message]?, i
}
}
}
if !data.instantPageInteractionsGraph.isEmpty {
entries.append(.instantPageInteractionsTitle(presentationData.theme, presentationData.strings.Stats_InstantViewInteractionsTitle))
entries.append(.instantPageInteractionsGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.instantPageInteractionsGraph, .step))
@ -439,6 +444,7 @@ public func channelStatsController(context: AccountContext, peerId: PeerId, cach
if let statsContext = statsContext, let stats = stats {
if case .OnDemand = stats.interactionsGraph {
statsContext.loadInteractionsGraph()
statsContext.loadTopHoursGraph()
statsContext.loadNewFollowersBySourceGraph()
statsContext.loadViewsBySourceGraph()
statsContext.loadLanguagesGraph()
@ -460,12 +466,21 @@ public func channelStatsController(context: AccountContext, peerId: PeerId, cach
}
messagesPromise.set(.single(nil) |> then(messageView))
let signal = combineLatest(context.sharedContext.presentationData, dataPromise.get(), messagesPromise.get())
let longLoadingSignal: Signal<Bool, NoError> = .single(false) |> then(.single(true) |> delay(1.5, queue: Queue.mainQueue()))
let previousData = Atomic<ChannelStats?>(value: nil)
let signal = combineLatest(context.sharedContext.presentationData, dataPromise.get(), messagesPromise.get(), longLoadingSignal)
|> deliverOnMainQueue
|> map { presentationData, data, messageView -> (ItemListControllerState, (ItemListNodeState, Any)) in
|> map { presentationData, data, messageView, longLoading -> (ItemListControllerState, (ItemListNodeState, Any)) in
let previous = previousData.swap(data)
var emptyStateItem: ItemListControllerEmptyStateItem?
if data == nil {
emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme)
if longLoading {
emptyStateItem = StatsEmptyStateItem(theme: presentationData.theme, strings: presentationData.strings)
} else {
emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme)
}
}
let messages = messageView?.entries.map { $0.message }.sorted(by: { (lhsMessage, rhsMessage) -> Bool in
@ -478,7 +493,7 @@ public func channelStatsController(context: AccountContext, peerId: PeerId, cach
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.ChannelInfo_Stats), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: statsControllerEntries(data: data, messages: messages, interactions: interactions, presentationData: presentationData), style: .blocks, emptyStateItem: emptyStateItem, crossfadeState: false, animateChanges: false)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: statsControllerEntries(data: data, messages: messages, interactions: interactions, presentationData: presentationData), style: .blocks, emptyStateItem: emptyStateItem, crossfadeState: previous == nil, animateChanges: false)
return (controllerState, (listState, arguments))
}

View File

@ -0,0 +1,107 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AnimatedStickerNode
import AppBundle
final class StatsEmptyStateItem: ItemListControllerEmptyStateItem {
let theme: PresentationTheme
let strings: PresentationStrings
init(theme: PresentationTheme, strings: PresentationStrings) {
self.theme = theme
self.strings = strings
}
func isEqual(to: ItemListControllerEmptyStateItem) -> Bool {
if let item = to as? StatsEmptyStateItem {
return self.theme === item.theme && self.strings === item.strings
} else {
return false
}
}
func node(current: ItemListControllerEmptyStateItemNode?) -> ItemListControllerEmptyStateItemNode {
if let current = current as? StatsEmptyStateItemNode {
current.item = self
return current
} else {
return StatsEmptyStateItemNode(item: self)
}
}
}
final class StatsEmptyStateItemNode: ItemListControllerEmptyStateItemNode {
private var animationNode: AnimatedStickerNode
private let titleNode: ASTextNode
private let textNode: ASTextNode
private var validLayout: (ContainerViewLayout, CGFloat)?
var item: StatsEmptyStateItem {
didSet {
self.updateThemeAndStrings(theme: self.item.theme, strings: self.item.strings)
if let (layout, navigationHeight) = self.validLayout {
self.updateLayout(layout: layout, navigationBarHeight: navigationHeight, transition: .immediate)
}
}
}
init(item: StatsEmptyStateItem) {
self.item = item
self.animationNode = AnimatedStickerNode()
if let path = getAppBundle().path(forResource: "Charts", ofType: "tgs") {
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 192, height: 192, playbackMode: .once, mode: .direct)
self.animationNode.visibility = true
}
self.titleNode = ASTextNode()
self.titleNode.isUserInteractionEnabled = false
self.textNode = ASTextNode()
self.textNode.isUserInteractionEnabled = false
super.init()
self.addSubnode(self.animationNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.updateThemeAndStrings(theme: self.item.theme, strings: self.item.strings)
}
private func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
self.titleNode.attributedText = NSAttributedString(string: strings.Stats_LoadingTitle, font: Font.bold(17.0), textColor: theme.list.freeTextColor, paragraphAlignment: .center)
self.textNode.attributedText = NSAttributedString(string: strings.Stats_LoadingText, font: Font.regular(14.0), textColor: theme.list.freeTextColor, paragraphAlignment: .center)
}
override func updateLayout(layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.validLayout = (layout, navigationBarHeight)
var insets = layout.insets(options: [])
insets.top += navigationBarHeight
let imageSpacing: CGFloat = 20.0
let textSpacing: CGFloat = 8.0
let imageSize = CGSize(width: 96.0, height: 96.0)
let imageHeight = layout.size.width < layout.size.height ? imageSize.height + imageSpacing : 0.0
self.animationNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - imageSize.width) / 2.0), y: -10.0), size: imageSize)
self.animationNode.updateLayout(size: imageSize)
let titleSize = self.titleNode.measure(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - layout.intrinsicInsets.left - layout.intrinsicInsets.right - 50.0, height: max(1.0, layout.size.height - insets.top - insets.bottom)))
let textSize = self.textNode.measure(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - layout.intrinsicInsets.left - layout.intrinsicInsets.right - 50.0, height: max(1.0, layout.size.height - insets.top - insets.bottom)))
let totalHeight = imageHeight + titleSize.height + textSpacing + textSize.height
let topOffset = insets.top + floor((layout.size.height - insets.top - insets.bottom - totalHeight) / 2.0)
transition.updateAlpha(node: self.animationNode, alpha: imageHeight > 0.0 ? 1.0 : 0.0)
transition.updateFrame(node: self.animationNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - imageSize.width) / 2.0), y: topOffset), size: imageSize))
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - titleSize.width - layout.safeInsets.left - layout.safeInsets.right - layout.intrinsicInsets.left - layout.intrinsicInsets.right) / 2.0), y: topOffset + imageHeight), size: titleSize))
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - textSize.width - layout.safeInsets.left - layout.safeInsets.right - layout.intrinsicInsets.left - layout.intrinsicInsets.right) / 2.0), y: self.titleNode.frame.maxY + textSpacing), size: textSize))
}
}

View File

@ -16,16 +16,14 @@ class StatsGraphItem: ListViewItem, ItemListItem {
let presentationData: ItemListPresentationData
let graph: ChannelStatsGraph
let type: ChartType
let height: CGFloat
let getDetailsData: ((Date, @escaping (String?) -> Void) -> Void)?
let sectionId: ItemListSectionId
let style: ItemListStyle
init(presentationData: ItemListPresentationData, graph: ChannelStatsGraph, type: ChartType, height: CGFloat = 0.0, getDetailsData: ((Date, @escaping (String?) -> Void) -> Void)? = nil, sectionId: ItemListSectionId, style: ItemListStyle) {
init(presentationData: ItemListPresentationData, graph: ChannelStatsGraph, type: ChartType, getDetailsData: ((Date, @escaping (String?) -> Void) -> Void)? = nil, sectionId: ItemListSectionId, style: ItemListStyle) {
self.presentationData = presentationData
self.graph = graph
self.type = type
self.height = height
self.getDetailsData = getDetailsData
self.sectionId = sectionId
self.style = style
@ -72,11 +70,13 @@ class StatsGraphItemNode: ListViewItemNode {
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private let chartContainerNode: ASDisplayNode
let chartNode: ChartNode
private let activityIndicator: ActivityIndicator
private var item: StatsGraphItem?
private var visibilityHeight: CGFloat?
init() {
self.backgroundNode = ASDisplayNode()
@ -84,41 +84,46 @@ class StatsGraphItemNode: ListViewItemNode {
self.backgroundNode.backgroundColor = .white
self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.chartContainerNode = ASDisplayNode()
self.chartContainerNode.clipsToBounds = true
self.chartContainerNode.isUserInteractionEnabled = true
self.chartNode = ChartNode()
self.activityIndicator = ActivityIndicator(type: ActivityIndicatorType.custom(.black, 16.0, 2.0, false))
self.activityIndicator.isHidden = true
super.init(layerBacked: false, dynamicBounce: false)
self.clipsToBounds = true
self.addSubnode(self.chartNode)
self.addSubnode(self.activityIndicator)
self.chartContainerNode.addSubnode(self.chartNode)
self.chartContainerNode.addSubnode(self.activityIndicator)
}
override func didLoad() {
super.didLoad()
self.chartNode.view.interactiveTransitionGestureRecognizerTest = { point -> Bool in
return point.x > 30.0
self.view.interactiveTransitionGestureRecognizerTest = { point -> Bool in
return point.x > 30.0 || (point.y > 250.0 && point.y < 295.0)
}
}
func asyncLayout() -> (_ item: StatsGraphItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentItem = self.item
let currentVisibilityHeight = self.visibilityHeight
return { item, params, neighbors in
let leftInset = params.leftInset
let rightInset: CGFloat = params.rightInset
var updatedTheme: PresentationTheme?
var updatedGraph: ChannelStatsGraph?
var updatedController: BaseChartController?
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
@ -126,9 +131,16 @@ class StatsGraphItemNode: ListViewItemNode {
if currentItem?.graph != item.graph {
updatedGraph = item.graph
if case let .Loaded(_, data) = updatedGraph {
updatedController = createChartController(data, type: item.type, getDetailsData: { [weak self] date, completion in
if let strongSelf = self, let item = strongSelf.item {
item.getDetailsData?(date, completion)
}
})
}
}
let contentSize: CGSize
var contentSize: CGSize
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let itemBackgroundColor: UIColor
@ -138,20 +150,39 @@ class StatsGraphItemNode: ListViewItemNode {
case .plain:
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor
contentSize = CGSize(width: params.width, height: 350.0 + item.height)
contentSize = CGSize(width: params.width, height: 301.0)
insets = itemListNeighborsPlainInsets(neighbors)
case .blocks:
itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
contentSize = CGSize(width: params.width, height: 350.0 + item.height)
contentSize = CGSize(width: params.width, height: 301.0)
insets = itemListNeighborsGroupedInsets(neighbors)
}
var visibilityHeight = currentVisibilityHeight
if let updatedController = updatedController {
var height: CGFloat = 0.0
var items: [ChartVisibilityItem] = []
for item in updatedController.actualChartsCollection.chartValues {
items.append(ChartVisibilityItem(title: item.name, color: .black))
}
if items.count > 1 {
height = calculateVisiblityHeight(width: params.width - params.leftInset - params.rightInset, items: items)
}
if item.type == .hourlyStep {
height -= 42.0
}
visibilityHeight = height
}
if let visibilityHeight = visibilityHeight {
contentSize.height += visibilityHeight
}
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.visibilityHeight = visibilityHeight
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
@ -178,14 +209,17 @@ class StatsGraphItemNode: ListViewItemNode {
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.chartContainerNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.chartContainerNode, at: 1)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 2)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 3)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
strongSelf.insertSubnode(strongSelf.maskNode, at: 4)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
@ -207,7 +241,8 @@ class StatsGraphItemNode: ListViewItemNode {
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.chartNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: layout.size.width - leftInset - rightInset, height: 750.0))
strongSelf.chartContainerNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: layout.size.width - leftInset - rightInset, height: contentSize.height))
strongSelf.chartNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width - leftInset - rightInset, height: 750.0))
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
@ -220,13 +255,13 @@ class StatsGraphItemNode: ListViewItemNode {
strongSelf.activityIndicator.type = .custom(item.presentationData.theme.list.itemSecondaryTextColor, 16.0, 2.0, false)
if let updatedTheme = updatedTheme {
strongSelf.chartNode.setupTheme(ChartTheme(presentationTheme: updatedTheme))
}
if let updatedGraph = updatedGraph {
if case let .Loaded(_, data) = updatedGraph {
strongSelf.chartNode.setup(data, type: item.type, getDetailsData: { [weak self] date, completion in
if let strongSelf = self, let item = strongSelf.item {
item.getDetailsData?(date, completion)
}
})
if case let .Loaded(_, data) = updatedGraph, let updatedController = updatedController {
strongSelf.chartNode.setup(controller: updatedController)
strongSelf.activityIndicator.isHidden = true
strongSelf.chartNode.isHidden = false
} else if case .OnDemand = updatedGraph {
@ -234,10 +269,6 @@ class StatsGraphItemNode: ListViewItemNode {
strongSelf.chartNode.isHidden = true
}
}
if let updatedTheme = updatedTheme {
strongSelf.chartNode.setupTheme(ChartTheme(presentationTheme: updatedTheme))
}
}
})
}

View File

@ -170,15 +170,16 @@ public class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
let itemBackgroundColor: UIColor
let itemSeparatorColor: UIColor
var leftInset = 16.0 + params.leftInset
var rightInset = 16.0 + params.rightInset
let leftInset = 16.0 + params.leftInset
let rightInset = 16.0 + params.rightInset
var totalLeftInset = leftInset
var additionalRightInset: CGFloat = 93.0
let additionalRightInset: CGFloat = 93.0
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
let contentKind = messageContentKind(contentSettings: item.context.currentContentSettings.with { $0 }, message: item.message, strings: item.presentationData.strings, nameDisplayOrder: .firstLast, accountPeerId: item.context.account.peerId)
let text = stringForMediaKind(contentKind, strings: item.presentationData.strings).0
var text = stringForMediaKind(contentKind, strings: item.presentationData.strings).0
text = foldLineBreaks(text)
var contentImageMedia: Media?
for media in item.message.media {

View File

@ -156,26 +156,26 @@ class StatsOverviewItemNode: ListViewItemNode {
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseHeaderFontSize)
let deltaFont = Font.regular(item.presentationData.fontSize.itemListBaseHeaderFontSize)
let (followersValueLabelLayout, followersValueLabelApply) = makeFollowersValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: compactNumericCountString(Int(item.stats.followers.current)), font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 140.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (followersValueLabelLayout, followersValueLabelApply) = makeFollowersValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: compactNumericCountString(Int(item.stats.followers.current)), font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (viewsPerPostValueLabelLayout, viewsPerPostValueLabelApply) = makeViewsPerPostValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: compactNumericCountString(Int(item.stats.viewsPerPost.current)), font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 140.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (viewsPerPostValueLabelLayout, viewsPerPostValueLabelApply) = makeViewsPerPostValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.stats.viewsPerPost.current > 0 ? compactNumericCountString(Int(item.stats.viewsPerPost.current)) : "", font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (sharesPerPostValueLabelLayout, sharesPerPostValueLabelApply) = makeSharesPerPostValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: compactNumericCountString(Int(item.stats.sharesPerPost.current)), font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (sharesPerPostValueLabelLayout, sharesPerPostValueLabelApply) = makeSharesPerPostValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.stats.sharesPerPost.current > 0 ? compactNumericCountString(Int(item.stats.sharesPerPost.current)) : "", font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
var enabledNotifications: Double = 0.0
if item.stats.enabledNotifications.total > 0 {
enabledNotifications = item.stats.enabledNotifications.value / item.stats.enabledNotifications.total
}
let (enabledNotificationsValueLabelLayout, enabledNotificationsValueLabelApply) = makeEnabledNotificationsValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: String(format: "%.02f%%", enabledNotifications * 100.0), font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (enabledNotificationsValueLabelLayout, enabledNotificationsValueLabelApply) = makeEnabledNotificationsValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: String(format: "%.02f%%", enabledNotifications * 100.0), font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (followersTitleLabelLayout, followersTitleLabelApply) = makeFollowersTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_Followers, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 140.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (followersTitleLabelLayout, followersTitleLabelApply) = makeFollowersTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_Followers, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (viewsPerPostTitleLabelLayout, viewsPerPostTitleLabelApply) = makeViewsPerPostTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_ViewsPerPost, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 140.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (viewsPerPostTitleLabelLayout, viewsPerPostTitleLabelApply) = makeViewsPerPostTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.stats.viewsPerPost.current > 0 ? item.presentationData.strings.Stats_ViewsPerPost : "", font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (sharesPerPostTitleLabelLayout, sharesPerPostTitleLabelApply) = makeSharesPerPostTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_SharesPerPost, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 140.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (sharesPerPostTitleLabelLayout, sharesPerPostTitleLabelApply) = makeSharesPerPostTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.stats.sharesPerPost.current > 0 ? item.presentationData.strings.Stats_SharesPerPost : "", font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (enabledNotificationsTitleLabelLayout, enabledNotificationsTitleLabelApply) = makeEnabledNotificationsTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_EnabledNotifications, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 140.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (enabledNotificationsTitleLabelLayout, enabledNotificationsTitleLabelApply) = makeEnabledNotificationsTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_EnabledNotifications, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let followersDeltaValue = item.stats.followers.current - item.stats.followers.previous
let followersDeltaCompact = compactNumericCountString(abs(Int(followersDeltaValue)))
@ -185,7 +185,9 @@ class StatsOverviewItemNode: ListViewItemNode {
followersDeltaPercentage = abs(followersDeltaValue / item.stats.followers.previous)
}
let (followersDeltaLabelLayout, followersDeltaLabelApply) = makeFollowersDeltaLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: String(format: "%@ (%.02f%%)", followersDelta, followersDeltaPercentage * 100.0), font: deltaFont, textColor: followersDeltaValue > 0.0 ? item.presentationData.theme.list.freeTextSuccessColor : item.presentationData.theme.list.freeTextErrorColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 140.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let followersDeltaText = abs(followersDeltaPercentage) > 0.0 ? String(format: "%@ (%.02f%%)", followersDelta, followersDeltaPercentage * 100.0) : ""
let (followersDeltaLabelLayout, followersDeltaLabelApply) = makeFollowersDeltaLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: followersDeltaText, font: deltaFont, textColor: followersDeltaValue > 0.0 ? item.presentationData.theme.list.freeTextSuccessColor : item.presentationData.theme.list.freeTextErrorColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let viewsPerPostDeltaValue = item.stats.viewsPerPost.current - item.stats.viewsPerPost.previous
let viewsPerPostDeltaCompact = compactNumericCountString(abs(Int(viewsPerPostDeltaValue)))
@ -195,7 +197,9 @@ class StatsOverviewItemNode: ListViewItemNode {
viewsPerPostDeltaPercentage = abs(viewsPerPostDeltaValue / item.stats.viewsPerPost.previous)
}
let (viewsPerPostDeltaLabelLayout, viewsPerPostDeltaLabelApply) = makeViewsPerPostDeltaLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: String(format: "%@ (%.02f%%)", viewsPerPostDelta, viewsPerPostDeltaPercentage * 100.0), font: deltaFont, textColor: viewsPerPostDeltaValue > 0.0 ? item.presentationData.theme.list.freeTextSuccessColor : item.presentationData.theme.list.freeTextErrorColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 140.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let viewsPerPostDeltaText = abs(viewsPerPostDeltaPercentage) > 0.0 ? String(format: "%@ (%.02f%%)", viewsPerPostDelta, viewsPerPostDeltaPercentage * 100.0) : ""
let (viewsPerPostDeltaLabelLayout, viewsPerPostDeltaLabelApply) = makeViewsPerPostDeltaLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: viewsPerPostDeltaText, font: deltaFont, textColor: viewsPerPostDeltaValue > 0.0 ? item.presentationData.theme.list.freeTextSuccessColor : item.presentationData.theme.list.freeTextErrorColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let sharesPerPostDeltaValue = item.stats.sharesPerPost.current - item.stats.sharesPerPost.previous
let sharesPerPostDeltaCompact = compactNumericCountString(abs(Int(sharesPerPostDeltaValue)))
@ -205,7 +209,9 @@ class StatsOverviewItemNode: ListViewItemNode {
sharesPerPostDeltaPercentage = abs(sharesPerPostDeltaValue / item.stats.sharesPerPost.previous)
}
let (sharesPerPostDeltaLabelLayout, sharesPerPostDeltaLabelApply) = makeSharesPerPostDeltaLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: String(format: "%@ (%.02f%%)", sharesPerPostDelta, sharesPerPostDeltaPercentage * 100.0), font: deltaFont, textColor: sharesPerPostDeltaValue > 0.0 ? item.presentationData.theme.list.freeTextSuccessColor : item.presentationData.theme.list.freeTextErrorColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 140.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let sharesPerPostDeltaText = abs(sharesPerPostDeltaPercentage) > 0.0 ? String(format: "%@ (%.02f%%)", sharesPerPostDelta, sharesPerPostDeltaPercentage * 100.0) : ""
let (sharesPerPostDeltaLabelLayout, sharesPerPostDeltaLabelApply) = makeSharesPerPostDeltaLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: sharesPerPostDeltaText, font: deltaFont, textColor: sharesPerPostDeltaValue > 0.0 ? item.presentationData.theme.list.freeTextSuccessColor : item.presentationData.theme.list.freeTextErrorColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let contentSize: CGSize
let insets: UIEdgeInsets
@ -213,11 +219,35 @@ class StatsOverviewItemNode: ListViewItemNode {
let itemBackgroundColor: UIColor
let itemSeparatorColor: UIColor
let height: CGFloat
if item.stats.viewsPerPost.current.isZero && item.stats.sharesPerPost.current.isZero {
height = 64.0
let horizontalSpacing: CGFloat = 4.0
let verticalSpacing: CGFloat = 18.0
let topInset: CGFloat = 14.0
let sideInset: CGFloat = 16.0
var height: CGFloat = topInset * 2.0
height += enabledNotificationsValueLabelLayout.size.height + enabledNotificationsTitleLabelLayout.size.height
var twoColumnLayout = true
if max(followersValueLabelLayout.size.width + followersDeltaLabelLayout.size.width + horizontalSpacing + enabledNotificationsValueLabelLayout.size.width, viewsPerPostValueLabelLayout.size.width + viewsPerPostDeltaLabelLayout.size.width + horizontalSpacing + sharesPerPostValueLabelLayout.size.width + sharesPerPostDeltaLabelLayout.size.width) > params.width - leftInset - rightInset {
twoColumnLayout = false
}
if twoColumnLayout {
if !item.stats.viewsPerPost.current.isZero || !item.stats.sharesPerPost.current.isZero {
height += verticalSpacing
height += sharesPerPostValueLabelLayout.size.height + sharesPerPostTitleLabelLayout.size.height
}
} else {
height = 120.0
height += verticalSpacing
height += enabledNotificationsValueLabelLayout.size.height + enabledNotificationsTitleLabelLayout.size.height
if !item.stats.viewsPerPost.current.isZero {
height += verticalSpacing
height += viewsPerPostValueLabelLayout.size.height + viewsPerPostTitleLabelLayout.size.height
}
if !item.stats.sharesPerPost.current.isZero {
height += verticalSpacing
height += sharesPerPostValueLabelLayout.size.height + sharesPerPostTitleLabelLayout.size.height
}
}
switch item.style {
@ -316,28 +346,25 @@ class StatsOverviewItemNode: ListViewItemNode {
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight))
}
let horizontalSpacing: CGFloat = 4.0
let verticalSpacing: CGFloat = 70.0
let topInset: CGFloat = 14.0
let sideInset: CGFloat = 16.0
strongSelf.followersValueLabel.frame = CGRect(origin: CGPoint(x: sideInset + leftInset, y: topInset), size: followersValueLabelLayout.size)
strongSelf.followersTitleLabel.frame = CGRect(origin: CGPoint(x: sideInset + leftInset, y: strongSelf.followersValueLabel.frame.maxY), size: followersTitleLabelLayout.size)
strongSelf.followersDeltaLabel.frame = CGRect(origin: CGPoint(x: strongSelf.followersValueLabel.frame.maxX + horizontalSpacing, y: strongSelf.followersValueLabel.frame.minY + 4.0), size: followersDeltaLabelLayout.size)
strongSelf.followersDeltaLabel.frame = CGRect(origin: CGPoint(x: strongSelf.followersValueLabel.frame.maxX + horizontalSpacing, y: strongSelf.followersValueLabel.frame.maxY - followersDeltaLabelLayout.size.height - 2.0), size: followersDeltaLabelLayout.size)
strongSelf.viewsPerPostValueLabel.frame = CGRect(origin: CGPoint(x: sideInset + leftInset, y: verticalSpacing), size: viewsPerPostValueLabelLayout.size)
let secondColumnX = twoColumnLayout ? max(layout.size.width / 2.0, sideInset + leftInset + max(followersValueLabelLayout.size.width + followersDeltaLabelLayout.size.width, viewsPerPostValueLabelLayout.size.width + viewsPerPostDeltaLabelLayout.size.width) + horizontalSpacing) : sideInset + leftInset
let enabledNotificationsY = twoColumnLayout ? topInset : strongSelf.followersTitleLabel.frame.maxY + verticalSpacing
strongSelf.enabledNotificationsValueLabel.frame = CGRect(origin: CGPoint(x: secondColumnX, y: enabledNotificationsY), size: enabledNotificationsValueLabelLayout.size)
strongSelf.enabledNotificationsTitleLabel.frame = CGRect(origin: CGPoint(x: secondColumnX, y: strongSelf.enabledNotificationsValueLabel.frame.maxY), size: enabledNotificationsTitleLabelLayout.size)
let viewsPerPostY = twoColumnLayout ? strongSelf.followersTitleLabel.frame.maxY + verticalSpacing : strongSelf.enabledNotificationsTitleLabel.frame.maxY + verticalSpacing
strongSelf.viewsPerPostValueLabel.frame = CGRect(origin: CGPoint(x: sideInset + leftInset, y: viewsPerPostY), size: viewsPerPostValueLabelLayout.size)
strongSelf.viewsPerPostTitleLabel.frame = CGRect(origin: CGPoint(x: sideInset + leftInset, y: strongSelf.viewsPerPostValueLabel.frame.maxY), size: viewsPerPostTitleLabelLayout.size)
strongSelf.viewsPerPostDeltaLabel.frame = CGRect(origin: CGPoint(x: strongSelf.viewsPerPostValueLabel.frame.maxX + horizontalSpacing, y: strongSelf.viewsPerPostValueLabel.frame.minY + 4.0), size: viewsPerPostDeltaLabelLayout.size)
let rightColumnX = max(layout.size.width / 2.0, max(strongSelf.followersDeltaLabel.frame.maxX, strongSelf.viewsPerPostDeltaLabel.frame.maxX) + horizontalSpacing)
strongSelf.sharesPerPostValueLabel.frame = CGRect(origin: CGPoint(x: rightColumnX, y: verticalSpacing), size: sharesPerPostValueLabelLayout.size)
strongSelf.enabledNotificationsValueLabel.frame = CGRect(origin: CGPoint(x: rightColumnX, y: topInset), size: enabledNotificationsValueLabelLayout.size)
strongSelf.sharesPerPostTitleLabel.frame = CGRect(origin: CGPoint(x: rightColumnX, y: strongSelf.sharesPerPostValueLabel.frame.maxY), size: sharesPerPostTitleLabelLayout.size)
strongSelf.enabledNotificationsTitleLabel.frame = CGRect(origin: CGPoint(x: rightColumnX, y: strongSelf.enabledNotificationsValueLabel.frame.maxY), size: enabledNotificationsTitleLabelLayout.size)
strongSelf.sharesPerPostDeltaLabel.frame = CGRect(origin: CGPoint(x: strongSelf.sharesPerPostValueLabel.frame.maxX + horizontalSpacing, y: strongSelf.sharesPerPostValueLabel.frame.minY + 4.0), size: sharesPerPostDeltaLabelLayout.size)
strongSelf.viewsPerPostDeltaLabel.frame = CGRect(origin: CGPoint(x: strongSelf.viewsPerPostValueLabel.frame.maxX + horizontalSpacing, y: strongSelf.viewsPerPostValueLabel.frame.maxY - viewsPerPostDeltaLabelLayout.size.height - 2.0), size: viewsPerPostDeltaLabelLayout.size)
let sharesPerPostY = twoColumnLayout ? strongSelf.enabledNotificationsTitleLabel.frame.maxY + verticalSpacing : strongSelf.viewsPerPostTitleLabel.frame.maxY + verticalSpacing
strongSelf.sharesPerPostValueLabel.frame = CGRect(origin: CGPoint(x: secondColumnX, y: sharesPerPostY), size: sharesPerPostValueLabelLayout.size)
strongSelf.sharesPerPostTitleLabel.frame = CGRect(origin: CGPoint(x: secondColumnX, y: strongSelf.sharesPerPostValueLabel.frame.maxY), size: sharesPerPostTitleLabelLayout.size)
strongSelf.sharesPerPostDeltaLabel.frame = CGRect(origin: CGPoint(x: strongSelf.sharesPerPostValueLabel.frame.maxX + horizontalSpacing, y: strongSelf.sharesPerPostValueLabel.frame.maxY - sharesPerPostDeltaLabelLayout.size.height - 2.0), size: sharesPerPostDeltaLabelLayout.size)
}
})
}

View File

@ -31,7 +31,7 @@ public enum ChannelStatsGraph: Equatable {
case .Empty:
return true
case let .Failed(error):
return true
return error.lowercased().contains("not enough data")
default:
return false
}
@ -598,8 +598,11 @@ extension ChannelStatsMessageInteractions {
extension ChannelStats {
convenience init(apiBroadcastStats: Api.stats.BroadcastStats, peerId: PeerId) {
switch apiBroadcastStats {
case let .broadcastStats(period, followers, viewsPerPost, sharesPerPost, enabledNotifications, growthGraph, followersGraph, muteGraph, topHoursGraph, interactionsGraph, instantViewInteractionsGraph, viewsBySourceGraph, newFollowersBySourceGraph, languagesGraph, recentMessageInteractions):
self.init(period: ChannelStatsDateRange(apiStatsDateRangeDays: period), followers: ChannelStatsValue(apiStatsAbsValueAndPrev: followers), viewsPerPost: ChannelStatsValue(apiStatsAbsValueAndPrev: viewsPerPost), sharesPerPost: ChannelStatsValue(apiStatsAbsValueAndPrev: sharesPerPost), enabledNotifications: ChannelStatsPercentValue(apiPercentValue: enabledNotifications), growthGraph: ChannelStatsGraph(apiStatsGraph: growthGraph), followersGraph: ChannelStatsGraph(apiStatsGraph: followersGraph), muteGraph: ChannelStatsGraph(apiStatsGraph: muteGraph), topHoursGraph: ChannelStatsGraph(apiStatsGraph: topHoursGraph), interactionsGraph: ChannelStatsGraph(apiStatsGraph: interactionsGraph), instantPageInteractionsGraph: ChannelStatsGraph(apiStatsGraph: instantViewInteractionsGraph), viewsBySourceGraph: ChannelStatsGraph(apiStatsGraph: viewsBySourceGraph), newFollowersBySourceGraph: ChannelStatsGraph(apiStatsGraph: newFollowersBySourceGraph), languagesGraph: ChannelStatsGraph(apiStatsGraph: languagesGraph), messageInteractions: recentMessageInteractions.map { ChannelStatsMessageInteractions(apiMessageInteractionCounters: $0, peerId: peerId) })
case let .broadcastStats(period, followers, viewsPerPost, sharesPerPost, enabledNotifications, apiGrowthGraph, apiFollowersGraph, apiMuteGraph, apiTopHoursGraph, apiInteractionsGraph, apiInstantViewInteractionsGraph, apiViewsBySourceGraph, apiNewFollowersBySourceGraph, apiLanguagesGraph, recentMessageInteractions):
let growthGraph = ChannelStatsGraph(apiStatsGraph: apiGrowthGraph)
let isEmpty = growthGraph.isEmpty
self.init(period: ChannelStatsDateRange(apiStatsDateRangeDays: period), followers: ChannelStatsValue(apiStatsAbsValueAndPrev: followers), viewsPerPost: ChannelStatsValue(apiStatsAbsValueAndPrev: viewsPerPost), sharesPerPost: ChannelStatsValue(apiStatsAbsValueAndPrev: sharesPerPost), enabledNotifications: ChannelStatsPercentValue(apiPercentValue: enabledNotifications), growthGraph: growthGraph, followersGraph: ChannelStatsGraph(apiStatsGraph: apiFollowersGraph), muteGraph: ChannelStatsGraph(apiStatsGraph: apiMuteGraph), topHoursGraph: ChannelStatsGraph(apiStatsGraph: apiTopHoursGraph), interactionsGraph: isEmpty ? .Empty : ChannelStatsGraph(apiStatsGraph: apiInteractionsGraph), instantPageInteractionsGraph: isEmpty ? .Empty : ChannelStatsGraph(apiStatsGraph: apiInstantViewInteractionsGraph), viewsBySourceGraph: isEmpty ? .Empty : ChannelStatsGraph(apiStatsGraph: apiViewsBySourceGraph), newFollowersBySourceGraph: isEmpty ? .Empty : ChannelStatsGraph(apiStatsGraph: apiNewFollowersBySourceGraph), languagesGraph: isEmpty ? .Empty : ChannelStatsGraph(apiStatsGraph: apiLanguagesGraph), messageInteractions: recentMessageInteractions.map { ChannelStatsMessageInteractions(apiMessageInteractionCounters: $0, peerId: peerId) })
}
}
}

View File

@ -221,3 +221,19 @@ public func descriptionStringForMessage(contentSettings: ContentSettings, messag
}
return stringForMediaKind(messageContentKind(contentSettings: contentSettings, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, accountPeerId: accountPeerId), strings: strings)
}
public func foldLineBreaks(_ text: String) -> String {
let lines = text.split { $0.isNewline }
var result = ""
for line in lines {
if line.isEmpty {
continue
}
if result.isEmpty {
result += line
} else {
result += " " + line
}
}
return result
}

View File

@ -203,6 +203,7 @@ framework(
"//submodules/AccountUtils:AccountUtils",
"//submodules/Svg:Svg",
"//submodules/StatisticsUI:StatisticsUI",
"//submodules/ManagedAnimationNode:ManagedAnimationNode",
],
frameworks = [
"$SDKROOT/System/Library/Frameworks/Foundation.framework",

View File

@ -201,6 +201,7 @@ swift_library(
"//submodules/SemanticStatusNode:SemanticStatusNode",
"//submodules/AccountUtils:AccountUtils",
"//submodules/Svg:Svg",
"//submodules/ManagedAnimationNode:ManagedAnimationNode",
],
visibility = [
"//visibility:public",

View File

@ -120,7 +120,6 @@ public final class ChatControllerInteraction {
var stickerSettings: ChatInterfaceStickerSettings
var searchTextHighightState: (String, [MessageIndex])?
var seenOneTimeAnimatedMedia = Set<MessageId>()
var seenDicePointsValue = [MessageId: Int]()
init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, TapLongTapOrDoubleTapGestureRecognizer?) -> Void, openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, tapMessage: ((Message) -> Void)?, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendCurrentMessage: @escaping (Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool, ASDisplayNode, CGRect) -> Bool, sendGif: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, requestMessageActionUrlAuth: @escaping (String, MessageId, Int32) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?, Message?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @escaping (Message) -> Void, openTheme: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, chatControllerNode: @escaping () -> ASDisplayNode?, reactionContainerNode: @escaping () -> ReactionSelectionParentNode?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction, Message?) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, navigateToFirstDateMessage: @escaping(Int32) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, rateCall: @escaping (Message, CallId) -> Void, requestSelectMessagePollOptions: @escaping (MessageId, [Data]) -> Void, requestOpenMessagePollResults: @escaping (MessageId, MediaId) -> Void, openAppStorePage: @escaping () -> Void, displayMessageTooltip: @escaping (MessageId, String, ASDisplayNode?, CGRect?) -> Void, seekToTimecode: @escaping (Message, Double, Bool) -> Void, scheduleCurrentMessage: @escaping () -> Void, sendScheduledMessagesNow: @escaping ([MessageId]) -> Void, editScheduledMessagesTime: @escaping ([MessageId]) -> Void, performTextSelectionAction: @escaping (UInt32, String, TextSelectionAction) -> Void, updateMessageReaction: @escaping (MessageId, String?) -> Void, openMessageReactions: @escaping (MessageId) -> Void, displaySwipeToReplyHint: @escaping () -> Void, dismissReplyMarkupMessage: @escaping (Message) -> Void, openMessagePollResults: @escaping (MessageId, Data) -> Void, openPollCreation: @escaping (Bool?) -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings) {
self.openMessage = openMessage

View File

@ -2240,35 +2240,40 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
var messages: [EnqueueMessage] = []
let inputText = convertMarkdownToAttributes(effectivePresentationInterfaceState.interfaceState.composeInputState.inputText)
for text in breakChatInputText(trimChatInputText(inputText)) {
if text.length != 0 {
var attributes: [MessageAttribute] = []
let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text))
if !entities.isEmpty {
attributes.append(TextEntitiesMessageAttribute(entities: entities))
let effectiveInputText = effectivePresentationInterfaceState.interfaceState.composeInputState.inputText
if effectiveInputText.string.trimmingCharacters(in: .whitespacesAndNewlines) == "🎲" {
messages.append(.message(text: "", attributes: [], mediaReference: AnyMediaReference.standalone(media: TelegramMediaDice()), replyToMessageId: self.chatPresentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil))
} else {
let inputText = convertMarkdownToAttributes(effectiveInputText)
for text in breakChatInputText(trimChatInputText(inputText)) {
if text.length != 0 {
var attributes: [MessageAttribute] = []
let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text))
if !entities.isEmpty {
attributes.append(TextEntitiesMessageAttribute(entities: entities))
}
var webpage: TelegramMediaWebpage?
if self.chatPresentationInterfaceState.interfaceState.composeDisableUrlPreview != nil {
attributes.append(OutgoingContentInfoMessageAttribute(flags: [.disableLinkPreviews]))
} else {
webpage = self.chatPresentationInterfaceState.urlPreview?.1
}
messages.append(.message(text: text.string, attributes: attributes, mediaReference: webpage.flatMap(AnyMediaReference.standalone), replyToMessageId: self.chatPresentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil))
}
var webpage: TelegramMediaWebpage?
if self.chatPresentationInterfaceState.interfaceState.composeDisableUrlPreview != nil {
attributes.append(OutgoingContentInfoMessageAttribute(flags: [.disableLinkPreviews]))
} else {
webpage = self.chatPresentationInterfaceState.urlPreview?.1
}
messages.append(.message(text: text.string, attributes: attributes, mediaReference: webpage.flatMap(AnyMediaReference.standalone), replyToMessageId: self.chatPresentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil))
}
}
var forwardingToSameChat = false
if case let .peer(id) = self.chatPresentationInterfaceState.chatLocation, id.namespace == Namespaces.Peer.CloudUser, id != self.context.account.peerId, let forwardMessageIds = self.chatPresentationInterfaceState.interfaceState.forwardMessageIds {
for messageId in forwardMessageIds {
if messageId.peerId == id {
forwardingToSameChat = true
var forwardingToSameChat = false
if case let .peer(id) = self.chatPresentationInterfaceState.chatLocation, id.namespace == Namespaces.Peer.CloudUser, id != self.context.account.peerId, let forwardMessageIds = self.chatPresentationInterfaceState.interfaceState.forwardMessageIds {
for messageId in forwardMessageIds {
if messageId.peerId == id {
forwardingToSameChat = true
}
}
}
}
if !messages.isEmpty && forwardingToSameChat {
self.controllerInteraction.displaySwipeToReplyHint()
if !messages.isEmpty && forwardingToSameChat {
self.controllerInteraction.displaySwipeToReplyHint()
}
}
if !messages.isEmpty || self.chatPresentationInterfaceState.interfaceState.forwardMessageIds != nil {

View File

@ -28,7 +28,12 @@ func chatHistoryEntriesForView(location: ChatLocation, view: MessageHistoryView,
var groupBucket: [(Message, Bool, ChatHistoryMessageSelection, ChatMessageEntryAttributes)] = []
loop: for entry in view.entries {
var contentTypeHint: ChatMessageEntryContentType = .generic
for media in entry.message.media {
if media is TelegramMediaDice {
contentTypeHint = .animatedEmoji
}
if let action = media as? TelegramMediaAction {
switch action.action {
case .channelMigratedFromGroup, .groupMigratedToChannel, .historyCleared:
@ -44,7 +49,7 @@ func chatHistoryEntriesForView(location: ChatLocation, view: MessageHistoryView,
adminRank = adminRanks[author.id]
}
var contentTypeHint: ChatMessageEntryContentType = .generic
if presentationData.largeEmoji, entry.message.media.isEmpty {
if stickersEnabled && entry.message.text.count == 1, let _ = associatedData.animatedEmojiStickers[entry.message.text.basicEmoji.0] {
contentTypeHint = .animatedEmoji

View File

@ -18,375 +18,24 @@ import AnimatedStickerNode
import TelegramAnimatedStickerNode
import Emoji
import Markdown
import RLottieBinding
import AppBundle
import GZip
import ManagedAnimationNode
private let nameFont = Font.medium(14.0)
private let inlineBotPrefixFont = Font.regular(14.0)
private let inlineBotNameFont = nameFont
private final class ManagedAnimationState {
let item: ManagedAnimationItem
protocol GenericAnimatedStickerNode: ASDisplayNode {
private let instance: LottieInstance
let frameCount: Int
let fps: Double
var relativeTime: Double = 0.0
var frameIndex: Int?
private let renderContext: DrawingContext
init?(displaySize: CGSize, item: ManagedAnimationItem, current: ManagedAnimationState?) {
let resolvedInstance: LottieInstance
let renderContext: DrawingContext
if let current = current {
resolvedInstance = current.instance
renderContext = current.renderContext
} else {
guard let path = item.source.path else {
return nil
}
guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else {
return nil
}
guard let unpackedData = TGGUnzipData(data, 5 * 1024 * 1024) else {
return nil
}
guard let instance = LottieInstance(data: unpackedData, cacheKey: item.source.cacheKey) else {
return nil
}
resolvedInstance = instance
renderContext = DrawingContext(size: displaySize, scale: UIScreenScale, premultiplied: true, clear: true)
}
self.item = item
self.instance = resolvedInstance
self.renderContext = renderContext
self.frameCount = Int(self.instance.frameCount)
self.fps = Double(self.instance.frameRate)
}
func draw() -> UIImage? {
self.instance.renderFrame(with: Int32(self.frameIndex ?? 0), into: self.renderContext.bytes.assumingMemoryBound(to: UInt8.self), width: Int32(self.renderContext.size.width * self.renderContext.scale), height: Int32(self.renderContext.size.height * self.renderContext.scale), bytesPerRow: Int32(self.renderContext.bytesPerRow))
return self.renderContext.generateImage()
}
}
struct ManagedAnimationFrameRange: Equatable {
var startFrame: Int
var endFrame: Int
}
enum ManagedAnimationSource: Equatable {
case local(String)
case resource(MediaBox, MediaResource)
extension AnimatedStickerNode: GenericAnimatedStickerNode {
var cacheKey: String {
switch self {
case let .local(name):
return name
case let .resource(mediaBox, resource):
return resource.id.uniqueId
}
}
var path: String? {
switch self {
case let .local(name):
return getAppBundle().path(forResource: name, ofType: "tgs")
case let .resource(mediaBox, resource):
return mediaBox.completedResourcePath(resource)
}
}
static func == (lhs: ManagedAnimationSource, rhs: ManagedAnimationSource) -> Bool {
switch lhs {
case let .local(lhsPath):
if case let .local(rhsPath) = rhs, lhsPath == rhsPath {
return true
} else {
return false
}
case let .resource(lhsMediaBox, lhsResource):
if case let .resource(rhsMediaBox, rhsResource) = rhs, lhsMediaBox === rhsMediaBox, lhsResource.isEqual(to: rhsResource) {
return true
} else {
return false
}
}
}
}
struct ManagedAnimationItem: Equatable {
let source: ManagedAnimationSource
var frames: ManagedAnimationFrameRange
var duration: Double
}
class ManagedAnimationNode: ASDisplayNode {
let intrinsicSize: CGSize
private let imageNode: ASImageNode
private let displayLink: CADisplayLink
fileprivate var state: ManagedAnimationState?
fileprivate var trackStack: [ManagedAnimationItem] = []
fileprivate var didTryAdvancingState = false
init(size: CGSize) {
self.intrinsicSize = size
self.imageNode = ASImageNode()
self.imageNode.displayWithoutProcessing = true
self.imageNode.displaysAsynchronously = false
self.imageNode.frame = CGRect(origin: CGPoint(), size: self.intrinsicSize)
final class DisplayLinkTarget: NSObject {
private let f: () -> Void
init(_ f: @escaping () -> Void) {
self.f = f
}
@objc func event() {
self.f()
}
}
var displayLinkUpdate: (() -> Void)?
self.displayLink = CADisplayLink(target: DisplayLinkTarget {
displayLinkUpdate?()
}, selector: #selector(DisplayLinkTarget.event))
super.init()
self.addSubnode(self.imageNode)
self.displayLink.add(to: RunLoop.main, forMode: .common)
displayLinkUpdate = { [weak self] in
self?.updateAnimation()
}
}
func advanceState() {
guard !self.trackStack.isEmpty else {
return
}
let item = self.trackStack.removeFirst()
if let state = self.state, state.item.source == item.source {
self.state = ManagedAnimationState(displaySize: self.intrinsicSize, item: item, current: state)
} else {
self.state = ManagedAnimationState(displaySize: self.intrinsicSize, item: item, current: nil)
}
self.didTryAdvancingState = false
}
fileprivate func updateAnimation() {
if self.state == nil {
self.advanceState()
}
guard let state = self.state else {
return
}
let timestamp = CACurrentMediaTime()
let fps = state.fps
let frameRange = state.item.frames
let duration: Double = state.item.duration
var t = state.relativeTime / duration
t = max(0.0, t)
t = min(1.0, t)
let frameOffset = Int(Double(frameRange.startFrame) * (1.0 - t) + Double(frameRange.endFrame) * t)
let lowerBound: Int = 0
let upperBound = state.frameCount - 1
let frameIndex = max(lowerBound, min(upperBound, frameOffset))
if state.frameIndex != frameIndex {
state.frameIndex = frameIndex
if let image = state.draw() {
self.imageNode.image = image
}
}
var animationAdvancement: Double = 1.0 / 60.0
animationAdvancement *= Double(min(2, self.trackStack.count + 1))
state.relativeTime += animationAdvancement
if state.relativeTime >= duration && !self.didTryAdvancingState {
self.didTryAdvancingState = true
self.advanceState()
}
}
func trackTo(item: ManagedAnimationItem) {
self.trackStack.append(item)
self.didTryAdvancingState = false
self.updateAnimation()
}
}
enum ManagedDiceAnimationState: Equatable {
case rolling
case value(Int)
}
final class ManagedDiceAnimationNode: ManagedAnimationNode {
private let context: AccountContext
private let emojis: [TelegramMediaFile]
private var diceState: ManagedDiceAnimationState = .rolling
private let disposable = MetaDisposable()
init(context: AccountContext, emojis: [TelegramMediaFile]) {
self.context = context
self.emojis = emojis
super.init(size: CGSize(width: 136.0, height: 136.0))
self.trackTo(item: ManagedAnimationItem(source: .local("DiceRolling"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0), duration: 0.3))
}
deinit {
self.disposable.dispose()
}
func setState(_ diceState: ManagedDiceAnimationState) {
let previousState = self.diceState
self.diceState = diceState
switch previousState {
case .rolling:
switch diceState {
case let .value(value):
// self.disposable.set(freeMediaFileInteractiveFetched(account: item.context.account, fileReference: .standalone(media: emojiFile)).start())
//
// return chatMessageAnimationData(postbox: self.account.postbox, resource: self.resource, fitzModifier: self.fitzModifier, width: width, height: height, synchronousLoad: false)
// |> filter { data in
// return data.size != 0
// }
// |> map { data -> (String, Bool) in
// return (data.path, data.complete)
// }
self.trackTo(item: ManagedAnimationItem(source: .local("DiceRolling"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0), duration: 0.3))
case .rolling:
break
}
case let .value(currentValue):
switch diceState {
case .rolling:
self.trackTo(item: ManagedAnimationItem(source: .local("DiceRolling"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0), duration: 0.3))
case let .value(value):
break
}
}
}
}
private class ChatMessageHeartbeatHaptic {
private var hapticFeedback = HapticFeedback()
private var timer: SwiftSignalKit.Timer?
private var time: Double = 0.0
var enabled = false {
didSet {
if !self.enabled {
self.reset()
}
}
}
var active: Bool {
return self.timer != nil
}
private func reset() {
if let timer = self.timer {
self.time = 0.0
timer.invalidate()
self.timer = nil
}
}
private func beat(time: Double) {
let epsilon = 0.1
if fabs(0.0 - time) < epsilon || fabs(1.0 - time) < epsilon || fabs(2.0 - time) < epsilon {
self.hapticFeedback.impact(.medium)
} else if fabs(0.2 - time) < epsilon || fabs(1.2 - time) < epsilon || fabs(2.2 - time) < epsilon {
self.hapticFeedback.impact(.light)
}
}
func start(time: Double) {
self.hapticFeedback.prepareImpact()
if time > 2.0 {
return
}
var startTime: Double = 0.0
var delay: Double = 0.0
if time > 0.0 {
if time <= 1.0 {
startTime = 1.0
} else if time <= 2.0 {
startTime = 2.0
}
}
delay = max(0.0, startTime - time)
let block = { [weak self] in
guard let strongSelf = self, strongSelf.enabled else {
return
}
strongSelf.time = startTime
strongSelf.beat(time: startTime)
strongSelf.timer = SwiftSignalKit.Timer(timeout: 0.2, repeat: true, completion: { [weak self] in
guard let strongSelf = self, strongSelf.enabled else {
return
}
strongSelf.time += 0.2
strongSelf.beat(time: strongSelf.time)
if strongSelf.time > 2.2 {
strongSelf.reset()
strongSelf.time = 0.0
strongSelf.timer?.invalidate()
strongSelf.timer = nil
}
}, queue: Queue.mainQueue())
strongSelf.timer?.start()
}
if delay > 0.0 {
Queue.mainQueue().after(delay, block)
} else {
block()
}
}
}
class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
private let contextSourceNode: ContextExtractedContentContainingNode
let imageNode: TransformImageNode
private let animationNode: AnimatedStickerNode
private var animationNode: GenericAnimatedStickerNode?
private var didSetUpAnimationNode = false
private var isPlaying = false
@ -399,6 +48,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
var telegramFile: TelegramMediaFile?
var emojiFile: TelegramMediaFile?
var telegramDice: TelegramMediaDice?
private let disposable = MetaDisposable()
private var viaBotNode: TextNode?
@ -410,34 +60,20 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
private var highlightedState: Bool = false
private var heartbeatHaptic: ChatMessageHeartbeatHaptic?
private var heartbeatHaptic: HeartbeatHaptic?
private var currentSwipeToReplyTranslation: CGFloat = 0.0
required init() {
self.contextSourceNode = ContextExtractedContentContainingNode()
self.imageNode = TransformImageNode()
self.animationNode = AnimatedStickerNode()
self.dateAndStatusNode = ChatMessageDateAndStatusNode()
super.init(layerBacked: false)
self.animationNode.started = { [weak self] in
if let strongSelf = self {
strongSelf.imageNode.alpha = 0.0
if let item = strongSelf.item {
if let _ = strongSelf.emojiFile {
item.controllerInteraction.seenOneTimeAnimatedMedia.insert(item.message.id)
}
}
}
}
self.imageNode.displaysAsynchronously = false
self.addSubnode(self.contextSourceNode)
self.contextSourceNode.contentNode.addSubnode(self.imageNode)
self.contextSourceNode.contentNode.addSubnode(self.animationNode)
self.contextSourceNode.contentNode.addSubnode(self.dateAndStatusNode)
}
@ -460,7 +96,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
}
if strongSelf.telegramFile == nil {
if strongSelf.animationNode.frame.contains(point) {
if let animationNode = strongSelf.animationNode, animationNode.frame.contains(point) {
return .waitForDoubleTap
}
}
@ -511,9 +147,36 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
}
}
private func setupNode(item: ChatMessageItem) {
var isDice = false
if let telegramDice = self.telegramDice, let diceEmojis = item.associatedData.animatedEmojiStickers["🎲"] {
let animationNode = ManagedDiceAnimationNode(context: item.context, emojis: diceEmojis.map { $0.file })
self.animationNode = animationNode
} else {
let animationNode = AnimatedStickerNode()
animationNode.started = { [weak self] in
if let strongSelf = self {
strongSelf.imageNode.alpha = 0.0
if let item = strongSelf.item {
if let _ = strongSelf.emojiFile {
item.controllerInteraction.seenOneTimeAnimatedMedia.insert(item.message.id)
}
}
}
}
self.animationNode = animationNode
}
if let animationNode = self.animationNode {
self.contextSourceNode.contentNode.insertSubnode(animationNode, aboveSubnode: self.imageNode)
}
}
override func setupItem(_ item: ChatMessageItem) {
super.setupItem(item)
for media in item.message.media {
if let telegramFile = media as? TelegramMediaFile {
if self.telegramFile?.id != telegramFile.id {
@ -524,29 +187,26 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
self.disposable.set(freeMediaFileInteractiveFetched(account: item.context.account, fileReference: .message(message: MessageReference(item.message), media: telegramFile)).start())
}
break
} else if let telegramDice = media as? TelegramMediaDice {
self.telegramDice = telegramDice
}
}
self.setupNode(item: item)
let (emoji, fitz) = item.message.text.basicEmoji
if self.telegramFile == nil {
if let telegramDice = self.telegramDice, let diceNode = self.animationNode as? ManagedDiceAnimationNode {
if let value = telegramDice.value {
diceNode.setState(value == 0 ? .rolling : .value(value))
} else {
diceNode.setState(.rolling)
}
} else if self.telegramFile == nil {
let (emoji, fitz) = item.message.text.basicEmoji
var emojiFile: TelegramMediaFile?
if false && emoji == "🎲" {
var pointsValue: Int
if let value = item.controllerInteraction.seenDicePointsValue[item.message.id] {
pointsValue = value
} else {
pointsValue = Int(arc4random_uniform(6))
item.controllerInteraction.seenDicePointsValue[item.message.id] = pointsValue
}
if let diceEmojis = item.associatedData.animatedEmojiStickers[emoji] {
emojiFile = diceEmojis[pointsValue].file
}
} else {
emojiFile = item.associatedData.animatedEmojiStickers[emoji]?.first?.file
if emojiFile == nil {
emojiFile = item.associatedData.animatedEmojiStickers[emoji.strippedEmoji]?.first?.file
}
emojiFile = item.associatedData.animatedEmojiStickers[emoji]?.first?.file
if emojiFile == nil {
emojiFile = item.associatedData.animatedEmojiStickers[emoji.strippedEmoji]?.first?.file
}
if self.emojiFile?.id != emojiFile?.id {
@ -570,65 +230,69 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
return
}
let isPlaying = self.visibilityStatus
if self.isPlaying != isPlaying {
self.isPlaying = isPlaying
var alreadySeen = false
if isPlaying, let _ = self.emojiFile {
if item.controllerInteraction.seenOneTimeAnimatedMedia.contains(item.message.id) {
alreadySeen = true
}
}
self.animationNode.visibility = isPlaying && !alreadySeen
if self.didSetUpAnimationNode && alreadySeen {
if let emojiFile = self.emojiFile, emojiFile.resource is LocalFileReferenceMediaResource {
} else {
self.animationNode.seekTo(.start)
}
}
if self.isPlaying && !self.didSetUpAnimationNode {
self.didSetUpAnimationNode = true
if let animationNode = self.animationNode as? AnimatedStickerNode {
let isPlaying = self.visibilityStatus
if self.isPlaying != isPlaying {
self.isPlaying = isPlaying
var file: TelegramMediaFile?
var playbackMode: AnimatedStickerPlaybackMode = .loop
var isEmoji = false
var fitzModifier: EmojiFitzModifier?
if let telegramFile = self.telegramFile {
file = telegramFile
if !item.controllerInteraction.stickerSettings.loopAnimatedStickers {
playbackMode = .once
}
} else if let emojiFile = self.emojiFile {
isEmoji = true
file = emojiFile
if alreadySeen && emojiFile.resource is LocalFileReferenceMediaResource {
playbackMode = .still(.end)
} else {
playbackMode = .once
}
let (_, fitz) = item.message.text.basicEmoji
if let fitz = fitz {
fitzModifier = EmojiFitzModifier(emoji: fitz)
var alreadySeen = false
if isPlaying, let _ = self.emojiFile {
if item.controllerInteraction.seenOneTimeAnimatedMedia.contains(item.message.id) {
alreadySeen = true
}
}
if let file = file {
let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512)
let fittedSize = isEmoji ? dimensions.cgSize.aspectFilled(CGSize(width: 384.0, height: 384.0)) : dimensions.cgSize.aspectFitted(CGSize(width: 384.0, height: 384.0))
let mode: AnimatedStickerMode
if file.resource is LocalFileReferenceMediaResource {
mode = .direct
animationNode.visibility = isPlaying && !alreadySeen
if self.didSetUpAnimationNode && alreadySeen {
if let emojiFile = self.emojiFile, emojiFile.resource is LocalFileReferenceMediaResource {
} else {
mode = .cached
animationNode.seekTo(.start)
}
}
if self.isPlaying && !self.didSetUpAnimationNode {
self.didSetUpAnimationNode = true
var file: TelegramMediaFile?
var playbackMode: AnimatedStickerPlaybackMode = .loop
var isEmoji = false
var fitzModifier: EmojiFitzModifier?
if let telegramFile = self.telegramFile {
file = telegramFile
if !item.controllerInteraction.stickerSettings.loopAnimatedStickers {
playbackMode = .once
}
} else if let emojiFile = self.emojiFile {
isEmoji = true
file = emojiFile
if alreadySeen && emojiFile.resource is LocalFileReferenceMediaResource {
playbackMode = .still(.end)
} else {
playbackMode = .once
}
let (_, fitz) = item.message.text.basicEmoji
if let fitz = fitz {
fitzModifier = EmojiFitzModifier(emoji: fitz)
}
}
if let file = file {
let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512)
let fittedSize = isEmoji ? dimensions.cgSize.aspectFilled(CGSize(width: 384.0, height: 384.0)) : dimensions.cgSize.aspectFitted(CGSize(width: 384.0, height: 384.0))
let mode: AnimatedStickerMode
if file.resource is LocalFileReferenceMediaResource {
mode = .direct
} else {
mode = .cached
}
animationNode.setup(source: AnimatedStickerResourceSource(account: item.context.account, resource: file.resource, fitzModifier: fitzModifier), width: Int(fittedSize.width), height: Int(fittedSize.height), playbackMode: playbackMode, mode: mode)
}
self.animationNode.setup(source: AnimatedStickerResourceSource(account: item.context.account, resource: file.resource, fitzModifier: fitzModifier), width: Int(fittedSize.width), height: Int(fittedSize.height), playbackMode: playbackMode, mode: mode)
}
}
} else if let animationNode = self.animationNode as? ManagedDiceAnimationNode {
}
}
@ -931,8 +595,10 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
}
strongSelf.imageNode.frame = updatedContentFrame
strongSelf.animationNode.frame = updatedContentFrame.insetBy(dx: imageInset, dy: imageInset)
strongSelf.animationNode.updateLayout(size: updatedContentFrame.insetBy(dx: imageInset, dy: imageInset).size)
strongSelf.animationNode?.frame = updatedContentFrame.insetBy(dx: imageInset, dy: imageInset)
if let animationNode = strongSelf.animationNode as? AnimatedStickerNode {
animationNode.updateLayout(size: updatedContentFrame.insetBy(dx: imageInset, dy: imageInset).size)
}
imageApply()
strongSelf.contextSourceNode.contentRect = strongSelf.imageNode.frame
@ -1175,13 +841,13 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
} else if let _ = self.emojiFile {
let (emoji, fitz) = item.message.text.basicEmoji
if emoji == "🎲" {
} else {
} else if let animationNode = self.animationNode as? AnimatedStickerNode {
var startTime: Signal<Double, NoError>
if self.animationNode.playIfNeeded() {
if animationNode.playIfNeeded() {
startTime = .single(0.0)
} else {
startTime = self.animationNode.status
startTime = animationNode.status
|> map { $0.timestamp }
|> take(1)
|> deliverOnMainQueue
@ -1195,11 +861,11 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
return
}
let heartbeatHaptic: ChatMessageHeartbeatHaptic
let heartbeatHaptic: HeartbeatHaptic
if let current = strongSelf.heartbeatHaptic {
heartbeatHaptic = current
} else {
heartbeatHaptic = ChatMessageHeartbeatHaptic()
heartbeatHaptic = HeartbeatHaptic()
heartbeatHaptic.enabled = true
strongSelf.heartbeatHaptic = heartbeatHaptic
}

View File

@ -201,7 +201,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView {
deliveryFailedInset += 24.0
}
let displaySize = CGSize(width: 212.0, height: 212.0)
let displaySize = layoutConstants.instantVideo.dimensions
var automaticDownload = true
for media in item.message.media {

View File

@ -406,10 +406,12 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible {
break
}
}
} else if let _ = media as? TelegramMediaAction {
} else if media is TelegramMediaAction {
viewClassName = ChatMessageBubbleItemNode.self
} else if let _ = media as? TelegramMediaExpiredContent {
} else if media is TelegramMediaExpiredContent {
viewClassName = ChatMessageBubbleItemNode.self
} else if media is TelegramMediaDice {
viewClassName = ChatMessageAnimatedStickerItemNode.self
}
}

View File

@ -101,7 +101,7 @@ struct ChatMessageItemLayoutConstants {
let image = ChatMessageItemImageLayoutConstants(bubbleInsets: UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0), statusInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 6.0, right: 6.0), defaultCornerRadius: 16.0, mergedCornerRadius: 8.0, contentMergedCornerRadius: 5.0, maxDimensions: CGSize(width: 440.0, height: 440.0), minDimensions: CGSize(width: 170.0, height: 74.0))
let video = ChatMessageItemVideoLayoutConstants(maxHorizontalHeight: 250.0, maxVerticalHeight: 360.0)
let file = ChatMessageItemFileLayoutConstants(bubbleInsets: UIEdgeInsets(top: 15.0, left: 9.0, bottom: 15.0, right: 12.0))
let instantVideo = ChatMessageItemInstantVideoConstants(insets: UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0), dimensions: CGSize(width: 212.0, height: 212.0))
let instantVideo = ChatMessageItemInstantVideoConstants(insets: UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0), dimensions: CGSize(width: 240.0, height: 240.0))
let wallpapers = ChatMessageItemWallpaperLayoutConstants(maxTextWidth: 180.0)
return ChatMessageItemLayoutConstants(avatarDiameter: 37.0, timestampHeaderHeight: 34.0, bubble: bubble, image: image, video: video, text: text, file: file, instantVideo: instantVideo, wallpapers: wallpapers)
@ -125,6 +125,7 @@ func chatMessageItemLayoutConstants(_ constants: (ChatMessageItemLayoutConstants
let textInset: CGFloat = min(maxInset, ceil(maxInset * radiusTransition + minInset * (1.0 - radiusTransition)))
result.text.bubbleInsets.left = textInset
result.text.bubbleInsets.right = textInset
result.instantVideo.dimensions = min(params.width, params.availableHeight) > 320.0 ? constants.1.instantVideo.dimensions : constants.0.instantVideo.dimensions
return result
}

View File

@ -13,23 +13,6 @@ import StickerResources
import PhotoResources
import TelegramStringFormatting
private func foldLineBreaks(_ text: String) -> String {
var lines = text.split { $0.isNewline }
var startedBothLines = false
var result = ""
for line in lines {
if line.isEmpty {
continue
}
if result.isEmpty {
result += line
} else {
result += " " + line
}
}
return result
}
final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
private let context: AccountContext
private let tapButton: HighlightTrackingButtonNode

View File

@ -0,0 +1,89 @@
import Foundation
import Display
import SwiftSignalKit
final class HeartbeatHaptic {
private var hapticFeedback = HapticFeedback()
private var timer: SwiftSignalKit.Timer?
private var time: Double = 0.0
var enabled = false {
didSet {
if !self.enabled {
self.reset()
}
}
}
var active: Bool {
return self.timer != nil
}
private func reset() {
if let timer = self.timer {
self.time = 0.0
timer.invalidate()
self.timer = nil
}
}
private func beat(time: Double) {
let epsilon = 0.1
if fabs(0.0 - time) < epsilon || fabs(1.0 - time) < epsilon || fabs(2.0 - time) < epsilon {
self.hapticFeedback.impact(.medium)
} else if fabs(0.2 - time) < epsilon || fabs(1.2 - time) < epsilon || fabs(2.2 - time) < epsilon {
self.hapticFeedback.impact(.light)
}
}
func start(time: Double) {
self.hapticFeedback.prepareImpact()
if time > 2.0 {
return
}
var startTime: Double = 0.0
var delay: Double = 0.0
if time > 0.0 {
if time <= 1.0 {
startTime = 1.0
} else if time <= 2.0 {
startTime = 2.0
}
}
delay = max(0.0, startTime - time)
let block = { [weak self] in
guard let strongSelf = self, strongSelf.enabled else {
return
}
strongSelf.time = startTime
strongSelf.beat(time: startTime)
strongSelf.timer = SwiftSignalKit.Timer(timeout: 0.2, repeat: true, completion: { [weak self] in
guard let strongSelf = self, strongSelf.enabled else {
return
}
strongSelf.time += 0.2
strongSelf.beat(time: strongSelf.time)
if strongSelf.time > 2.2 {
strongSelf.reset()
strongSelf.time = 0.0
strongSelf.timer?.invalidate()
strongSelf.timer = nil
}
}, queue: Queue.mainQueue())
strongSelf.timer?.start()
}
if delay > 0.0 {
Queue.mainQueue().after(delay, block)
} else {
block()
}
}
}

View File

@ -0,0 +1,114 @@
import Foundation
import Display
import AsyncDisplayKit
import SyncCore
import TelegramCore
import SwiftSignalKit
import AccountContext
import StickerResources
import ManagedAnimationNode
enum ManagedDiceAnimationState: Equatable {
case rolling
case value(Int32)
}
final class ManagedDiceAnimationNode: ManagedAnimationNode, GenericAnimatedStickerNode {
private let context: AccountContext
private let emojis: [TelegramMediaFile]
private var diceState: ManagedDiceAnimationState? = nil
private let disposable = MetaDisposable()
init(context: AccountContext, emojis: [TelegramMediaFile]) {
self.context = context
self.emojis = emojis
super.init(size: CGSize(width: 136.0, height: 136.0))
self.trackTo(item: ManagedAnimationItem(source: .local("DiceRolling"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0), duration: 0.3))
}
deinit {
self.disposable.dispose()
}
func setState(_ diceState: ManagedDiceAnimationState) {
let previousState = self.diceState
self.diceState = diceState
if let previousState = previousState {
switch previousState {
case .rolling:
switch diceState {
case let .value(value):
let file = self.emojis[Int(value) - 1]
let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512)
let fittedSize = dimensions.cgSize.aspectFilled(CGSize(width: 384.0, height: 384.0))
let fetched = freeMediaFileInteractiveFetched(account: self.context.account, fileReference: .standalone(media: file))
let sticker = Signal<Void, NoError> { subscriber in
let fetchedDisposable = fetched.start()
let resourceDisposable = (chatMessageAnimationData(postbox: self.context.account.postbox, resource: file.resource, fitzModifier: nil, width: Int(fittedSize.width), height: Int(fittedSize.height), synchronousLoad: false)
|> filter { data in
return data.complete
}).start(next: { next in
subscriber.putNext(Void())
})
return ActionDisposable {
fetchedDisposable.dispose()
resourceDisposable.dispose()
}
}
self.disposable.set(sticker.start(next: { [weak self] data in
if let strongSelf = self {
strongSelf.trackTo(item: ManagedAnimationItem(source: .resource(strongSelf.context.account.postbox.mediaBox, file.resource), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0), duration: 0.3))
}
}))
case .rolling:
break
}
case let .value(currentValue):
switch diceState {
case .rolling:
self.trackTo(item: ManagedAnimationItem(source: .local("DiceRolling"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0), duration: 0.3))
case let .value(value):
break
}
}
} else {
switch diceState {
case let .value(value):
let file = self.emojis[Int(value) - 1]
let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512)
let fittedSize = dimensions.cgSize.aspectFilled(CGSize(width: 384.0, height: 384.0))
let fetched = freeMediaFileInteractiveFetched(account: self.context.account, fileReference: .standalone(media: file))
let sticker = Signal<Void, NoError> { subscriber in
let fetchedDisposable = fetched.start()
let resourceDisposable = (chatMessageAnimationData(postbox: self.context.account.postbox, resource: file.resource, fitzModifier: nil, width: Int(fittedSize.width), height: Int(fittedSize.height), synchronousLoad: false)
|> filter { data in
return data.complete
}).start(next: { next in
subscriber.putNext(Void())
})
return ActionDisposable {
fetchedDisposable.dispose()
resourceDisposable.dispose()
}
}
self.disposable.set(sticker.start(next: { [weak self] data in
if let strongSelf = self {
strongSelf.trackTo(item: ManagedAnimationItem(source: .resource(strongSelf.context.account.postbox.mediaBox, file.resource), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0), duration: 0.3))
}
}))
case .rolling:
self.trackTo(item: ManagedAnimationItem(source: .local("DiceRolling"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0), duration: 0.3))
}
}
}
}

View File

@ -34,7 +34,6 @@ final class OverlayInstantVideoDecoration: UniversalVideoDecoration {
self.contentContainerNode = ASDisplayNode()
self.contentContainerNode.backgroundColor = .white
self.contentContainerNode.cornerRadius = 60.0
self.contentContainerNode.clipsToBounds = true
self.foregroundContainerNode = ASDisplayNode()
@ -74,7 +73,7 @@ final class OverlayInstantVideoDecoration: UniversalVideoDecoration {
if let snapshot = snapshot {
self.contentContainerNode.view.addSubview(snapshot)
if let validLayoutSize = self.validLayoutSize {
if let _ = self.validLayoutSize {
snapshot.frame = CGRect(origin: CGPoint(), size: snapshot.frame.size)
}
}
@ -84,6 +83,8 @@ final class OverlayInstantVideoDecoration: UniversalVideoDecoration {
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
self.validLayoutSize = size
self.contentContainerNode.cornerRadius = size.width / 2.0
let shadowInsets = UIEdgeInsets(top: 2.0, left: 3.0, bottom: 4.0, right: 3.0)
transition.updateFrame(node: self.shadowNode, frame: CGRect(origin: CGPoint(x: -shadowInsets.left, y: -shadowInsets.top), size: CGSize(width: size.width + shadowInsets.left + shadowInsets.right, height: size.height + shadowInsets.top + shadowInsets.bottom)))

View File

@ -83,8 +83,12 @@ final class OverlayInstantVideoNode: OverlayMediaItemNode {
self.updateLayout(self.bounds.size)
}
override func preferredSizeForOverlayDisplay() -> CGSize {
return CGSize(width: 120.0, height: 120.0)
override func preferredSizeForOverlayDisplay(boundingSize: CGSize) -> CGSize {
if min(boundingSize.width, boundingSize.height) > 320.0 {
return CGSize(width: 150.0, height: 150.0)
} else {
return CGSize(width: 120.0, height: 120.0)
}
}
override func dismiss() {

View File

@ -239,7 +239,7 @@ final class OverlayMediaControllerNode: ASDisplayNode, UIGestureRecognizerDelega
location = groupLocation
}
}
let nodeData = OverlayMediaVideoNodeData(node: node, location: location, isMinimized: false, currentSize: node.preferredSizeForOverlayDisplay())
let nodeData = OverlayMediaVideoNodeData(node: node, location: location, isMinimized: false, currentSize: node.preferredSizeForOverlayDisplay(boundingSize: self.frame.size))
self.videoNodes.append(nodeData)
self.addSubnode(node)
if let validLayout = self.validLayout {

View File

@ -98,7 +98,7 @@ public final class OverlayUniversalVideoNode: OverlayMediaItemNode {
self.updateLayout(self.bounds.size)
}
override public func preferredSizeForOverlayDisplay() -> CGSize {
override public func preferredSizeForOverlayDisplay(boundingSize: CGSize) -> CGSize {
return self.content.dimensions.aspectFitted(CGSize(width: 300.0, height: 300.0))
}