mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-23 22:55:00 +00:00
Update channel statistics [skip ci]
This commit is contained in:
@@ -38,6 +38,7 @@ struct ChartDetailsViewModel {
|
|||||||
class ChartDetailsView: UIControl {
|
class ChartDetailsView: UIControl {
|
||||||
let titleLabel = UILabel()
|
let titleLabel = UILabel()
|
||||||
let arrowView = UIImageView()
|
let arrowView = UIImageView()
|
||||||
|
let activityIndicator = UIActivityIndicatorView()
|
||||||
|
|
||||||
var prefixViews: [UILabel] = []
|
var prefixViews: [UILabel] = []
|
||||||
var labelsViews: [UILabel] = []
|
var labelsViews: [UILabel] = []
|
||||||
@@ -45,11 +46,7 @@ class ChartDetailsView: UIControl {
|
|||||||
|
|
||||||
private var viewModel: ChartDetailsViewModel?
|
private var viewModel: ChartDetailsViewModel?
|
||||||
private var colorMode: ColorMode = .day
|
private var colorMode: ColorMode = .day
|
||||||
|
|
||||||
static func fromNib() -> ChartDetailsView {
|
|
||||||
return Bundle.main.loadNibNamed("ChartDetailsView", owner: nil, options: nil)?.first as! ChartDetailsView
|
|
||||||
}
|
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ class ChartStackSection: UIView, ColorModeContainer {
|
|||||||
controller.chartRangeUpdatedClosure = { [unowned self] (range, animated) in
|
controller.chartRangeUpdatedClosure = { [unowned self] (range, animated) in
|
||||||
self.rangeView.setRange(range, animated: animated)
|
self.rangeView.setRange(range, animated: animated)
|
||||||
}
|
}
|
||||||
controller.chartRangePagingClosure = { [unowned self] (isEnabled, pageSize) in
|
controller.chartRangePagingClosure = { [unowned self] (isEnabled, pageSize) in
|
||||||
self.rangeView.setRangePaging(enabled: isEnabled, minimumSize: pageSize)
|
self.rangeView.setRangePaging(enabled: isEnabled, minimumSize: pageSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ 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 { Date(timeIntervalSince1970: try Convert.doubleFrom($0) / 1000) }
|
||||||
case .chart, .bar, .area:
|
case .chart, .bar, .area, .step:
|
||||||
guard let colorString = colors[columnId],
|
guard let colorString = colors[columnId],
|
||||||
let color = UIColor(hexString: colorString) else {
|
let color = UIColor(hexString: colorString) else {
|
||||||
throw ChartsError.generalConversion("Unable to get color name from: \(colors) - \(columnId)")
|
throw ChartsError.generalConversion("Unable to get color name from: \(colors) - \(columnId)")
|
||||||
@@ -88,4 +88,5 @@ private enum ColumnType: String {
|
|||||||
case chart = "line"
|
case chart = "line"
|
||||||
case area = "area"
|
case area = "area"
|
||||||
case bar = "bar"
|
case bar = "bar"
|
||||||
|
case step = "step"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import SyncCore
|
|||||||
import MapKit
|
import MapKit
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import TelegramUIPreferences
|
import TelegramUIPreferences
|
||||||
|
import TelegramStringFormatting
|
||||||
import ItemListUI
|
import ItemListUI
|
||||||
import PresentationDataUtils
|
import PresentationDataUtils
|
||||||
import AccountContext
|
import AccountContext
|
||||||
@@ -15,8 +16,11 @@ import PresentationDataUtils
|
|||||||
import AppBundle
|
import AppBundle
|
||||||
import Charts
|
import Charts
|
||||||
|
|
||||||
private final class StatsArguments {
|
private final class StatsControllerArguments {
|
||||||
init() {
|
let loadDetailedGraph: (ChannelStatsGraph, Int64) -> Signal<ChannelStatsGraph?, NoError>
|
||||||
|
|
||||||
|
init(loadDetailedGraph: @escaping (ChannelStatsGraph, Int64) -> Signal<ChannelStatsGraph?, NoError>) {
|
||||||
|
self.loadDetailedGraph = loadDetailedGraph
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,7 +37,7 @@ private enum StatsSection: Int32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private enum StatsEntry: ItemListNodeEntry {
|
private enum StatsEntry: ItemListNodeEntry {
|
||||||
case overviewHeader(PresentationTheme, String)
|
case overviewHeader(PresentationTheme, String, String)
|
||||||
case overview(PresentationTheme, ChannelStats)
|
case overview(PresentationTheme, ChannelStats)
|
||||||
|
|
||||||
case growthTitle(PresentationTheme, String)
|
case growthTitle(PresentationTheme, String)
|
||||||
@@ -126,8 +130,8 @@ private enum StatsEntry: ItemListNodeEntry {
|
|||||||
|
|
||||||
static func ==(lhs: StatsEntry, rhs: StatsEntry) -> Bool {
|
static func ==(lhs: StatsEntry, rhs: StatsEntry) -> Bool {
|
||||||
switch lhs {
|
switch lhs {
|
||||||
case let .overviewHeader(lhsTheme, lhsText):
|
case let .overviewHeader(lhsTheme, lhsText, lhsDates):
|
||||||
if case let .overviewHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
if case let .overviewHeader(rhsTheme, rhsText, rhsDates) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsDates == rhsDates {
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
@@ -242,9 +246,11 @@ private enum StatsEntry: ItemListNodeEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
|
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
|
||||||
|
let arguments = arguments as! StatsControllerArguments
|
||||||
switch self {
|
switch self {
|
||||||
case let .overviewHeader(theme, text),
|
case let .overviewHeader(theme, text, dates):
|
||||||
let .growthTitle(theme, text),
|
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, accessoryText: ItemListSectionHeaderAccessoryText(value: dates, color: .generic), sectionId: self.section)
|
||||||
|
case let .growthTitle(theme, text),
|
||||||
let .followersTitle(theme, text),
|
let .followersTitle(theme, text),
|
||||||
let .notificationsTitle(theme, text),
|
let .notificationsTitle(theme, text),
|
||||||
let .viewsByHourTitle(theme, text),
|
let .viewsByHourTitle(theme, text),
|
||||||
@@ -258,19 +264,21 @@ private enum StatsEntry: ItemListNodeEntry {
|
|||||||
case let .growthGraph(theme, strings, dateTimeFormat, graph, type):
|
case let .growthGraph(theme, strings, dateTimeFormat, 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(theme, strings, dateTimeFormat, 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(theme, strings, dateTimeFormat, 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(theme, strings, dateTimeFormat, 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 .postInteractionsGraph(theme, strings, dateTimeFormat, graph, type):
|
case let .postInteractionsGraph(theme, strings, dateTimeFormat, graph, type):
|
||||||
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, sectionId: self.section, style: .blocks)
|
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, getDetailsData: { date, completion in
|
||||||
|
let _ = arguments.loadDetailedGraph(graph, Int64(date.timeIntervalSince1970))
|
||||||
|
}, sectionId: self.section, style: .blocks)
|
||||||
case let .viewsBySourceGraph(theme, strings, dateTimeFormat, graph, type):
|
case let .viewsBySourceGraph(theme, strings, dateTimeFormat, 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, height: 160.0, sectionId: self.section, style: .blocks)
|
||||||
case let .followersBySourceGraph(theme, strings, dateTimeFormat, graph, type):
|
case let .followersBySourceGraph(theme, strings, dateTimeFormat, 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, height: 160.0, sectionId: self.section, style: .blocks)
|
||||||
case let .languagesGraph(theme, strings, dateTimeFormat, graph, type):
|
case let .languagesGraph(theme, strings, dateTimeFormat, 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, height: 100.0, sectionId: self.section, style: .blocks)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -279,7 +287,10 @@ private func statsControllerEntries(data: ChannelStats?, presentationData: Prese
|
|||||||
var entries: [StatsEntry] = []
|
var entries: [StatsEntry] = []
|
||||||
|
|
||||||
if let data = data {
|
if let data = data {
|
||||||
entries.append(.overviewHeader(presentationData.theme, presentationData.strings.Stats_Overview))
|
let minDate = stringForDate(timestamp: data.period.minDate, strings: presentationData.strings)
|
||||||
|
let maxDate = stringForDate(timestamp: data.period.maxDate, strings: presentationData.strings)
|
||||||
|
|
||||||
|
entries.append(.overviewHeader(presentationData.theme, presentationData.strings.Stats_Overview, "\(minDate) – \(maxDate)"))
|
||||||
entries.append(.overview(presentationData.theme, data))
|
entries.append(.overview(presentationData.theme, data))
|
||||||
|
|
||||||
entries.append(.growthTitle(presentationData.theme, presentationData.strings.Stats_GrowthTitle))
|
entries.append(.growthTitle(presentationData.theme, presentationData.strings.Stats_GrowthTitle))
|
||||||
@@ -339,7 +350,9 @@ public func channelStatsController(context: AccountContext, peer: Peer, cachedPe
|
|||||||
})
|
})
|
||||||
dataPromise.set(.single(nil) |> then(dataSignal))
|
dataPromise.set(.single(nil) |> then(dataSignal))
|
||||||
|
|
||||||
let arguments = StatsArguments()
|
let arguments = StatsControllerArguments(loadDetailedGraph: { graph, x -> Signal<ChannelStatsGraph?, NoError> in
|
||||||
|
return statsContext.loadDetailedGraph(graph, x: x)
|
||||||
|
})
|
||||||
|
|
||||||
let signal = combineLatest(context.sharedContext.presentationData, dataPromise.get())
|
let signal = combineLatest(context.sharedContext.presentationData, dataPromise.get())
|
||||||
|> deliverOnMainQueue
|
|> deliverOnMainQueue
|
||||||
|
|||||||
@@ -15,14 +15,16 @@ class StatsGraphItem: ListViewItem, ItemListItem {
|
|||||||
let graph: ChannelStatsGraph
|
let graph: ChannelStatsGraph
|
||||||
let type: ChartType
|
let type: ChartType
|
||||||
let height: CGFloat
|
let height: CGFloat
|
||||||
|
let getDetailsData: ((Date, (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, sectionId: ItemListSectionId, style: ItemListStyle) {
|
init(presentationData: ItemListPresentationData, graph: ChannelStatsGraph, type: ChartType, height: CGFloat = 0.0, getDetailsData: ((Date, (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.height = height
|
||||||
|
self.getDetailsData = getDetailsData
|
||||||
self.sectionId = sectionId
|
self.sectionId = sectionId
|
||||||
self.style = style
|
self.style = style
|
||||||
}
|
}
|
||||||
@@ -184,10 +186,11 @@ class StatsGraphItemNode: 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))
|
||||||
}
|
}
|
||||||
|
|
||||||
if let updatedGraph = updatedGraph, case let .Loaded(data) = updatedGraph {
|
if let updatedGraph = updatedGraph, case let .Loaded(_, data) = updatedGraph {
|
||||||
var data = data.replacingOccurrences(of: "step", with: "bar")
|
strongSelf.chartNode.setup(data, type: item.type, getDetailsData: { [weak self] date, completion in
|
||||||
strongSelf.chartNode.setup(data, type: item.type, getDetailsData: { date, completion in
|
if let strongSelf = self, let item = strongSelf.item {
|
||||||
|
item.getDetailsData?(date, completion)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,18 @@ public struct ChannelStatsPercentValue: Equatable {
|
|||||||
public enum ChannelStatsGraph: Equatable {
|
public enum ChannelStatsGraph: Equatable {
|
||||||
case OnDemand(token: String)
|
case OnDemand(token: String)
|
||||||
case Failed(error: String)
|
case Failed(error: String)
|
||||||
case Loaded(data: String)
|
case Loaded(token: String?, data: String)
|
||||||
|
|
||||||
|
var token: String? {
|
||||||
|
switch self {
|
||||||
|
case let .OnDemand(token):
|
||||||
|
return token
|
||||||
|
case let .Loaded(token, data):
|
||||||
|
return token
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class ChannelStats: Equatable {
|
public final class ChannelStats: Equatable {
|
||||||
@@ -367,6 +378,14 @@ private final class ChannelStatsContextImpl {
|
|||||||
}), forKey: token)
|
}), forKey: token)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadDetailedGraph(_ graph: ChannelStatsGraph, x: Int64) -> Signal<ChannelStatsGraph?, NoError> {
|
||||||
|
if let token = graph.token {
|
||||||
|
return requestGraph(network: self.network, datacenterId: self.datacenterId, token: token, x: x)
|
||||||
|
} else {
|
||||||
|
return .single(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class ChannelStatsContext {
|
public final class ChannelStatsContext {
|
||||||
@@ -437,14 +456,26 @@ public final class ChannelStatsContext {
|
|||||||
impl.loadLanguagesGraph()
|
impl.loadLanguagesGraph()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func loadDetailedGraph(_ graph: ChannelStatsGraph, x: Int64) -> Signal<ChannelStatsGraph?, NoError> {
|
||||||
|
return Signal { subscriber in
|
||||||
|
let disposable = MetaDisposable()
|
||||||
|
self.impl.with { impl in
|
||||||
|
disposable.set(impl.loadDetailedGraph(graph, x: x).start(next: { value in
|
||||||
|
subscriber.putNext(value)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
return disposable
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ChannelStatsGraph {
|
extension ChannelStatsGraph {
|
||||||
init(apiStatsGraph: Api.StatsGraph) {
|
init(apiStatsGraph: Api.StatsGraph) {
|
||||||
switch apiStatsGraph {
|
switch apiStatsGraph {
|
||||||
case let .statsGraph(_, json, _):
|
case let .statsGraph(_, json, zoomToken):
|
||||||
if case let .dataJSON(string) = json {
|
if case let .dataJSON(string) = json {
|
||||||
self = .Loaded(data: string)
|
self = .Loaded(token: zoomToken, data: string)
|
||||||
} else {
|
} else {
|
||||||
self = .Failed(error: "")
|
self = .Failed(error: "")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"filename" : "arrow_left.pdf"
|
"filename" : "arrow_right.pdf"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"filename" : "arrow_right.pdf"
|
"filename" : "arrow_left.pdf"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
Reference in New Issue
Block a user