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.InteractionsTitle" = "INTERACTIONS";
"Stats.InstantViewInteractionsTitle" = "INSTANT VIEW INTERACTIONS"; "Stats.InstantViewInteractionsTitle" = "INSTANT VIEW INTERACTIONS";
"Stats.ViewsBySourceTitle" = "VIEWS BY SOURCE"; "Stats.ViewsBySourceTitle" = "VIEWS BY SOURCE";
"Stats.ViewsByHoursTitle" = "VIEWS BY HOURS";
"Stats.FollowersBySourceTitle" = "FOLLOWERS BY SOURCE"; "Stats.FollowersBySourceTitle" = "FOLLOWERS BY SOURCE";
"Stats.LanguagesTitle" = "LANGUAGES"; "Stats.LanguagesTitle" = "LANGUAGES";
"Stats.PostsTitle" = "RECENT POSTS"; "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_many" = "%@ forwards";
"Stats.MessageForwards_any" = "%@ 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_0" = "%@ views";
"InstantPage.Views_1" = "%@ view"; "InstantPage.Views_1" = "%@ view";
"InstantPage.Views_2" = "%@ views"; "InstantPage.Views_2" = "%@ views";

View File

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

View File

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

View File

@ -67,7 +67,15 @@ public extension ChartsCollection {
} }
switch type { switch type {
case .axix: 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: case .chart, .bar, .area, .step:
guard let colorString = colors[columnId], guard let colorString = colors[columnId],
let color = GColor(hexString: colorString) else { let color = GColor(hexString: colorString) else {

View File

@ -245,9 +245,15 @@ class GeneralChartComponentController: ChartThemeContainer {
var labels: [LinesChartLabel] = [] var labels: [LinesChartLabel] = []
for index in stride(from: chartsCollection.axisValues.count - 1, to: -1, by: -strideInterval).reversed() { for index in stride(from: chartsCollection.axisValues.count - 1, to: -1, by: -strideInterval).reversed() {
let date = chartsCollection.axisValues[index] let date = chartsCollection.axisValues[index]
labels.append(LinesChartLabel(value: CGFloat(date.timeIntervalSince1970), 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))) text: scaleType.dateFormatter.string(from: date)))
} }
}
prevoiusHorizontalStrideInterval = strideInterval prevoiusHorizontalStrideInterval = strideInterval
horizontalScalesRenderer.setup(labels: labels, animated: animated) horizontalScalesRenderer.setup(labels: labels, animated: animated)
} }
@ -318,6 +324,7 @@ class GeneralChartComponentController: ChartThemeContainer {
tapAction: { [weak self] in tapAction: { [weak self] in
self?.zoomInOnDateClosure?(closestDate) }, self?.zoomInOnDateClosure?(closestDate) },
hideAction: { [weak self] in hideAction: { [weak self] in
self?.setDetailsChartVisibleClosure?(false, true)
}) })
return viewModel return viewModel

View File

@ -157,9 +157,15 @@ public class BaseLinesChartController: BaseChartController {
var labels: [LinesChartLabel] = [] var labels: [LinesChartLabel] = []
for index in stride(from: initialChartsCollection.axisValues.count - 1, to: -1, by: -strideInterval).reversed() { for index in stride(from: initialChartsCollection.axisValues.count - 1, to: -1, by: -strideInterval).reversed() {
let date = initialChartsCollection.axisValues[index] let date = initialChartsCollection.axisValues[index]
labels.append(LinesChartLabel(value: CGFloat(date.timeIntervalSince1970), 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))) text: scaleType.dateFormatter.string(from: date)))
} }
}
return (strideInterval, labels) return (strideInterval, labels)
} }
return nil return nil
@ -179,14 +185,14 @@ public class BaseLinesChartController: BaseChartController {
} }
func verticalLimitsLabels(verticalRange: ClosedRange<CGFloat>) -> (ClosedRange<CGFloat>, [LinesChartLabel]) { func verticalLimitsLabels(verticalRange: ClosedRange<CGFloat>) -> (ClosedRange<CGFloat>, [LinesChartLabel]) {
let ditance = verticalRange.distance let distance = verticalRange.distance
let chartHeight = chartFrame().height 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) let approximateNumberOfChartValues = (chartHeight / BaseConstants.minimumAxisYLabelsDistance)
var numberOfOffsetsPerItem = ditance / approximateNumberOfChartValues var numberOfOffsetsPerItem = distance / approximateNumberOfChartValues
var multiplier: CGFloat = 1.0 var multiplier: CGFloat = 1.0
while numberOfOffsetsPerItem > 10 { while numberOfOffsetsPerItem > 10 {
numberOfOffsetsPerItem /= 10 numberOfOffsetsPerItem /= 10

View File

@ -237,7 +237,7 @@ public class TwoAxisLinesChartController: BaseLinesChartController {
let chartHeight = chartFrame().height let chartHeight = chartFrame().height
let approximateNumberOfChartValues = (chartHeight / BaseConstants.minimumAxisYLabelsDistance) 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 (index, controller) = arg
let verticalRange = LinesChartRenderer.LineData.verticalRange(lines: controller.chartLines, let verticalRange = LinesChartRenderer.LineData.verticalRange(lines: controller.chartLines,
calculatingRange: horizontalRange, calculatingRange: horizontalRange,

View File

@ -138,7 +138,7 @@ class PercentChartComponentController: GeneralChartComponentController {
let values: [ChartDetailsViewModel.Value] = chartsCollection.chartValues.enumerated().map { arg in let values: [ChartDetailsViewModel.Value] = chartsCollection.chartValues.enumerated().map { arg in
let (index, component) = arg 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, title: component.name,
value: BaseConstants.detailsNumberFormatter.string(from: component.values[pointIndex]), value: BaseConstants.detailsNumberFormatter.string(from: component.values[pointIndex]),
color: component.color, color: component.color,
@ -151,7 +151,7 @@ class PercentChartComponentController: GeneralChartComponentController {
dateString = BaseConstants.headerMediumRangeFormatter.string(from: closestDate) dateString = BaseConstants.headerMediumRangeFormatter.string(from: closestDate)
} }
let viewModel = ChartDetailsViewModel(title: dateString, let viewModel = ChartDetailsViewModel(title: dateString,
showArrow: self.isZoomable && !self.isZoomed, showArrow: total > 0 && self.isZoomable && !self.isZoomed,
showPrefixes: true, showPrefixes: true,
values: values, values: values,
totalValue: nil, totalValue: nil,

View File

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

View File

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

View File

@ -1,5 +1,5 @@
// //
// DailyBarsChartController.swift // StackedBarsChartController.swift
// GraphTest // GraphTest
// //
// Created by Andrei Salavei on 4/7/19. // Created by Andrei Salavei on 4/7/19.
@ -17,6 +17,12 @@ public class StepBarsChartController: BaseChartController {
let barsController: BarsComponentController let barsController: BarsComponentController
let zoomedBarsController: BarsComponentController let zoomedBarsController: BarsComponentController
override public var isZoomable: Bool {
didSet {
barsController.isZoomable = self.isZoomable
}
}
override public init(chartsCollection: ChartsCollection) { override public init(chartsCollection: ChartsCollection) {
let horizontalScalesRenderer = HorizontalScalesRenderer() let horizontalScalesRenderer = HorizontalScalesRenderer()
let verticalScalesRenderer = VerticalScalesRenderer() let verticalScalesRenderer = VerticalScalesRenderer()
@ -24,12 +30,12 @@ public class StepBarsChartController: BaseChartController {
mainBarsRenderer: BarChartRenderer(step: true), mainBarsRenderer: BarChartRenderer(step: true),
horizontalScalesRenderer: horizontalScalesRenderer, horizontalScalesRenderer: horizontalScalesRenderer,
verticalScalesRenderer: verticalScalesRenderer, verticalScalesRenderer: verticalScalesRenderer,
previewBarsChartRenderer: BarChartRenderer()) previewBarsChartRenderer: BarChartRenderer(step: true), step: true)
zoomedBarsController = BarsComponentController(isZoomed: true, zoomedBarsController = BarsComponentController(isZoomed: true,
mainBarsRenderer: BarChartRenderer(step: true), mainBarsRenderer: BarChartRenderer(),
horizontalScalesRenderer: horizontalScalesRenderer, horizontalScalesRenderer: horizontalScalesRenderer,
verticalScalesRenderer: verticalScalesRenderer, verticalScalesRenderer: verticalScalesRenderer,
previewBarsChartRenderer: BarChartRenderer()) previewBarsChartRenderer: BarChartRenderer(), step: true)
super.init(chartsCollection: chartsCollection) super.init(chartsCollection: chartsCollection)
@ -40,7 +46,7 @@ public class StepBarsChartController: BaseChartController {
self.didTapZoomIn(date: date) self.didTapZoomIn(date: date)
} }
controller.setChartTitleClosure = { [unowned self] (title, animated) in controller.setChartTitleClosure = { [unowned self] (title, animated) in
self.setChartTitleClosure?(title, animated) self.setChartTitleClosure?("", animated)
} }
controller.setDetailsViewPositionClosure = { [unowned self] (position) in controller.setDetailsViewPositionClosure = { [unowned self] (position) in
self.setDetailsViewPositionClosure?(position) 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] { public override var mainChartRenderers: [ChartViewRenderer] {
return [barsController.mainBarsRenderer, return [barsController.mainBarsRenderer,
zoomedBarsController.mainBarsRenderer, zoomedBarsController.mainBarsRenderer,
barsController.horizontalScalesRenderer, barsController.horizontalScalesRenderer,
barsController.verticalScalesRenderer, barsController.verticalScalesRenderer,
barsController.secondVerticalScalesRenderer!
// performanceRenderer // performanceRenderer
] ]
} }
@ -95,18 +107,15 @@ public class StepBarsChartController: BaseChartController {
if isZoomed { if isZoomed {
let toHorizontalRange = zoomedBarsController.initialHorizontalRange let toHorizontalRange = zoomedBarsController.initialHorizontalRange
let destinationHorizontalRange = (toHorizontalRange.lowerBound - barsController.barsWidth)...(toHorizontalRange.upperBound - barsController.barsWidth) let destinationHorizontalRange = (toHorizontalRange.lowerBound - barsController.barsWidth)...(toHorizontalRange.upperBound - barsController.barsWidth)
// let initialChartVerticalRange = lineProportionAnimationRange() let verticalVisibleRange = barsController.currentVerticalMainChartRange
let initialVerticalRange = verticalVisibleRange.lowerBound...(verticalVisibleRange.upperBound + verticalVisibleRange.distance * 10)
// let visibleVerticalRange = BarChartRenderer.BarsData.verticalRange(bars: zoomedBarsController.visibleBars, zoomedBarsController.mainBarsRenderer.setup(horizontalRange: barsController.currentHorizontalMainChartRange, animated: false)
// calculatingRange: zoomedBarsController.initialHorizontalRange) ?? BaseConstants.defaultRange
zoomedBarsController.mainBarsRenderer.setup(verticalRange: 0...117278, animated: false)
zoomedBarsController.setupMainChart(horizontalRange: barsController.currentHorizontalMainChartRange, animated: false)
zoomedBarsController.previewBarsChartRenderer.setup(horizontalRange: barsController.currentPreviewHorizontalRange, animated: false) zoomedBarsController.previewBarsChartRenderer.setup(horizontalRange: barsController.currentPreviewHorizontalRange, animated: false)
// zoomedBarsController.mainLinesRenderer.setup(verticalRange: initialChartVerticalRange, animated: false) zoomedBarsController.mainBarsRenderer.setup(verticalRange: initialVerticalRange, animated: false)
// zoomedBarsController.previewLinesChartRenderer.setup(verticalRange: initialChartVerticalRange, animated: false) zoomedBarsController.previewBarsChartRenderer.setup(verticalRange: initialVerticalRange, animated: false)
zoomedBarsController.mainBarsRenderer.setVisible(false, animated: false) zoomedBarsController.mainBarsRenderer.setVisible(true, animated: false)
zoomedBarsController.previewBarsChartRenderer.setVisible(false, animated: false) zoomedBarsController.previewBarsChartRenderer.setVisible(true, animated: false)
barsController.setupMainChart(horizontalRange: destinationHorizontalRange, animated: animated) barsController.setupMainChart(horizontalRange: destinationHorizontalRange, animated: animated)
barsController.previewBarsChartRenderer.setup(horizontalRange: zoomedBarsController.totalHorizontalRange, animated: animated) barsController.previewBarsChartRenderer.setup(horizontalRange: zoomedBarsController.totalHorizontalRange, animated: animated)
@ -116,31 +125,42 @@ public class StepBarsChartController: BaseChartController {
zoomedBarsController.willAppear(animated: animated) zoomedBarsController.willAppear(animated: animated)
barsController.willDisappear(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 { } else {
if !zoomedBarsController.chartsCollection.isBlank { if !zoomedBarsController.chartsCollection.isBlank {
barsController.hideDetailsView(animated: false) barsController.hideDetailsView(animated: false)
barsController.chartVisibility = zoomedBarsController.chartVisibility
let visibleVerticalRange = BarChartRenderer.BarsData.verticalRange(bars: barsController.visibleBars, let visibleVerticalRange = BarChartRenderer.BarsData.verticalRange(bars: barsController.visibleBars,
separate: true,
calculatingRange: barsController.initialHorizontalRange) ?? BaseConstants.defaultRange calculatingRange: barsController.initialHorizontalRange) ?? BaseConstants.defaultRange
barsController.mainBarsRenderer.setup(verticalRange: visibleVerticalRange, animated: false) barsController.mainBarsRenderer.setup(verticalRange: visibleVerticalRange, animated: false)
let toHorizontalRange = barsController.initialHorizontalRange 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.setupMainChart(horizontalRange: toHorizontalRange, animated: animated)
// zoomedBarsController.mainLinesRenderer.setup(verticalRange: destinationChartVerticalRange, animated: animated) zoomedBarsController.mainBarsRenderer.setup(verticalRange: targetVerticalRange, animated: animated, timeFunction: .easeIn)
// zoomedBarsController.previewLinesChartRenderer.setup(verticalRange: destinationChartVerticalRange, animated: animated) zoomedBarsController.previewBarsChartRenderer.setup(verticalRange: targetVerticalRange, animated: animated, timeFunction: .easeIn)
zoomedBarsController.previewBarsChartRenderer.setup(horizontalRange: barsController.totalHorizontalRange, animated: animated) zoomedBarsController.previewBarsChartRenderer.setup(horizontalRange: barsController.totalHorizontalRange, animated: animated)
zoomedBarsController.mainBarsRenderer.setVisible(false, animated: animated) DispatchQueue.main.asyncAfter(deadline: .now() + .defaultDuration) { [weak self] in
zoomedBarsController.previewBarsChartRenderer.setVisible(false, animated: animated) self?.zoomedBarsController.mainBarsRenderer.setVisible(false, animated: false)
self?.zoomedBarsController.previewBarsChartRenderer.setVisible(false, animated: false)
}
} }
barsController.willAppear(animated: animated) barsController.willAppear(animated: animated)
zoomedBarsController.willDisappear(animated: animated) zoomedBarsController.willDisappear(animated: animated)
if !zoomedBarsController.chartsCollection.isBlank {
barsController.updateChartsVisibility(visibility: zoomedBarsController.chartVisibility, animated: false)
}
} }
self.setBackButtonVisibilityClosure?(isZoomed, animated) self.setBackButtonVisibilityClosure?(isZoomed, animated)
self.refreshChartToolsClosure?(animated)
} }
public override func updateChartsVisibility(visibility: [Bool], animated: Bool) { public override func updateChartsVisibility(visibility: [Bool], animated: Bool) {
@ -166,7 +186,6 @@ public class StepBarsChartController: BaseChartController {
public override var actualChartsCollection: ChartsCollection { public override var actualChartsCollection: ChartsCollection {
let collection = isZoomed ? zoomedBarsController.chartsCollection : barsController.chartsCollection let collection = isZoomed ? zoomedBarsController.chartsCollection : barsController.chartsCollection
if collection.isBlank { if collection.isBlank {
return self.initialChartsCollection return self.initialChartsCollection
} }
@ -189,6 +208,10 @@ public class StepBarsChartController: BaseChartController {
} }
} }
public override var drawChartVisibity: Bool {
return true
}
public override var currentChartHorizontalRangeFraction: ClosedRange<CGFloat> { public override var currentChartHorizontalRangeFraction: ClosedRange<CGFloat> {
if isZoomed { if isZoomed {
return zoomedBarsController.currentChartHorizontalRangeFraction 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() { public override func didTapZoomOut() {
cancelChartInteraction() cancelChartInteraction()
switchToChart(chartsCollection: barsController.chartsCollection, isZoomed: false, animated: true) 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) super.apply(theme: theme, animated: animated)
zoomedBarsController.apply(theme: theme, animated: animated) zoomedBarsController.apply(theme: theme, animated: animated)
barsController.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 step = false
private var lineWidth: CGFloat = 2.0
init(step: Bool = false) { init(step: Bool = false, lineWidth: CGFloat = 2.0) {
self.step = step self.step = step
self.lineWidth = lineWidth
super.init() super.init()
} }
@ -158,14 +160,10 @@ class BarChartRenderer: BaseChartRenderer {
var leftX = transform(toChartCoordinateHorizontal: currentLocation - bars.barWidth, chartFrame: chartFrame) var leftX = transform(toChartCoordinateHorizontal: currentLocation - bars.barWidth, chartFrame: chartFrame)
var rightX: CGFloat = 0 var rightX: CGFloat = 0
let startPoint = CGPoint(x: leftX, var backgroundPaths: [[CGPoint]] = bars.components.map { _ in Array() }
y: transform(toChartCoordinateVertical: verticalRange.current.lowerBound, chartFrame: chartFrame))
var backgourndPaths: [[CGPoint]] = bars.components.map { _ in Array() }
let itemsCount = ((bars.locations.count - barIndex) * 2) + 4 let itemsCount = ((bars.locations.count - barIndex) * 2) + 4
for path in backgourndPaths.indices { for path in backgroundPaths.indices {
backgourndPaths[path].reserveCapacity(itemsCount) backgroundPaths[path].reserveCapacity(itemsCount)
backgourndPaths[path].append(startPoint)
} }
var maxValues: [CGFloat] = bars.components.map { _ in 0 } var maxValues: [CGFloat] = bars.components.map { _ in 0 }
@ -173,18 +171,13 @@ class BarChartRenderer: BaseChartRenderer {
currentLocation = bars.locations[barIndex] currentLocation = bars.locations[barIndex]
rightX = transform(toChartCoordinateHorizontal: currentLocation, chartFrame: chartFrame) rightX = transform(toChartCoordinateHorizontal: currentLocation, chartFrame: chartFrame)
var stackedValue: CGFloat = 0 let bottomY: CGFloat = transform(toChartCoordinateVertical: 0.0, chartFrame: chartFrame)
var bottomY: CGFloat = transform(toChartCoordinateVertical: stackedValue, chartFrame: chartFrame)
for (index, component) in bars.components.enumerated() { for (index, component) in bars.components.enumerated() {
let visibilityPercent = componentsAnimators[index].current let visibilityPercent = componentsAnimators[index].current
if visibilityPercent == 0 { continue } if visibilityPercent == 0 { continue }
var value = component.values[barIndex] let value = component.values[barIndex]
if value < 5000 {
value *= 40
}
let height = value * visibilityPercent let height = value * visibilityPercent
// stackedValue += height
let topY = transform(toChartCoordinateVertical: height, chartFrame: chartFrame) let topY = transform(toChartCoordinateVertical: height, chartFrame: chartFrame)
let componentHeight = (bottomY - topY) let componentHeight = (bottomY - topY)
maxValues[index] = max(maxValues[index], componentHeight) maxValues[index] = max(maxValues[index], componentHeight)
@ -195,9 +188,8 @@ class BarChartRenderer: BaseChartRenderer {
height: componentHeight) height: componentHeight)
selectedPaths[index].append(rect) selectedPaths[index].append(rect)
} }
backgourndPaths[index].append(CGPoint(x: leftX, y: topY)) backgroundPaths[index].append(CGPoint(x: leftX, y: topY))
backgourndPaths[index].append(CGPoint(x: rightX, y: topY)) backgroundPaths[index].append(CGPoint(x: rightX, y: topY))
// bottomY = topY
} }
if currentLocation > range.upperBound { if currentLocation > range.upperBound {
break break
@ -206,8 +198,6 @@ class BarChartRenderer: BaseChartRenderer {
barIndex += 1 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) let colorOffset = Double((1.0 - (1.0 - generalUnselectedAlpha) * selectedIndexAnimator.current) * chartsAlpha)
for (index, component) in bars.components.enumerated().reversed() { for (index, component) in bars.components.enumerated().reversed() {
@ -216,12 +206,12 @@ class BarChartRenderer: BaseChartRenderer {
} }
context.saveGState() context.saveGState()
context.setLineWidth(2.0) context.setLineWidth(self.lineWidth)
context.setStrokeColor(GColor.valueBetween(start: backgroundColorAnimator.current.color, context.setStrokeColor(GColor.valueBetween(start: backgroundColorAnimator.current.color,
end: component.color, end: component.color,
offset: colorOffset).cgColor) offset: colorOffset).cgColor)
context.beginPath() context.beginPath()
context.addLines(between: backgourndPaths[index]) context.addLines(between: backgroundPaths[index])
context.strokePath() context.strokePath()
context.restoreGState() context.restoreGState()
} }
@ -236,11 +226,11 @@ class BarChartRenderer: BaseChartRenderer {
let startPoint = CGPoint(x: leftX, let startPoint = CGPoint(x: leftX,
y: transform(toChartCoordinateVertical: verticalRange.current.lowerBound, chartFrame: chartFrame)) 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 let itemsCount = ((bars.locations.count - barIndex) * 2) + 4
for path in backgourndPaths.indices { for path in backgroundPaths.indices {
backgourndPaths[path].reserveCapacity(itemsCount) backgroundPaths[path].reserveCapacity(itemsCount)
backgourndPaths[path].append(startPoint) backgroundPaths[path].append(startPoint)
} }
var maxValues: [CGFloat] = bars.components.map { _ in 0 } var maxValues: [CGFloat] = bars.components.map { _ in 0 }
while barIndex < bars.locations.count { while barIndex < bars.locations.count {
@ -265,8 +255,8 @@ class BarChartRenderer: BaseChartRenderer {
height: componentHeight) height: componentHeight)
selectedPaths[index].append(rect) selectedPaths[index].append(rect)
} }
backgourndPaths[index].append(CGPoint(x: leftX, y: topY)) backgroundPaths[index].append(CGPoint(x: leftX, y: topY))
backgourndPaths[index].append(CGPoint(x: rightX, y: topY)) backgroundPaths[index].append(CGPoint(x: rightX, y: topY))
bottomY = topY bottomY = topY
} }
if currentLocation > range.upperBound { if currentLocation > range.upperBound {
@ -285,13 +275,13 @@ class BarChartRenderer: BaseChartRenderer {
continue continue
} }
context.saveGState() context.saveGState()
backgourndPaths[index].append(endPoint) backgroundPaths[index].append(endPoint)
context.setFillColor(GColor.valueBetween(start: backgroundColorAnimator.current.color, context.setFillColor(GColor.valueBetween(start: backgroundColorAnimator.current.color,
end: component.color, end: component.color,
offset: colorOffset).cgColor) offset: colorOffset).cgColor)
context.beginPath() context.beginPath()
context.addLines(between: backgourndPaths[index]) context.addLines(between: backgroundPaths[index])
context.closePath() context.closePath()
context.fillPath() context.fillPath()
context.restoreGState() context.restoreGState()
@ -308,7 +298,7 @@ class BarChartRenderer: BaseChartRenderer {
} }
extension BarChartRenderer.BarsData { extension BarChartRenderer.BarsData {
static func initialComponents(chartsCollection: ChartsCollection) -> static func initialComponents(chartsCollection: ChartsCollection, separate: Bool = false, initialComponents: [BarChartRenderer.BarsData.Component]? = nil) ->
(width: CGFloat, (width: CGFloat,
chartBars: BarChartRenderer.BarsData, chartBars: BarChartRenderer.BarsData,
totalHorizontalRange: ClosedRange<CGFloat>, totalHorizontalRange: ClosedRange<CGFloat>,
@ -319,15 +309,13 @@ extension BarChartRenderer.BarsData {
} else { } else {
width = 1 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) }) } values: $0.values.map { CGFloat($0) }) }
let chartBars = BarChartRenderer.BarsData(barWidth: width, let chartBars = BarChartRenderer.BarsData(barWidth: width,
locations: chartsCollection.axisValues.map { CGFloat($0.timeIntervalSince1970) }, locations: chartsCollection.axisValues.map { CGFloat($0.timeIntervalSince1970) },
components: components) components: components)
let totalVerticalRange = BarChartRenderer.BarsData.verticalRange(bars: chartBars, separate: separate) ?? 0...1
let totalVerticalRange = BarChartRenderer.BarsData.verticalRange(bars: chartBars) ?? 0...1
let totalHorizontalRange = BarChartRenderer.BarsData.visibleHorizontalRange(bars: chartBars, width: width) ?? 0...1 let totalHorizontalRange = BarChartRenderer.BarsData.visibleHorizontalRange(bars: chartBars, width: width) ?? 0...1
return (width: width, chartBars: chartBars, totalHorizontalRange: totalHorizontalRange, totalVerticalRange: totalVerticalRange) return (width: width, chartBars: chartBars, totalHorizontalRange: totalHorizontalRange, totalVerticalRange: totalVerticalRange)
} }
@ -342,7 +330,7 @@ extension BarChartRenderer.BarsData {
return (firstPoint - width)...lastPoint 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 { guard bars.components.count > 0 else {
return nil return nil
} }
@ -353,11 +341,17 @@ extension BarChartRenderer.BarsData {
var vMax: CGFloat = bars.components[0].values[index] var vMax: CGFloat = bars.components[0].values[index]
while index < bars.locations.count { while index < bars.locations.count {
if separate {
for component in bars.components {
vMax = max(vMax, component.values[index])
}
} else {
var summ: CGFloat = 0 var summ: CGFloat = 0
for component in bars.components { for component in bars.components {
summ += component.values[index] summ += component.values[index]
} }
vMax = max(vMax, summ) vMax = max(vMax, summ)
}
if bars.locations[index] > calculatingRange.upperBound { if bars.locations[index] > calculatingRange.upperBound {
break break
@ -370,11 +364,18 @@ extension BarChartRenderer.BarsData {
var vMax: CGFloat = bars.components[0].values[index] var vMax: CGFloat = bars.components[0].values[index]
while index < bars.locations.count { while index < bars.locations.count {
if separate {
for component in bars.components {
vMax = max(vMax, component.values[index])
}
} else {
var summ: CGFloat = 0 var summ: CGFloat = 0
for component in bars.components { for component in bars.components {
summ += component.values[index] summ += component.values[index]
} }
vMax = max(vMax, summ) vMax = max(vMax, summ)
}
index += 1 index += 1
} }
return 0...vMax return 0...vMax

View File

@ -62,7 +62,8 @@ class LinesChartRenderer: BaseChartRenderer {
for (index, toLine) in toLines.enumerated() { for (index, toLine) in toLines.enumerated() {
let alpha = linesAlphaAnimators[index].current * chartsAlpha let alpha = linesAlphaAnimators[index].current * chartsAlpha
if alpha == 0 { continue } if alpha == 0 { continue }
context.setStrokeColor(toLine.color.withAlphaComponent(alpha).cgColor) context.setAlpha(alpha)
context.setStrokeColor(toLine.color.cgColor)
context.setLineWidth(lineWidth) context.setLineWidth(lineWidth)
if linesShapeAnimator.isAnimating { if linesShapeAnimator.isAnimating {
@ -312,13 +313,7 @@ class LinesChartRenderer: BaseChartRenderer {
context.setLineCap(.round) context.setLineCap(.round)
context.strokeLineSegments(between: lines) context.strokeLineSegments(between: lines)
} else { } else {
let alpha = linesAlphaAnimators[index].current * chartsAlpha
if alpha == 0 { continue }
context.setStrokeColor(toLine.color.withAlphaComponent(alpha).cgColor)
context.setLineWidth(lineWidth)
if var index = toLine.points.firstIndex(where: { $0.x >= range.lowerBound }) { if var index = toLine.points.firstIndex(where: { $0.x >= range.lowerBound }) {
var lines: [CGPoint] = [] var lines: [CGPoint] = []
index = max(0, index - 1) index = max(0, index - 1)
@ -436,33 +431,8 @@ class LinesChartRenderer: BaseChartRenderer {
context.setLineCap(.round) context.setLineCap(.round)
context.strokeLineSegments(between: lines) 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": case "lightblue":
self.init(hexString: "#5ac8fa") self.init(hexString: "#5ac8fa")
return 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: default:
break break
} }

View File

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

View File

@ -13,6 +13,8 @@ public enum ChartType {
case pie case pie
case bars case bars
case step case step
case twoAxisStep
case hourlyStep
} }
public extension ChartTheme { public extension ChartTheme {
@ -24,38 +26,10 @@ public extension ChartTheme {
} }
} }
public final class ChartNode: ASDisplayNode { public func createChartController(_ data: String, type: ChartType, getDetailsData: @escaping (Date, @escaping (String?) -> Void) -> Void) -> BaseChartController? {
private var chartView: ChartStackSection { var resultController: BaseChartController?
return self.view as! ChartStackSection
}
public override init() {
super.init()
self.setViewBlock({
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(_ data: String, type: ChartType, getDetailsData: @escaping (Date, @escaping (String?) -> Void) -> Void) {
if let data = data.data(using: .utf8) { if let data = data.data(using: .utf8) {
ChartsDataManager.readChart(data: data, extraCopiesCount: 0, sync: true, success: { [weak self] collection in ChartsDataManager.readChart(data: data, extraCopiesCount: 0, sync: true, success: { collection in
let controller: BaseChartController let controller: BaseChartController
switch type { switch type {
case .lines: case .lines:
@ -71,6 +45,11 @@ public final class ChartNode: ASDisplayNode {
controller.isZoomable = false controller.isZoomable = false
case .step: case .step:
controller = StepBarsChartController(chartsCollection: collection) 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 controller.getDetailsData = { date, completion in
getDetailsData(date, { detailsData in getDetailsData(date, { detailsData in
@ -87,13 +66,41 @@ public final class ChartNode: ASDisplayNode {
} }
}) })
} }
if let strongSelf = self { resultController = controller
strongSelf.chartView.setup(controller: controller, title: "")
}
}) { error in }) { error in
} }
} }
return resultController
}
public final class ChartNode: ASDisplayNode {
private var chartView: ChartStackSection {
return self.view as! ChartStackSection
}
public override init() {
super.init()
self.setViewBlock({
return ChartStackSection()
})
}
public func setupTheme(_ theme: ChartTheme) {
self.chartView.apply(theme: ChartTheme.defaultDayTheme, animated: false)
}
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 override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return super.hitTest(point, with: event)
} }
required public init?(coder aDecoder: NSCoder) { required public init?(coder aDecoder: NSCoder) {

View File

@ -35,6 +35,8 @@ class ChartStackSection: UIView, ChartThemeContainer {
var controller: BaseChartController? var controller: BaseChartController?
var theme: ChartTheme? var theme: ChartTheme?
var displayRange: Bool = true
init() { init() {
sectionContainerView = UIView() sectionContainerView = UIView()
chartView = ChartView() 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.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.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.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.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.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.controller = controller
self.displayRange = displayRange
if let theme = self.theme { if let theme = self.theme {
controller.apply(theme: theme, animated: false) controller.apply(theme: theme, animated: false)
} }
@ -222,7 +231,10 @@ class ChartStackSection: UIView, ChartThemeContainer {
controller.initializeChart() controller.initializeChart()
updateToolViews(animated: false) updateToolViews(animated: false)
rangeView.setRange(0.8...1.0, animated: false) let range: ClosedRange<CGFloat> = displayRange ? 0.8 ... 1.0 : 0.0 ... 1.0
controller.updateChartRange(0.8...1.0, animated: false) 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?) { 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, let fractionPoint = CGPoint(x: (point.x - chartFrame.origin.x) / chartFrame.width,
y: (point.y - chartFrame.origin.y) / chartFrame.height) y: (point.y - chartFrame.origin.y) / chartFrame.height)
userDidSelectCoordinateClosure?(fractionPoint) userDidSelectCoordinateClosure?(fractionPoint)

View File

@ -17,9 +17,42 @@ private enum Constants {
static let insets = UIEdgeInsets(top: 0, left: 16, bottom: 16, right: 16) static let insets = UIEdgeInsets(top: 0, left: 16, bottom: 16, right: 16)
} }
struct ChartVisibilityItem { public struct ChartVisibilityItem {
var title: String var title: String
var color: UIColor 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 { class ChartVisibilityView: UIView {
@ -81,28 +114,6 @@ class ChartVisibilityView: UIView {
private var selectionViews: [ChartVisibilityItemView] = [] 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)? var selectionCallbackClosure: (([Bool]) -> Void)?
func setItemSelected(_ selected: Bool, at index: Int, animated: Bool) { func setItemSelected(_ selected: Bool, at index: Int, animated: Bool) {
@ -129,7 +140,7 @@ class ChartVisibilityView: UIView {
} }
private func updateFrames() { 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 selectionViews[index].frame = frame
} }
} }
@ -140,7 +151,7 @@ class ChartVisibilityView: UIView {
size.height = 0 size.height = 0
return size 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 } guard let lastFrame = frames.last else { return .zero }
let size = CGSize(width: frame.width, height: lastFrame.maxY + Constants.insets.bottom) let size = CGSize(width: frame.width, height: lastFrame.maxY + Constants.insets.bottom)
return size return size

View File

@ -293,7 +293,11 @@ typedef enum
[_wrapperView addSubview:_fadeView]; [_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){ _circleWrapperView = [[UIView alloc] initWithFrame:(CGRect){
.origin.x = (_wrapperView.bounds.size.width - circleWrapperViewLength) / 2.0f, .origin.x = (_wrapperView.bounds.size.width - circleWrapperViewLength) / 2.0f,
.origin.y = _wrapperView.bounds.size.height + circleWrapperViewLength * 0.3f, .origin.y = _wrapperView.bounds.size.height + circleWrapperViewLength * 0.3f,
@ -309,7 +313,7 @@ typedef enum
_shadowView.frame = _circleWrapperView.bounds; _shadowView.frame = _circleWrapperView.bounds;
[_circleWrapperView addSubview:_shadowView]; [_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.clipsToBounds = true;
_circleView.layer.cornerRadius = _circleView.frame.size.width / 2.0f; _circleView.layer.cornerRadius = _circleView.frame.size.width / 2.0f;
[_circleWrapperView addSubview:_circleView]; [_circleWrapperView addSubview:_circleView];
@ -325,7 +329,7 @@ typedef enum
_placeholderView.accessibilityIgnoresInvertColors = true; _placeholderView.accessibilityIgnoresInvertColors = true;
} }
CGFloat ringViewLength = 234.0f; CGFloat ringViewLength = minSide > 320.0f ? 260.0f : 234.0f;
_ringView = [[TGVideoMessageRingView alloc] initWithFrame:(CGRect){ _ringView = [[TGVideoMessageRingView alloc] initWithFrame:(CGRect){
.origin.x = (_circleWrapperView.bounds.size.width - ringViewLength) / 2.0f, .origin.x = (_circleWrapperView.bounds.size.width - ringViewLength) / 2.0f,
.origin.y = (_circleWrapperView.bounds.size.height - 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/SolidRoundedButtonNode:SolidRoundedButtonNode",
"//submodules/OverlayStatusController:OverlayStatusController", "//submodules/OverlayStatusController:OverlayStatusController",
"//submodules/rlottie:RLottieBinding", "//submodules/rlottie:RLottieBinding",
"//submodules/ManagedAnimationNode:ManagedAnimationNode",
], ],
frameworks = [ frameworks = [
"$SDKROOT/System/Library/Frameworks/Foundation.framework", "$SDKROOT/System/Library/Frameworks/Foundation.framework",

View File

@ -39,6 +39,7 @@ swift_library(
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode", "//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
"//submodules/OverlayStatusController:OverlayStatusController", "//submodules/OverlayStatusController:OverlayStatusController",
"//submodules/rlottie:RLottieBinding", "//submodules/rlottie:RLottieBinding",
"//submodules/ManagedAnimationNode:ManagedAnimationNode",
], ],
visibility = [ visibility = [
"//visibility:public", "//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/PhotoResources:PhotoResources",
"//submodules/GraphCore:GraphCore", "//submodules/GraphCore:GraphCore",
"//submodules/GraphUI:GraphUI", "//submodules/GraphUI:GraphUI",
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
], ],
frameworks = [ frameworks = [
"$SDKROOT/System/Library/Frameworks/Foundation.framework", "$SDKROOT/System/Library/Frameworks/Foundation.framework",

View File

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

View File

@ -296,36 +296,36 @@ private enum StatsEntry: ItemListNodeEntry {
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! StatsControllerArguments let arguments = arguments as! StatsControllerArguments
switch self { 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) return ItemListSectionHeaderItem(presentationData: presentationData, text: text, accessoryText: ItemListSectionHeaderAccessoryText(value: dates, color: .generic), sectionId: self.section)
case let .growthTitle(theme, text), case let .growthTitle(_, text),
let .followersTitle(theme, text), let .followersTitle(_, text),
let .notificationsTitle(theme, text), let .notificationsTitle(_, text),
let .viewsByHourTitle(theme, text), let .viewsByHourTitle(_, text),
let .viewsBySourceTitle(theme, text), let .viewsBySourceTitle(_, text),
let .followersBySourceTitle(theme, text), let .followersBySourceTitle(_, text),
let .languagesTitle(theme, text), let .languagesTitle(_, text),
let .postInteractionsTitle(theme, text), let .postInteractionsTitle(_, text),
let .postsTitle(theme, text), let .postsTitle(_, text),
let .instantPageInteractionsTitle(theme, text): let .instantPageInteractionsTitle(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) 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) 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) 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) 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) 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) return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, sectionId: self.section, style: .blocks)
case let .viewsBySourceGraph(theme, strings, dateTimeFormat, graph, type): case let .viewsBySourceGraph(_, _, _, graph, type):
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, height: 160.0, sectionId: self.section, style: .blocks) return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, sectionId: self.section, style: .blocks)
case let .followersBySourceGraph(theme, strings, dateTimeFormat, graph, type): case let .followersBySourceGraph(_, _, _, graph, type):
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, height: 160.0, sectionId: self.section, style: .blocks) return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, sectionId: self.section, style: .blocks)
case let .languagesGraph(theme, strings, dateTimeFormat, graph, type): case let .languagesGraph(_, _, _, graph, type):
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, height: 100.0, sectionId: self.section, style: .blocks) return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, sectionId: self.section, style: .blocks)
case let .postInteractionsGraph(theme, strings, dateTimeFormat, graph, type): case let .postInteractionsGraph(_, _, _, graph, type):
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, getDetailsData: { date, completion in return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, getDetailsData: { date, completion in
let _ = arguments.loadDetailedGraph(graph, Int64(date.timeIntervalSince1970) * 1000).start(next: { graph in let _ = arguments.loadDetailedGraph(graph, Int64(date.timeIntervalSince1970) * 1000).start(next: { graph in
if let graph = graph, case let .Loaded(_, data) = graph { if let graph = graph, case let .Loaded(_, data) = graph {
@ -333,18 +333,18 @@ private enum StatsEntry: ItemListNodeEntry {
} }
}) })
}, sectionId: self.section, style: .blocks) }, 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: { 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) 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)
} }
} }
} }
@ -374,6 +374,11 @@ private func statsControllerEntries(data: ChannelStats?, messages: [Message]?, i
entries.append(.notificationsGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.muteGraph, .lines)) 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 { if !data.viewsBySourceGraph.isEmpty {
entries.append(.viewsBySourceTitle(presentationData.theme, presentationData.strings.Stats_ViewsBySourceTitle)) entries.append(.viewsBySourceTitle(presentationData.theme, presentationData.strings.Stats_ViewsBySourceTitle))
entries.append(.viewsBySourceGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.viewsBySourceGraph, .bars)) entries.append(.viewsBySourceGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.viewsBySourceGraph, .bars))
@ -391,7 +396,7 @@ private func statsControllerEntries(data: ChannelStats?, messages: [Message]?, i
if !data.interactionsGraph.isEmpty { if !data.interactionsGraph.isEmpty {
entries.append(.postInteractionsTitle(presentationData.theme, presentationData.strings.Stats_InteractionsTitle)) 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 { if let messages = messages, !messages.isEmpty, let interactions = interactions, !interactions.isEmpty {
@ -439,6 +444,7 @@ public func channelStatsController(context: AccountContext, peerId: PeerId, cach
if let statsContext = statsContext, let stats = stats { if let statsContext = statsContext, let stats = stats {
if case .OnDemand = stats.interactionsGraph { if case .OnDemand = stats.interactionsGraph {
statsContext.loadInteractionsGraph() statsContext.loadInteractionsGraph()
statsContext.loadTopHoursGraph()
statsContext.loadNewFollowersBySourceGraph() statsContext.loadNewFollowersBySourceGraph()
statsContext.loadViewsBySourceGraph() statsContext.loadViewsBySourceGraph()
statsContext.loadLanguagesGraph() statsContext.loadLanguagesGraph()
@ -460,13 +466,22 @@ public func channelStatsController(context: AccountContext, peerId: PeerId, cach
} }
messagesPromise.set(.single(nil) |> then(messageView)) 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 |> 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? var emptyStateItem: ItemListControllerEmptyStateItem?
if data == nil { if data == nil {
if longLoading {
emptyStateItem = StatsEmptyStateItem(theme: presentationData.theme, strings: presentationData.strings)
} else {
emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme) emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme)
} }
}
let messages = messageView?.entries.map { $0.message }.sorted(by: { (lhsMessage, rhsMessage) -> Bool in let messages = messageView?.entries.map { $0.message }.sorted(by: { (lhsMessage, rhsMessage) -> Bool in
return lhsMessage.timestamp > rhsMessage.timestamp return lhsMessage.timestamp > rhsMessage.timestamp
@ -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 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)) 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 presentationData: ItemListPresentationData
let graph: ChannelStatsGraph let graph: ChannelStatsGraph
let type: ChartType let type: ChartType
let height: CGFloat
let getDetailsData: ((Date, @escaping (String?) -> Void) -> Void)? let getDetailsData: ((Date, @escaping (String?) -> Void) -> Void)?
let sectionId: ItemListSectionId let sectionId: ItemListSectionId
let style: ItemListStyle 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.presentationData = presentationData
self.graph = graph self.graph = graph
self.type = type self.type = type
self.height = height
self.getDetailsData = getDetailsData self.getDetailsData = getDetailsData
self.sectionId = sectionId self.sectionId = sectionId
self.style = style self.style = style
@ -72,11 +70,13 @@ class StatsGraphItemNode: ListViewItemNode {
private let topStripeNode: ASDisplayNode private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode private let maskNode: ASImageNode
private let chartContainerNode: ASDisplayNode
let chartNode: ChartNode let chartNode: ChartNode
private let activityIndicator: ActivityIndicator private let activityIndicator: ActivityIndicator
private var item: StatsGraphItem? private var item: StatsGraphItem?
private var visibilityHeight: CGFloat?
init() { init() {
self.backgroundNode = ASDisplayNode() self.backgroundNode = ASDisplayNode()
@ -84,6 +84,7 @@ class StatsGraphItemNode: ListViewItemNode {
self.backgroundNode.backgroundColor = .white self.backgroundNode.backgroundColor = .white
self.maskNode = ASImageNode() self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false
self.topStripeNode = ASDisplayNode() self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true self.topStripeNode.isLayerBacked = true
@ -91,34 +92,38 @@ class StatsGraphItemNode: ListViewItemNode {
self.bottomStripeNode = ASDisplayNode() self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true self.bottomStripeNode.isLayerBacked = true
self.chartContainerNode = ASDisplayNode()
self.chartContainerNode.clipsToBounds = true
self.chartContainerNode.isUserInteractionEnabled = true
self.chartNode = ChartNode() self.chartNode = ChartNode()
self.activityIndicator = ActivityIndicator(type: ActivityIndicatorType.custom(.black, 16.0, 2.0, false)) self.activityIndicator = ActivityIndicator(type: ActivityIndicatorType.custom(.black, 16.0, 2.0, false))
self.activityIndicator.isHidden = true self.activityIndicator.isHidden = true
super.init(layerBacked: false, dynamicBounce: false) super.init(layerBacked: false, dynamicBounce: false)
self.clipsToBounds = true self.chartContainerNode.addSubnode(self.chartNode)
self.chartContainerNode.addSubnode(self.activityIndicator)
self.addSubnode(self.chartNode)
self.addSubnode(self.activityIndicator)
} }
override func didLoad() { override func didLoad() {
super.didLoad() super.didLoad()
self.chartNode.view.interactiveTransitionGestureRecognizerTest = { point -> Bool in self.view.interactiveTransitionGestureRecognizerTest = { point -> Bool in
return point.x > 30.0 return point.x > 30.0 || (point.y > 250.0 && point.y < 295.0)
} }
} }
func asyncLayout() -> (_ item: StatsGraphItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { func asyncLayout() -> (_ item: StatsGraphItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentItem = self.item let currentItem = self.item
let currentVisibilityHeight = self.visibilityHeight
return { item, params, neighbors in return { item, params, neighbors in
let leftInset = params.leftInset let leftInset = params.leftInset
let rightInset: CGFloat = params.rightInset let rightInset: CGFloat = params.rightInset
var updatedTheme: PresentationTheme? var updatedTheme: PresentationTheme?
var updatedGraph: ChannelStatsGraph? var updatedGraph: ChannelStatsGraph?
var updatedController: BaseChartController?
if currentItem?.presentationData.theme !== item.presentationData.theme { if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme updatedTheme = item.presentationData.theme
@ -126,9 +131,16 @@ class StatsGraphItemNode: ListViewItemNode {
if currentItem?.graph != item.graph { if currentItem?.graph != item.graph {
updatedGraph = 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 insets: UIEdgeInsets
let separatorHeight = UIScreenPixel let separatorHeight = UIScreenPixel
let itemBackgroundColor: UIColor let itemBackgroundColor: UIColor
@ -138,20 +150,39 @@ class StatsGraphItemNode: ListViewItemNode {
case .plain: case .plain:
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor 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) insets = itemListNeighborsPlainInsets(neighbors)
case .blocks: case .blocks:
itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor 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) insets = itemListNeighborsGroupedInsets(neighbors)
} }
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) 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 return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in
if let strongSelf = self { if let strongSelf = self {
strongSelf.item = item strongSelf.item = item
strongSelf.visibilityHeight = visibilityHeight
if let _ = updatedTheme { if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
@ -178,14 +209,17 @@ class StatsGraphItemNode: ListViewItemNode {
if strongSelf.backgroundNode.supernode == nil { if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
} }
if strongSelf.chartContainerNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.chartContainerNode, at: 1)
}
if strongSelf.topStripeNode.supernode == nil { if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) strongSelf.insertSubnode(strongSelf.topStripeNode, at: 2)
} }
if strongSelf.bottomStripeNode.supernode == nil { if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 3)
} }
if strongSelf.maskNode.supernode == nil { if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3) strongSelf.insertSubnode(strongSelf.maskNode, at: 4)
} }
let hasCorners = itemListHasRoundedBlockLayout(params) let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false var hasTopCorners = false
@ -207,7 +241,8 @@ class StatsGraphItemNode: ListViewItemNode {
strongSelf.bottomStripeNode.isHidden = hasCorners 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.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))) 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) strongSelf.activityIndicator.type = .custom(item.presentationData.theme.list.itemSecondaryTextColor, 16.0, 2.0, false)
if let updatedGraph = updatedGraph { if let updatedTheme = updatedTheme {
if case let .Loaded(_, data) = updatedGraph { strongSelf.chartNode.setupTheme(ChartTheme(presentationTheme: updatedTheme))
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 let updatedGraph = updatedGraph {
if case let .Loaded(_, data) = updatedGraph, let updatedController = updatedController {
strongSelf.chartNode.setup(controller: updatedController)
strongSelf.activityIndicator.isHidden = true strongSelf.activityIndicator.isHidden = true
strongSelf.chartNode.isHidden = false strongSelf.chartNode.isHidden = false
} else if case .OnDemand = updatedGraph { } else if case .OnDemand = updatedGraph {
@ -234,10 +269,6 @@ class StatsGraphItemNode: ListViewItemNode {
strongSelf.chartNode.isHidden = true 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 itemBackgroundColor: UIColor
let itemSeparatorColor: UIColor let itemSeparatorColor: UIColor
var leftInset = 16.0 + params.leftInset let leftInset = 16.0 + params.leftInset
var rightInset = 16.0 + params.rightInset let rightInset = 16.0 + params.rightInset
var totalLeftInset = leftInset var totalLeftInset = leftInset
var additionalRightInset: CGFloat = 93.0 let additionalRightInset: CGFloat = 93.0
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize) 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 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? var contentImageMedia: Media?
for media in item.message.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 titleFont = Font.regular(item.presentationData.fontSize.itemListBaseHeaderFontSize)
let deltaFont = 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 var enabledNotifications: Double = 0.0
if item.stats.enabledNotifications.total > 0 { if item.stats.enabledNotifications.total > 0 {
enabledNotifications = item.stats.enabledNotifications.value / item.stats.enabledNotifications.total 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 followersDeltaValue = item.stats.followers.current - item.stats.followers.previous
let followersDeltaCompact = compactNumericCountString(abs(Int(followersDeltaValue))) let followersDeltaCompact = compactNumericCountString(abs(Int(followersDeltaValue)))
@ -185,7 +185,9 @@ class StatsOverviewItemNode: ListViewItemNode {
followersDeltaPercentage = abs(followersDeltaValue / item.stats.followers.previous) 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 viewsPerPostDeltaValue = item.stats.viewsPerPost.current - item.stats.viewsPerPost.previous
let viewsPerPostDeltaCompact = compactNumericCountString(abs(Int(viewsPerPostDeltaValue))) let viewsPerPostDeltaCompact = compactNumericCountString(abs(Int(viewsPerPostDeltaValue)))
@ -195,7 +197,9 @@ class StatsOverviewItemNode: ListViewItemNode {
viewsPerPostDeltaPercentage = abs(viewsPerPostDeltaValue / item.stats.viewsPerPost.previous) 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 sharesPerPostDeltaValue = item.stats.sharesPerPost.current - item.stats.sharesPerPost.previous
let sharesPerPostDeltaCompact = compactNumericCountString(abs(Int(sharesPerPostDeltaValue))) let sharesPerPostDeltaCompact = compactNumericCountString(abs(Int(sharesPerPostDeltaValue)))
@ -205,7 +209,9 @@ class StatsOverviewItemNode: ListViewItemNode {
sharesPerPostDeltaPercentage = abs(sharesPerPostDeltaValue / item.stats.sharesPerPost.previous) 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 contentSize: CGSize
let insets: UIEdgeInsets let insets: UIEdgeInsets
@ -213,11 +219,35 @@ class StatsOverviewItemNode: ListViewItemNode {
let itemBackgroundColor: UIColor let itemBackgroundColor: UIColor
let itemSeparatorColor: UIColor let itemSeparatorColor: UIColor
let height: CGFloat let horizontalSpacing: CGFloat = 4.0
if item.stats.viewsPerPost.current.isZero && item.stats.sharesPerPost.current.isZero { let verticalSpacing: CGFloat = 18.0
height = 64.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 { } 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 { 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)) 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.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.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.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) 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 rightColumnX = max(layout.size.width / 2.0, max(strongSelf.followersDeltaLabel.frame.maxX, strongSelf.viewsPerPostDeltaLabel.frame.maxX) + horizontalSpacing) 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.sharesPerPostValueLabel.frame = CGRect(origin: CGPoint(x: rightColumnX, y: verticalSpacing), size: sharesPerPostValueLabelLayout.size) strongSelf.sharesPerPostTitleLabel.frame = CGRect(origin: CGPoint(x: secondColumnX, y: strongSelf.sharesPerPostValueLabel.frame.maxY), size: sharesPerPostTitleLabelLayout.size)
strongSelf.enabledNotificationsValueLabel.frame = CGRect(origin: CGPoint(x: rightColumnX, y: topInset), size: enabledNotificationsValueLabelLayout.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)
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)
} }
}) })
} }

View File

@ -31,7 +31,7 @@ public enum ChannelStatsGraph: Equatable {
case .Empty: case .Empty:
return true return true
case let .Failed(error): case let .Failed(error):
return true return error.lowercased().contains("not enough data")
default: default:
return false return false
} }
@ -598,8 +598,11 @@ extension ChannelStatsMessageInteractions {
extension ChannelStats { extension ChannelStats {
convenience init(apiBroadcastStats: Api.stats.BroadcastStats, peerId: PeerId) { convenience init(apiBroadcastStats: Api.stats.BroadcastStats, peerId: PeerId) {
switch apiBroadcastStats { switch apiBroadcastStats {
case let .broadcastStats(period, followers, viewsPerPost, sharesPerPost, enabledNotifications, growthGraph, followersGraph, muteGraph, topHoursGraph, interactionsGraph, instantViewInteractionsGraph, viewsBySourceGraph, newFollowersBySourceGraph, languagesGraph, recentMessageInteractions): case let .broadcastStats(period, followers, viewsPerPost, sharesPerPost, enabledNotifications, apiGrowthGraph, apiFollowersGraph, apiMuteGraph, apiTopHoursGraph, apiInteractionsGraph, apiInstantViewInteractionsGraph, apiViewsBySourceGraph, apiNewFollowersBySourceGraph, apiLanguagesGraph, 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) }) 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) 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/AccountUtils:AccountUtils",
"//submodules/Svg:Svg", "//submodules/Svg:Svg",
"//submodules/StatisticsUI:StatisticsUI", "//submodules/StatisticsUI:StatisticsUI",
"//submodules/ManagedAnimationNode:ManagedAnimationNode",
], ],
frameworks = [ frameworks = [
"$SDKROOT/System/Library/Frameworks/Foundation.framework", "$SDKROOT/System/Library/Frameworks/Foundation.framework",

View File

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

View File

@ -120,7 +120,6 @@ public final class ChatControllerInteraction {
var stickerSettings: ChatInterfaceStickerSettings var stickerSettings: ChatInterfaceStickerSettings
var searchTextHighightState: (String, [MessageIndex])? var searchTextHighightState: (String, [MessageIndex])?
var seenOneTimeAnimatedMedia = Set<MessageId>() 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) { 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 self.openMessage = openMessage

View File

@ -2240,7 +2240,11 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
var messages: [EnqueueMessage] = [] var messages: [EnqueueMessage] = []
let inputText = convertMarkdownToAttributes(effectivePresentationInterfaceState.interfaceState.composeInputState.inputText) 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)) { for text in breakChatInputText(trimChatInputText(inputText)) {
if text.length != 0 { if text.length != 0 {
@ -2270,6 +2274,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
if !messages.isEmpty && forwardingToSameChat { if !messages.isEmpty && forwardingToSameChat {
self.controllerInteraction.displaySwipeToReplyHint() self.controllerInteraction.displaySwipeToReplyHint()
} }
}
if !messages.isEmpty || self.chatPresentationInterfaceState.interfaceState.forwardMessageIds != nil { if !messages.isEmpty || self.chatPresentationInterfaceState.interfaceState.forwardMessageIds != nil {
self.setupSendActionOnViewUpdate({ [weak self] in self.setupSendActionOnViewUpdate({ [weak self] in

View File

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

View File

@ -18,375 +18,24 @@ import AnimatedStickerNode
import TelegramAnimatedStickerNode import TelegramAnimatedStickerNode
import Emoji import Emoji
import Markdown import Markdown
import ManagedAnimationNode
import RLottieBinding
import AppBundle
import GZip
private let nameFont = Font.medium(14.0) private let nameFont = Font.medium(14.0)
private let inlineBotPrefixFont = Font.regular(14.0) private let inlineBotPrefixFont = Font.regular(14.0)
private let inlineBotNameFont = nameFont private let inlineBotNameFont = nameFont
private final class ManagedAnimationState { protocol GenericAnimatedStickerNode: ASDisplayNode {
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 = 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 extension AnimatedStickerNode: GenericAnimatedStickerNode {
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)
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 { class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
private let contextSourceNode: ContextExtractedContentContainingNode private let contextSourceNode: ContextExtractedContentContainingNode
let imageNode: TransformImageNode let imageNode: TransformImageNode
private let animationNode: AnimatedStickerNode private var animationNode: GenericAnimatedStickerNode?
private var didSetUpAnimationNode = false private var didSetUpAnimationNode = false
private var isPlaying = false private var isPlaying = false
@ -399,6 +48,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
var telegramFile: TelegramMediaFile? var telegramFile: TelegramMediaFile?
var emojiFile: TelegramMediaFile? var emojiFile: TelegramMediaFile?
var telegramDice: TelegramMediaDice?
private let disposable = MetaDisposable() private let disposable = MetaDisposable()
private var viaBotNode: TextNode? private var viaBotNode: TextNode?
@ -410,34 +60,20 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
private var highlightedState: Bool = false private var highlightedState: Bool = false
private var heartbeatHaptic: ChatMessageHeartbeatHaptic? private var heartbeatHaptic: HeartbeatHaptic?
private var currentSwipeToReplyTranslation: CGFloat = 0.0 private var currentSwipeToReplyTranslation: CGFloat = 0.0
required init() { required init() {
self.contextSourceNode = ContextExtractedContentContainingNode() self.contextSourceNode = ContextExtractedContentContainingNode()
self.imageNode = TransformImageNode() self.imageNode = TransformImageNode()
self.animationNode = AnimatedStickerNode()
self.dateAndStatusNode = ChatMessageDateAndStatusNode() self.dateAndStatusNode = ChatMessageDateAndStatusNode()
super.init(layerBacked: false) 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.imageNode.displaysAsynchronously = false
self.addSubnode(self.contextSourceNode) self.addSubnode(self.contextSourceNode)
self.contextSourceNode.contentNode.addSubnode(self.imageNode) self.contextSourceNode.contentNode.addSubnode(self.imageNode)
self.contextSourceNode.contentNode.addSubnode(self.animationNode)
self.contextSourceNode.contentNode.addSubnode(self.dateAndStatusNode) self.contextSourceNode.contentNode.addSubnode(self.dateAndStatusNode)
} }
@ -460,7 +96,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
} }
if strongSelf.telegramFile == nil { if strongSelf.telegramFile == nil {
if strongSelf.animationNode.frame.contains(point) { if let animationNode = strongSelf.animationNode, animationNode.frame.contains(point) {
return .waitForDoubleTap return .waitForDoubleTap
} }
} }
@ -511,6 +147,33 @@ 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) { override func setupItem(_ item: ChatMessageItem) {
super.setupItem(item) super.setupItem(item)
@ -524,30 +187,27 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
self.disposable.set(freeMediaFileInteractiveFetched(account: item.context.account, fileReference: .message(message: MessageReference(item.message), media: telegramFile)).start()) self.disposable.set(freeMediaFileInteractiveFetched(account: item.context.account, fileReference: .message(message: MessageReference(item.message), media: telegramFile)).start())
} }
break break
} else if let telegramDice = media as? TelegramMediaDice {
self.telegramDice = telegramDice
} }
} }
self.setupNode(item: item)
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 let (emoji, fitz) = item.message.text.basicEmoji
if self.telegramFile == nil {
var emojiFile: TelegramMediaFile? 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 emojiFile = item.associatedData.animatedEmojiStickers[emoji]?.first?.file
if emojiFile == nil { if emojiFile == nil {
emojiFile = item.associatedData.animatedEmojiStickers[emoji.strippedEmoji]?.first?.file emojiFile = item.associatedData.animatedEmojiStickers[emoji.strippedEmoji]?.first?.file
} }
}
if self.emojiFile?.id != emojiFile?.id { if self.emojiFile?.id != emojiFile?.id {
self.emojiFile = emojiFile self.emojiFile = emojiFile
@ -570,6 +230,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
return return
} }
if let animationNode = self.animationNode as? AnimatedStickerNode {
let isPlaying = self.visibilityStatus let isPlaying = self.visibilityStatus
if self.isPlaying != isPlaying { if self.isPlaying != isPlaying {
self.isPlaying = isPlaying self.isPlaying = isPlaying
@ -581,12 +242,12 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
} }
} }
self.animationNode.visibility = isPlaying && !alreadySeen animationNode.visibility = isPlaying && !alreadySeen
if self.didSetUpAnimationNode && alreadySeen { if self.didSetUpAnimationNode && alreadySeen {
if let emojiFile = self.emojiFile, emojiFile.resource is LocalFileReferenceMediaResource { if let emojiFile = self.emojiFile, emojiFile.resource is LocalFileReferenceMediaResource {
} else { } else {
self.animationNode.seekTo(.start) animationNode.seekTo(.start)
} }
} }
@ -626,10 +287,13 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
} else { } else {
mode = .cached mode = .cached
} }
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) 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 {
}
} }
override func updateStickerSettings() { override func updateStickerSettings() {
@ -931,8 +595,10 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
} }
strongSelf.imageNode.frame = updatedContentFrame strongSelf.imageNode.frame = updatedContentFrame
strongSelf.animationNode.frame = updatedContentFrame.insetBy(dx: imageInset, dy: imageInset) strongSelf.animationNode?.frame = updatedContentFrame.insetBy(dx: imageInset, dy: imageInset)
strongSelf.animationNode.updateLayout(size: updatedContentFrame.insetBy(dx: imageInset, dy: imageInset).size) if let animationNode = strongSelf.animationNode as? AnimatedStickerNode {
animationNode.updateLayout(size: updatedContentFrame.insetBy(dx: imageInset, dy: imageInset).size)
}
imageApply() imageApply()
strongSelf.contextSourceNode.contentRect = strongSelf.imageNode.frame strongSelf.contextSourceNode.contentRect = strongSelf.imageNode.frame
@ -1176,12 +842,12 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
let (emoji, fitz) = item.message.text.basicEmoji let (emoji, fitz) = item.message.text.basicEmoji
if emoji == "🎲" { if emoji == "🎲" {
} else { } else if let animationNode = self.animationNode as? AnimatedStickerNode {
var startTime: Signal<Double, NoError> var startTime: Signal<Double, NoError>
if self.animationNode.playIfNeeded() { if animationNode.playIfNeeded() {
startTime = .single(0.0) startTime = .single(0.0)
} else { } else {
startTime = self.animationNode.status startTime = animationNode.status
|> map { $0.timestamp } |> map { $0.timestamp }
|> take(1) |> take(1)
|> deliverOnMainQueue |> deliverOnMainQueue
@ -1195,11 +861,11 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
return return
} }
let heartbeatHaptic: ChatMessageHeartbeatHaptic let heartbeatHaptic: HeartbeatHaptic
if let current = strongSelf.heartbeatHaptic { if let current = strongSelf.heartbeatHaptic {
heartbeatHaptic = current heartbeatHaptic = current
} else { } else {
heartbeatHaptic = ChatMessageHeartbeatHaptic() heartbeatHaptic = HeartbeatHaptic()
heartbeatHaptic.enabled = true heartbeatHaptic.enabled = true
strongSelf.heartbeatHaptic = heartbeatHaptic strongSelf.heartbeatHaptic = heartbeatHaptic
} }

View File

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

View File

@ -406,10 +406,12 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible {
break break
} }
} }
} else if let _ = media as? TelegramMediaAction { } else if media is TelegramMediaAction {
viewClassName = ChatMessageBubbleItemNode.self viewClassName = ChatMessageBubbleItemNode.self
} else if let _ = media as? TelegramMediaExpiredContent { } else if media is TelegramMediaExpiredContent {
viewClassName = ChatMessageBubbleItemNode.self 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 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 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 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) 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) 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))) let textInset: CGFloat = min(maxInset, ceil(maxInset * radiusTransition + minInset * (1.0 - radiusTransition)))
result.text.bubbleInsets.left = textInset result.text.bubbleInsets.left = textInset
result.text.bubbleInsets.right = 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 return result
} }

View File

@ -13,23 +13,6 @@ import StickerResources
import PhotoResources import PhotoResources
import TelegramStringFormatting 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 { final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
private let context: AccountContext private let context: AccountContext
private let tapButton: HighlightTrackingButtonNode 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 = ASDisplayNode()
self.contentContainerNode.backgroundColor = .white self.contentContainerNode.backgroundColor = .white
self.contentContainerNode.cornerRadius = 60.0
self.contentContainerNode.clipsToBounds = true self.contentContainerNode.clipsToBounds = true
self.foregroundContainerNode = ASDisplayNode() self.foregroundContainerNode = ASDisplayNode()
@ -74,7 +73,7 @@ final class OverlayInstantVideoDecoration: UniversalVideoDecoration {
if let snapshot = snapshot { if let snapshot = snapshot {
self.contentContainerNode.view.addSubview(snapshot) self.contentContainerNode.view.addSubview(snapshot)
if let validLayoutSize = self.validLayoutSize { if let _ = self.validLayoutSize {
snapshot.frame = CGRect(origin: CGPoint(), size: snapshot.frame.size) snapshot.frame = CGRect(origin: CGPoint(), size: snapshot.frame.size)
} }
} }
@ -84,6 +83,8 @@ final class OverlayInstantVideoDecoration: UniversalVideoDecoration {
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
self.validLayoutSize = size 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) 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))) 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,9 +83,13 @@ final class OverlayInstantVideoNode: OverlayMediaItemNode {
self.updateLayout(self.bounds.size) self.updateLayout(self.bounds.size)
} }
override func preferredSizeForOverlayDisplay() -> CGSize { 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) return CGSize(width: 120.0, height: 120.0)
} }
}
override func dismiss() { override func dismiss() {
self.close() self.close()

View File

@ -239,7 +239,7 @@ final class OverlayMediaControllerNode: ASDisplayNode, UIGestureRecognizerDelega
location = groupLocation 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.videoNodes.append(nodeData)
self.addSubnode(node) self.addSubnode(node)
if let validLayout = self.validLayout { if let validLayout = self.validLayout {

View File

@ -98,7 +98,7 @@ public final class OverlayUniversalVideoNode: OverlayMediaItemNode {
self.updateLayout(self.bounds.size) 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)) return self.content.dimensions.aspectFitted(CGSize(width: 300.0, height: 300.0))
} }