diff --git a/submodules/GraphUI/Sources/ChartVisibilityItemView.swift b/submodules/GraphUI/Sources/ChartVisibilityItemView.swift index 3b6025e0c0..2ededc5265 100644 --- a/submodules/GraphUI/Sources/ChartVisibilityItemView.swift +++ b/submodules/GraphUI/Sources/ChartVisibilityItemView.swift @@ -32,7 +32,7 @@ class ChartVisibilityItemView: UIView { func setupView() { checkButton.frame = bounds checkButton.titleLabel?.font = ChartVisibilityItemView.textFont - checkButton.layer.cornerRadius = 6 + checkButton.layer.cornerRadius = 15 checkButton.layer.masksToBounds = true checkButton.addTarget(self, action: #selector(didTapButton), for: .touchUpInside) let pressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(didRecognizedLongPress(recognizer:))) diff --git a/submodules/StatisticsUI/Sources/ChannelStatsController.swift b/submodules/StatisticsUI/Sources/ChannelStatsController.swift index 82e7b01c0e..4f8aab4445 100644 --- a/submodules/StatisticsUI/Sources/ChannelStatsController.swift +++ b/submodules/StatisticsUI/Sources/ChannelStatsController.swift @@ -62,8 +62,11 @@ private enum StatsSection: Int32 { case followersBySource case languages case postInteractions - case recentPosts case instantPageInteractions + case reactionsByEmotion + case storyInteractions + case storyReactionsByEmotion + case recentPosts case boostLevel case boostOverview case boostPrepaid @@ -99,13 +102,22 @@ private enum StatsEntry: ItemListNodeEntry { case postInteractionsTitle(PresentationTheme, String) case postInteractionsGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType) + + case reactionsByEmotionTitle(PresentationTheme, String) + case reactionsByEmotionGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType) - case postsTitle(PresentationTheme, String) - case post(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, Message, ChannelStatsMessageInteractions) + case storyInteractionsTitle(PresentationTheme, String) + case storyInteractionsGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType) + + case storyReactionsByEmotionTitle(PresentationTheme, String) + case storyReactionsByEmotionGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType) case instantPageInteractionsTitle(PresentationTheme, String) case instantPageInteractionsGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType) + case postsTitle(PresentationTheme, String) + case post(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, Message, ChannelStatsMessageInteractions) + case boostLevel(PresentationTheme, Int32, Int32, CGFloat) case boostOverviewTitle(PresentationTheme, String) @@ -149,10 +161,16 @@ private enum StatsEntry: ItemListNodeEntry { return StatsSection.languages.rawValue case .postInteractionsTitle, .postInteractionsGraph: return StatsSection.postInteractions.rawValue - case .postsTitle, .post: - return StatsSection.recentPosts.rawValue case .instantPageInteractionsTitle, .instantPageInteractionsGraph: return StatsSection.instantPageInteractions.rawValue + case .reactionsByEmotionTitle, .reactionsByEmotionGraph: + return StatsSection.reactionsByEmotion.rawValue + case .storyInteractionsTitle, .storyInteractionsGraph: + return StatsSection.storyInteractions.rawValue + case .storyReactionsByEmotionTitle, .storyReactionsByEmotionGraph: + return StatsSection.storyReactionsByEmotion.rawValue + case .postsTitle, .post: + return StatsSection.recentPosts.rawValue case .boostLevel: return StatsSection.boostLevel.rawValue case .boostOverviewTitle, .boostOverview: @@ -207,13 +225,25 @@ private enum StatsEntry: ItemListNodeEntry { case .postInteractionsGraph: return 17 case .instantPageInteractionsTitle: - return 18 - case .instantPageInteractionsGraph: - return 19 - case .postsTitle: + return 18 + case .instantPageInteractionsGraph: + return 19 + case .reactionsByEmotionTitle: return 20 + case .reactionsByEmotionGraph: + return 21 + case .storyInteractionsTitle: + return 22 + case .storyInteractionsGraph: + return 23 + case .storyReactionsByEmotionTitle: + return 24 + case .storyReactionsByEmotionGraph: + return 25 + case .postsTitle: + return 26 case let .post(index, _, _, _, _, _): - return 21 + index + return 27 + index case .boostLevel: return 2000 case .boostOverviewTitle: @@ -367,12 +397,6 @@ private enum StatsEntry: ItemListNodeEntry { } else { return false } - case let .post(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsMessage, lhsInteractions): - if case let .post(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsMessage, rhsInteractions) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsMessage.id == rhsMessage.id, lhsInteractions == rhsInteractions { - return true - } else { - return false - } case let .instantPageInteractionsTitle(lhsTheme, lhsText): if case let .instantPageInteractionsTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true @@ -385,6 +409,48 @@ private enum StatsEntry: ItemListNodeEntry { } else { return false } + case let .reactionsByEmotionTitle(lhsTheme, lhsText): + if case let .reactionsByEmotionTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .reactionsByEmotionGraph(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsGraph, lhsType): + if case let .reactionsByEmotionGraph(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsGraph, rhsType) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsGraph == rhsGraph, lhsType == rhsType { + return true + } else { + return false + } + case let .storyInteractionsTitle(lhsTheme, lhsText): + if case let .storyInteractionsTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .storyInteractionsGraph(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsGraph, lhsType): + if case let .storyInteractionsGraph(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsGraph, rhsType) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsGraph == rhsGraph, lhsType == rhsType { + return true + } else { + return false + } + case let .storyReactionsByEmotionTitle(lhsTheme, lhsText): + if case let .storyReactionsByEmotionTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .storyReactionsByEmotionGraph(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsGraph, lhsType): + if case let .storyReactionsByEmotionGraph(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsGraph, rhsType) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsGraph == rhsGraph, lhsType == rhsType { + return true + } else { + return false + } + case let .post(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsMessage, lhsInteractions): + if case let .post(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsMessage, rhsInteractions) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsMessage.id == rhsMessage.id, lhsInteractions == rhsInteractions { + return true + } else { + return false + } case let .boostLevel(lhsTheme, lhsBoosts, lhsLevel, lhsPosition): if case let .boostLevel(rhsTheme, rhsBoosts, rhsLevel, rhsPosition) = rhs, lhsTheme === rhsTheme, lhsBoosts == rhsBoosts, lhsLevel == rhsLevel, lhsPosition == rhsPosition { return true @@ -507,8 +573,11 @@ private enum StatsEntry: ItemListNodeEntry { let .followersBySourceTitle(_, text), let .languagesTitle(_, text), let .postInteractionsTitle(_, text), - let .postsTitle(_, text), let .instantPageInteractionsTitle(_, text), + let .reactionsByEmotionTitle(_, text), + let .storyInteractionsTitle(_, text), + let .storyReactionsByEmotionTitle(_, text), + let .postsTitle(_, text), let .boostOverviewTitle(_, text), let .boostPrepaidTitle(_, text), let .boostersTitle(_, text), @@ -527,10 +596,13 @@ private enum StatsEntry: ItemListNodeEntry { let .viewsByHourGraph(_, _, _, graph, type), let .viewsBySourceGraph(_, _, _, graph, type), let .followersBySourceGraph(_, _, _, graph, type), - let .languagesGraph(_, _, _, graph, type): + let .languagesGraph(_, _, _, graph, type), + let .reactionsByEmotionGraph(_, _, _, graph, type), + let .storyReactionsByEmotionGraph(_, _, _, graph, type): return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, sectionId: self.section, style: .blocks) case let .postInteractionsGraph(_, _, _, graph, type), - let .instantPageInteractionsGraph(_, _, _, graph, type): + let .instantPageInteractionsGraph(_, _, _, graph, type), + let .storyInteractionsGraph(_, _, _, 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 { @@ -760,6 +832,21 @@ private func channelStatsControllerEntries(state: ChannelStatsControllerState, p entries.append(.instantPageInteractionsGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.instantPageInteractionsGraph, .twoAxisStep)) } + if !data.reactionsByEmotionGraph.isEmpty { + entries.append(.reactionsByEmotionTitle(presentationData.theme, presentationData.strings.Stats_ReactionsByEmotionTitle)) + entries.append(.reactionsByEmotionGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.reactionsByEmotionGraph, .bars)) + } + + if !data.storyInteractionsGraph.isEmpty { + entries.append(.storyInteractionsTitle(presentationData.theme, presentationData.strings.Stats_StoryInteractionsTitle)) + entries.append(.storyInteractionsGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.storyInteractionsGraph, .twoAxisStep)) + } + + if !data.storyReactionsByEmotionGraph.isEmpty { + entries.append(.storyReactionsByEmotionTitle(presentationData.theme, presentationData.strings.Stats_StoryReactionsByEmotionTitle)) + entries.append(.storyReactionsByEmotionGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.storyReactionsByEmotionGraph, .bars)) + } + if let messages = messages, !messages.isEmpty, let interactions = interactions, !interactions.isEmpty { entries.append(.postsTitle(presentationData.theme, presentationData.strings.Stats_PostsTitle)) var index: Int32 = 0 @@ -915,6 +1002,9 @@ public func channelStatsController(context: AccountContext, updatedPresentationD statsContext.loadViewsBySourceGraph() statsContext.loadLanguagesGraph() statsContext.loadInstantPageInteractionsGraph() + statsContext.loadReactionsByEmotionGraph() + statsContext.loadStoryInteractionsGraph() + statsContext.loadStoryReactionsByEmotionGraph() } } }) diff --git a/submodules/StatisticsUI/Sources/MessageStatsController.swift b/submodules/StatisticsUI/Sources/MessageStatsController.swift index e4bef176be..0e2758ef6f 100644 --- a/submodules/StatisticsUI/Sources/MessageStatsController.swift +++ b/submodules/StatisticsUI/Sources/MessageStatsController.swift @@ -29,6 +29,7 @@ private final class MessageStatsControllerArguments { private enum StatsSection: Int32 { case overview case interactions + case reactions case publicForwards } @@ -39,6 +40,9 @@ private enum StatsEntry: ItemListNodeEntry { case interactionsTitle(PresentationTheme, String) case interactionsGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType) + case reactionsTitle(PresentationTheme, String) + case reactionsGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType) + case publicForwardsTitle(PresentationTheme, String) case publicForward(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, EngineMessage) @@ -48,6 +52,8 @@ private enum StatsEntry: ItemListNodeEntry { return StatsSection.overview.rawValue case .interactionsTitle, .interactionsGraph: return StatsSection.interactions.rawValue + case .reactionsTitle, .reactionsGraph: + return StatsSection.reactions.rawValue case .publicForwardsTitle, .publicForward: return StatsSection.publicForwards.rawValue } @@ -63,10 +69,14 @@ private enum StatsEntry: ItemListNodeEntry { return 2 case .interactionsGraph: return 3 - case .publicForwardsTitle: + case .reactionsTitle: return 4 + case .reactionsGraph: + return 5 + case .publicForwardsTitle: + return 6 case let .publicForward(index, _, _, _, _): - return 5 + index + return 7 + index } } @@ -96,6 +106,18 @@ private enum StatsEntry: ItemListNodeEntry { } else { return false } + case let .reactionsTitle(lhsTheme, lhsText): + if case let .reactionsTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .reactionsGraph(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsGraph, lhsType): + if case let .reactionsGraph(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsGraph, rhsType) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsGraph == rhsGraph, lhsType == rhsType { + return true + } else { + return false + } case let .publicForwardsTitle(lhsTheme, lhsText): if case let .publicForwardsTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true @@ -120,11 +142,12 @@ private enum StatsEntry: ItemListNodeEntry { switch self { case let .overviewTitle(_, text), let .interactionsTitle(_, text), + let .reactionsTitle(_, text), let .publicForwardsTitle(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .overview(_, stats, publicShares): return MessageStatsOverviewItem(presentationData: presentationData, stats: stats, publicShares: publicShares, sectionId: self.section, style: .blocks) - case let .interactionsGraph(_, _, _, graph, type): + case let .interactionsGraph(_, _, _, graph, type), let .reactionsGraph(_, _, _, 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 { @@ -170,6 +193,11 @@ private func messageStatsControllerEntries(data: MessageStats?, messages: Search entries.append(.interactionsGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.interactionsGraph, chartType)) } + + if !data.reactionsGraph.isEmpty { + entries.append(.reactionsTitle(presentationData.theme, presentationData.strings.Stats_MessageReactionsTitle.uppercased())) + entries.append(.reactionsGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.reactionsGraph, .bars)) + } if let messages = messages, !messages.messages.isEmpty { entries.append(.publicForwardsTitle(presentationData.theme, presentationData.strings.Stats_MessagePublicForwardsTitle.uppercased())) diff --git a/submodules/StatisticsUI/Sources/StatsGraphItem.swift b/submodules/StatisticsUI/Sources/StatsGraphItem.swift index dd10d55497..486051b656 100644 --- a/submodules/StatisticsUI/Sources/StatsGraphItem.swift +++ b/submodules/StatisticsUI/Sources/StatsGraphItem.swift @@ -180,6 +180,7 @@ class StatsGraphItemNode: ListViewItemNode { if let visibilityHeight = visibilityHeight { contentSize.height += visibilityHeight } + contentSize.height += 7.0 let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in diff --git a/submodules/TelegramCore/Sources/MessageStatistics.swift b/submodules/TelegramCore/Sources/Statistics/MessageStatistics.swift similarity index 90% rename from submodules/TelegramCore/Sources/MessageStatistics.swift rename to submodules/TelegramCore/Sources/Statistics/MessageStatistics.swift index 60d59f3b6e..a77f6e1b4c 100644 --- a/submodules/TelegramCore/Sources/MessageStatistics.swift +++ b/submodules/TelegramCore/Sources/Statistics/MessageStatistics.swift @@ -9,12 +9,14 @@ public struct MessageStats: Equatable { public let forwards: Int public let interactionsGraph: StatsGraph public let interactionsGraphDelta: Int64 + public let reactionsGraph: StatsGraph - init(views: Int, forwards: Int, interactionsGraph: StatsGraph, interactionsGraphDelta: Int64) { + init(views: Int, forwards: Int, interactionsGraph: StatsGraph, interactionsGraphDelta: Int64, reactionsGraph: StatsGraph) { self.views = views self.forwards = forwards self.interactionsGraph = interactionsGraph self.interactionsGraphDelta = interactionsGraphDelta + self.reactionsGraph = reactionsGraph } public static func == (lhs: MessageStats, rhs: MessageStats) -> Bool { @@ -30,11 +32,14 @@ public struct MessageStats: Equatable { if lhs.interactionsGraphDelta != rhs.interactionsGraphDelta { return false } + if lhs.reactionsGraph != rhs.reactionsGraph { + return false + } return true } public func withUpdatedInteractionsGraph(_ interactionsGraph: StatsGraph) -> MessageStats { - return MessageStats(views: self.views, forwards: self.forwards, interactionsGraph: interactionsGraph, interactionsGraphDelta: self.interactionsGraphDelta) + return MessageStats(views: self.views, forwards: self.forwards, interactionsGraph: interactionsGraph, interactionsGraphDelta: self.interactionsGraphDelta, reactionsGraph: self.reactionsGraph) } } @@ -83,8 +88,8 @@ private func requestMessageStats(postbox: Postbox, network: Network, datacenterI return signal |> mapToSignal { result -> Signal in - if case let .messageStats(apiViewsGraph, _) = result { - let interactionsGraph = StatsGraph(apiStatsGraph: apiViewsGraph) + if case let .messageStats(apiInteractionsGraph, apiReactionsGraph) = result { + let interactionsGraph = StatsGraph(apiStatsGraph: apiInteractionsGraph) var interactionsGraphDelta: Int64 = 86400 if case let .Loaded(_, data) = interactionsGraph { if let start = data.range(of: "[\"x\",") { @@ -101,8 +106,14 @@ private func requestMessageStats(postbox: Postbox, network: Network, datacenterI } } } - - return .single(MessageStats(views: views, forwards: forwards, interactionsGraph: interactionsGraph, interactionsGraphDelta: interactionsGraphDelta)) + let reactionsGraph = StatsGraph(apiStatsGraph: apiReactionsGraph) + return .single(MessageStats( + views: views, + forwards: forwards, + interactionsGraph: interactionsGraph, + interactionsGraphDelta: interactionsGraphDelta, + reactionsGraph: reactionsGraph + )) } else { return .single(nil) } diff --git a/submodules/TelegramCore/Sources/PeerStatistics.swift b/submodules/TelegramCore/Sources/Statistics/PeerStatistics.swift similarity index 95% rename from submodules/TelegramCore/Sources/PeerStatistics.swift rename to submodules/TelegramCore/Sources/Statistics/PeerStatistics.swift index ce6c239d16..38dc52b287 100644 --- a/submodules/TelegramCore/Sources/PeerStatistics.swift +++ b/submodules/TelegramCore/Sources/Statistics/PeerStatistics.swift @@ -468,6 +468,51 @@ private final class ChannelStatsContextImpl { } } + func loadReactionsByEmotionGraph() { + guard let stats = self._state.stats else { + return + } + if case let .OnDemand(token) = stats.reactionsByEmotionGraph { + self.disposables.set((requestGraph(network: self.network, datacenterId: self.datacenterId, token: token) + |> deliverOnMainQueue).start(next: { [weak self] graph in + if let strongSelf = self, let graph = graph { + strongSelf._state = ChannelStatsContextState(stats: strongSelf._state.stats?.withUpdatedReactionsByEmotionGraph(graph)) + strongSelf._statePromise.set(.single(strongSelf._state)) + } + }), forKey: token) + } + } + + func loadStoryInteractionsGraph() { + guard let stats = self._state.stats else { + return + } + if case let .OnDemand(token) = stats.storyInteractionsGraph { + self.disposables.set((requestGraph(network: self.network, datacenterId: self.datacenterId, token: token) + |> deliverOnMainQueue).start(next: { [weak self] graph in + if let strongSelf = self, let graph = graph { + strongSelf._state = ChannelStatsContextState(stats: strongSelf._state.stats?.withUpdatedStoryInteractionsGraph(graph)) + strongSelf._statePromise.set(.single(strongSelf._state)) + } + }), forKey: token) + } + } + + func loadStoryReactionsByEmotionGraph() { + guard let stats = self._state.stats else { + return + } + if case let .OnDemand(token) = stats.storyReactionsByEmotionGraph { + self.disposables.set((requestGraph(network: self.network, datacenterId: self.datacenterId, token: token) + |> deliverOnMainQueue).start(next: { [weak self] graph in + if let strongSelf = self, let graph = graph { + strongSelf._state = ChannelStatsContextState(stats: strongSelf._state.stats?.withUpdatedStoryReactionsByEmotionGraph(graph)) + strongSelf._statePromise.set(.single(strongSelf._state)) + } + }), forKey: token) + } + } + func loadDetailedGraph(_ graph: StatsGraph, x: Int64) -> Signal { if let token = graph.token { return requestGraph(network: self.network, datacenterId: self.datacenterId, token: token, x: x) @@ -551,6 +596,21 @@ public final class ChannelStatsContext { impl.loadLanguagesGraph() } } + public func loadReactionsByEmotionGraph() { + self.impl.with { impl in + impl.loadReactionsByEmotionGraph() + } + } + public func loadStoryInteractionsGraph() { + self.impl.with { impl in + impl.loadStoryInteractionsGraph() + } + } + public func loadStoryReactionsByEmotionGraph() { + self.impl.with { impl in + impl.loadStoryReactionsByEmotionGraph() + } + } public func loadDetailedGraph(_ graph: StatsGraph, x: Int64) -> Signal { return Signal { subscriber in diff --git a/submodules/TelegramCore/Sources/Statistics/StoryStatistics.swift b/submodules/TelegramCore/Sources/Statistics/StoryStatistics.swift new file mode 100644 index 0000000000..f6513bc075 --- /dev/null +++ b/submodules/TelegramCore/Sources/Statistics/StoryStatistics.swift @@ -0,0 +1,222 @@ +import Foundation +import SwiftSignalKit +import Postbox +import TelegramApi +import MtProtoKit + +public struct StoryStats: Equatable { + public let views: Int + public let forwards: Int + public let interactionsGraph: StatsGraph + public let interactionsGraphDelta: Int64 + public let reactionsGraph: StatsGraph + + init(views: Int, forwards: Int, interactionsGraph: StatsGraph, interactionsGraphDelta: Int64, reactionsGraph: StatsGraph) { + self.views = views + self.forwards = forwards + self.interactionsGraph = interactionsGraph + self.interactionsGraphDelta = interactionsGraphDelta + self.reactionsGraph = reactionsGraph + } + + public static func == (lhs: StoryStats, rhs: StoryStats) -> Bool { + if lhs.views != rhs.views { + return false + } + if lhs.forwards != rhs.forwards { + return false + } + if lhs.interactionsGraph != rhs.interactionsGraph { + return false + } + if lhs.interactionsGraphDelta != rhs.interactionsGraphDelta { + return false + } + if lhs.reactionsGraph != rhs.reactionsGraph { + return false + } + return true + } + + public func withUpdatedInteractionsGraph(_ interactionsGraph: StatsGraph) -> StoryStats { + return StoryStats(views: self.views, forwards: self.forwards, interactionsGraph: interactionsGraph, interactionsGraphDelta: self.interactionsGraphDelta, reactionsGraph: self.reactionsGraph) + } +} + +public struct StoryStatsContextState: Equatable { + public var stats: StoryStats? +} + +private func requestStoryStats(postbox: Postbox, network: Network, datacenterId: Int32, peerId: EnginePeer.Id, storyId: Int32, dark: Bool = false) -> Signal { + return postbox.transaction { transaction -> Peer? in + if let peer = transaction.getPeer(peerId) { + return peer + } else { + return nil + } + } |> mapToSignal { peer -> Signal in + guard let peer = peer, let inputPeer = apiInputPeer(peer) else { + return .never() + } + + var flags: Int32 = 0 + if dark { + flags |= (1 << 1) + } + + let request = Api.functions.stats.getStoryStats(flags: flags, peer: inputPeer, id: storyId) + let signal: Signal + if network.datacenterId != datacenterId { + signal = network.download(datacenterId: Int(datacenterId), isMedia: false, tag: nil) + |> castError(MTRpcError.self) + |> mapToSignal { worker in + return worker.request(request) + } + } else { + signal = network.request(request) + } + + let views: Int = 0 + let forwards: Int = 0 +// for attribute in story.attributes { +// if let viewsAttribute = attribute as? ViewCountStoryAttribute { +// views = viewsAttribute.count +// } else if let forwardsAttribute = attribute as? ForwardCountStoryAttribute { +// forwards = forwardsAttribute.count +// } +// } + + return signal + |> mapToSignal { result -> Signal in + if case let .storyStats(apiInteractionsGraph, apiReactionsGraph) = result { + let interactionsGraph = StatsGraph(apiStatsGraph: apiInteractionsGraph) + var interactionsGraphDelta: Int64 = 86400 + if case let .Loaded(_, data) = interactionsGraph { + if let start = data.range(of: "[\"x\",") { + let substring = data.suffix(from: start.upperBound) + if let end = substring.range(of: "],") { + let valuesString = substring.prefix(through: substring.index(before: end.lowerBound)) + let values = valuesString.components(separatedBy: ",").compactMap { Int64($0) } + if values.count > 1 { + let first = values[0] + let second = values[1] + let delta = abs(second - first) / 1000 + interactionsGraphDelta = delta + } + } + } + } + let reactionsGraph = StatsGraph(apiStatsGraph: apiReactionsGraph) + return .single(StoryStats( + views: views, + forwards: forwards, + interactionsGraph: interactionsGraph, + interactionsGraphDelta: interactionsGraphDelta, + reactionsGraph: reactionsGraph + )) + } else { + return .single(nil) + } + } + |> retryRequest + } +} + +private final class StoryStatsContextImpl { + private let postbox: Postbox + private let network: Network + private let datacenterId: Int32 + private let peerId: EnginePeer.Id + private let storyId: Int32 + + private var _state: StoryStatsContextState { + didSet { + if self._state != oldValue { + self._statePromise.set(.single(self._state)) + } + } + } + private let _statePromise = Promise() + var state: Signal { + return self._statePromise.get() + } + + private let disposable = MetaDisposable() + private let disposables = DisposableDict() + + init(postbox: Postbox, network: Network, datacenterId: Int32, peerId: EnginePeer.Id, storyId: Int32) { + assert(Queue.mainQueue().isCurrent()) + + self.postbox = postbox + self.network = network + self.datacenterId = datacenterId + self.peerId = peerId + self.storyId = storyId + self._state = StoryStatsContextState(stats: nil) + self._statePromise.set(.single(self._state)) + + self.load() + } + + deinit { + assert(Queue.mainQueue().isCurrent()) + self.disposable.dispose() + self.disposables.dispose() + } + + private func load() { + assert(Queue.mainQueue().isCurrent()) + + self.disposable.set((requestStoryStats(postbox: self.postbox, network: self.network, datacenterId: self.datacenterId, peerId: self.peerId, storyId: self.storyId) + |> deliverOnMainQueue).start(next: { [weak self] stats in + if let strongSelf = self { + strongSelf._state = StoryStatsContextState(stats: stats) + strongSelf._statePromise.set(.single(strongSelf._state)) + } + })) + } + + func loadDetailedGraph(_ graph: StatsGraph, x: Int64) -> Signal { + if let token = graph.token { + return requestGraph(network: self.network, datacenterId: self.datacenterId, token: token, x: x) + } else { + return .single(nil) + } + } +} + +public final class StoryStatsContext { + private let impl: QueueLocalObject + + public var state: Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + self.impl.with { impl in + disposable.set(impl.state.start(next: { value in + subscriber.putNext(value) + })) + } + return disposable + } + } + + public init(postbox: Postbox, network: Network, datacenterId: Int32, peerId: EnginePeer.Id, storyId: Int32) { + self.impl = QueueLocalObject(queue: Queue.mainQueue(), generate: { + return StoryStatsContextImpl(postbox: postbox, network: network, datacenterId: datacenterId, peerId: peerId, storyId: storyId) + }) + } + + public func loadDetailedGraph(_ graph: StatsGraph, x: Int64) -> Signal { + 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) + subscriber.putCompletion() + })) + } + return disposable + } + } +} +