import Foundation import UIKit import Display import SwiftSignalKit import Postbox import TelegramCore import SyncCore import MapKit import TelegramPresentationData import TelegramUIPreferences import TelegramStringFormatting import ItemListUI import PresentationDataUtils import AccountContext import PresentationDataUtils import AppBundle import GraphUI import ItemListPeerItem private final class GroupStatsControllerArguments { let context: AccountContext let loadDetailedGraph: (StatsGraph, Int64) -> Signal let openPeer: (PeerId) -> Void let openPeerHistory: (PeerId) -> Void let openPeerAdminActions: (PeerId) -> Void let promotePeer: (PeerId) -> Void let setPostersPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void let setAdminsPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void let setInvitersPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void init(context: AccountContext, loadDetailedGraph: @escaping (StatsGraph, Int64) -> Signal, openPeer: @escaping (PeerId) -> Void, openPeerHistory: @escaping (PeerId) -> Void, openPeerAdminActions: @escaping (PeerId) -> Void, promotePeer: @escaping (PeerId) -> Void, setPostersPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, setAdminsPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, setInvitersPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void) { self.context = context self.loadDetailedGraph = loadDetailedGraph self.openPeer = openPeer self.openPeerHistory = openPeerHistory self.openPeerAdminActions = openPeerAdminActions self.promotePeer = promotePeer self.setPostersPeerIdWithRevealedOptions = setPostersPeerIdWithRevealedOptions self.setAdminsPeerIdWithRevealedOptions = setAdminsPeerIdWithRevealedOptions self.setInvitersPeerIdWithRevealedOptions = setInvitersPeerIdWithRevealedOptions } } private enum StatsSection: Int32 { case overview case growth case members case newMembersBySource case languages case messages case actions case topHours case topWeekdays case topPosters case topAdmins case topInviters } private enum StatsEntry: ItemListNodeEntry { case overviewTitle(PresentationTheme, String, String) case overview(PresentationTheme, GroupStats) case growthTitle(PresentationTheme, String) case growthGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType) case membersTitle(PresentationTheme, String) case membersGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType) case newMembersBySourceTitle(PresentationTheme, String) case newMembersBySourceGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType) case languagesTitle(PresentationTheme, String) case languagesGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType) case messagesTitle(PresentationTheme, String) case messagesGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType) case actionsTitle(PresentationTheme, String) case actionsGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType) case topHoursTitle(PresentationTheme, String) case topHoursGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType) case topWeekdaysTitle(PresentationTheme, String) case topWeekdaysGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType) case topPostersTitle(PresentationTheme, String, String) case topPoster(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, Peer, GroupStatsTopPoster, Bool) case topAdminsTitle(PresentationTheme, String, String) case topAdmin(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, Peer, GroupStatsTopAdmin, Bool) case topInvitersTitle(PresentationTheme, String, String) case topInviter(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, Peer, GroupStatsTopInviter, Bool) var section: ItemListSectionId { switch self { case .overviewTitle, .overview: return StatsSection.overview.rawValue case .growthTitle, .growthGraph: return StatsSection.growth.rawValue case .membersTitle, .membersGraph: return StatsSection.members.rawValue case .newMembersBySourceTitle, .newMembersBySourceGraph: return StatsSection.newMembersBySource.rawValue case .languagesTitle, .languagesGraph: return StatsSection.languages.rawValue case .messagesTitle, . messagesGraph: return StatsSection.messages.rawValue case .actionsTitle, .actionsGraph: return StatsSection.actions.rawValue case .topHoursTitle, .topHoursGraph: return StatsSection.topHours.rawValue case .topWeekdaysTitle, .topWeekdaysGraph: return StatsSection.topWeekdays.rawValue case .topPostersTitle, .topPoster: return StatsSection.topPosters.rawValue case .topAdminsTitle, .topAdmin: return StatsSection.topAdmins.rawValue case .topInvitersTitle, .topInviter: return StatsSection.topInviters.rawValue } } var stableId: Int32 { switch self { case .overviewTitle: return 0 case .overview: return 1 case .growthTitle: return 2 case .growthGraph: return 3 case .membersTitle: return 4 case .membersGraph: return 5 case .newMembersBySourceTitle: return 6 case .newMembersBySourceGraph: return 7 case .languagesTitle: return 8 case .languagesGraph: return 9 case .messagesTitle: return 10 case .messagesGraph: return 11 case .actionsTitle: return 12 case .actionsGraph: return 13 case .topHoursTitle: return 14 case .topHoursGraph: return 15 case .topWeekdaysTitle: return 16 case .topWeekdaysGraph: return 17 case .topPostersTitle: return 1000 case let .topPoster(index, _, _, _, _, _, _): return 1001 + index case .topAdminsTitle: return 2000 case let .topAdmin(index, _, _, _, _, _, _): return 2001 + index case .topInvitersTitle: return 3000 case let .topInviter(index, _, _, _, _, _, _): return 30001 + index } } static func ==(lhs: StatsEntry, rhs: StatsEntry) -> Bool { switch lhs { case let .overviewTitle(lhsTheme, lhsText, lhsDates): if case let .overviewTitle(rhsTheme, rhsText, rhsDates) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsDates == rhsDates { return true } else { return false } case let .overview(lhsTheme, lhsStats): if case let .overview(rhsTheme, rhsStats) = rhs, lhsTheme === rhsTheme, lhsStats == rhsStats { return true } else { return false } case let .growthTitle(lhsTheme, lhsText): if case let .growthTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .growthGraph(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsGraph, lhsType): if case let .growthGraph(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsGraph, rhsType) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsGraph == rhsGraph, lhsType == rhsType { return true } else { return false } case let .membersTitle(lhsTheme, lhsText): if case let .membersTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .membersGraph(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsGraph, lhsType): if case let .membersGraph(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsGraph, rhsType) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsGraph == rhsGraph, lhsType == rhsType { return true } else { return false } case let .newMembersBySourceTitle(lhsTheme, lhsText): if case let .newMembersBySourceTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .newMembersBySourceGraph(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsGraph, lhsType): if case let .newMembersBySourceGraph(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsGraph, rhsType) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsGraph == rhsGraph, lhsType == rhsType { return true } else { return false } case let .languagesTitle(lhsTheme, lhsText): if case let .languagesTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .languagesGraph(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsGraph, lhsType): if case let .languagesGraph(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsGraph, rhsType) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsGraph == rhsGraph, lhsType == rhsType { return true } else { return false } case let .messagesTitle(lhsTheme, lhsText): if case let .messagesTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .messagesGraph(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsGraph, lhsType): if case let .messagesGraph(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsGraph, rhsType) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsGraph == rhsGraph, lhsType == rhsType { return true } else { return false } case let .actionsTitle(lhsTheme, lhsText): if case let .actionsTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .actionsGraph(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsGraph, lhsType): if case let .actionsGraph(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsGraph, rhsType) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsGraph == rhsGraph, lhsType == rhsType { return true } else { return false } case let .topHoursTitle(lhsTheme, lhsText): if case let .topHoursTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .topHoursGraph(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsGraph, lhsType): if case let .topHoursGraph(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsGraph, rhsType) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsGraph == rhsGraph, lhsType == rhsType { return true } else { return false } case let .topWeekdaysTitle(lhsTheme, lhsText): if case let .topWeekdaysTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .topWeekdaysGraph(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsGraph, lhsType): if case let .topWeekdaysGraph(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsGraph, rhsType) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsGraph == rhsGraph, lhsType == rhsType { return true } else { return false } case let .topPostersTitle(lhsTheme, lhsText, lhsDates): if case let .topPostersTitle(rhsTheme, rhsText, rhsDates) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsDates == rhsDates { return true } else { return false } case let .topPoster(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsPeer, lhsTopPoster, lhsRevealed): if case let .topPoster(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsPeer, rhsTopPoster, rhsRevealed) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, arePeersEqual(lhsPeer, rhsPeer), lhsTopPoster == rhsTopPoster, lhsRevealed == rhsRevealed { return true } else { return false } case let .topAdminsTitle(lhsTheme, lhsText, lhsDates): if case let .topAdminsTitle(rhsTheme, rhsText, rhsDates) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsDates == rhsDates { return true } else { return false } case let .topAdmin(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsPeer, lhsTopAdmin, lhsRevealed): if case let .topAdmin(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsPeer, rhsTopAdmin, rhsRevealed) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, arePeersEqual(lhsPeer, rhsPeer), lhsTopAdmin == rhsTopAdmin, lhsRevealed == rhsRevealed { return true } else { return false } case let .topInvitersTitle(lhsTheme, lhsText, lhsDates): if case let .topInvitersTitle(rhsTheme, rhsText, rhsDates) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsDates == rhsDates { return true } else { return false } case let .topInviter(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsPeer, lhsTopInviter, lhsRevealed): if case let .topInviter(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsPeer, rhsTopInviter, rhsRevealed) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, arePeersEqual(lhsPeer, rhsPeer), lhsTopInviter == rhsTopInviter, lhsRevealed == rhsRevealed { return true } else { return false } } } static func <(lhs: StatsEntry, rhs: StatsEntry) -> Bool { return lhs.stableId < rhs.stableId } func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! GroupStatsControllerArguments switch self { case let .overviewTitle(_, text, dates): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, accessoryText: ItemListSectionHeaderAccessoryText(value: dates, color: .generic), sectionId: self.section) case let .growthTitle(_, text), let .membersTitle(_, text), let .newMembersBySourceTitle(_, text), let .languagesTitle(_, text), let .messagesTitle(_, text), let .actionsTitle(_, text), let .topHoursTitle(_, text), let .topWeekdaysTitle(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .topPostersTitle(_, text, dates), let .topAdminsTitle(_, text, dates), let .topInvitersTitle(_, text, dates): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, accessoryText: ItemListSectionHeaderAccessoryText(value: dates, color: .generic), sectionId: self.section) case let .overview(_, stats): return StatsOverviewItem(presentationData: presentationData, stats: stats, sectionId: self.section, style: .blocks) case let .growthGraph(_, _, _, graph, type), let .membersGraph(_, _, _, graph, type), let .newMembersBySourceGraph(_, _, _, graph, type), let .languagesGraph(_, _, _, graph, type), let .messagesGraph(_, _, _, graph, type), let .actionsGraph(_, _, _, graph, type), let .topHoursGraph(_, _, _, graph, type), let .topWeekdaysGraph(_, _, _, graph, type): return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, sectionId: self.section, style: .blocks) case let .topPoster(_, _, strings, dateTimeFormat, peer, topPoster, revealed): var textComponents: [String] = [] if topPoster.messageCount > 0 { textComponents.append(strings.Stats_GroupTopPosterMessages(topPoster.messageCount)) if topPoster.averageChars > 0 { textComponents.append(strings.Stats_GroupTopPosterChars(topPoster.averageChars)) } } var options: [ItemListPeerItemRevealOption] = [] options.append(ItemListPeerItemRevealOption(type: .accent, title: strings.Stats_GroupTopPoster_History, action: { arguments.openPeerHistory(peer.id) })) options.append(ItemListPeerItemRevealOption(type: .neutral, title: strings.Stats_GroupTopPoster_Promote, action: { arguments.promotePeer(peer.id) })) return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: .firstLast, context: arguments.context, peer: peer, height: .generic, aliasHandling: .standard, nameColor: .primary, nameStyle: .plain, presence: nil, text: .text(textComponents.joined(separator: ", ")), label: .none, editing: ItemListPeerItemEditing(editable: true, editing: false, revealed: revealed), revealOptions: ItemListPeerItemRevealOptions(options: options), switchValue: nil, enabled: true, highlighted: false, selectable: true, sectionId: self.section, action: { arguments.openPeer(peer.id) }, setPeerIdWithRevealedOptions: { peerId, fromPeerId in arguments.setPostersPeerIdWithRevealedOptions(peerId, fromPeerId) }, removePeer: { _ in }) case let .topAdmin(_, _, strings, dateTimeFormat, peer, topAdmin, revealed): var textComponents: [String] = [] if topAdmin.deletedCount > 0 { textComponents.append(strings.Stats_GroupTopAdminDeletions(topAdmin.deletedCount)) } if topAdmin.kickedCount > 0 { textComponents.append(strings.Stats_GroupTopAdminKicks(topAdmin.kickedCount)) } if topAdmin.bannedCount > 0 { textComponents.append(strings.Stats_GroupTopAdminBans(topAdmin.bannedCount)) } var options: [ItemListPeerItemRevealOption] = [] options.append(ItemListPeerItemRevealOption(type: .accent, title: strings.Stats_GroupTopAdmin_Actions, action: { arguments.openPeerAdminActions(peer.id) })) options.append(ItemListPeerItemRevealOption(type: .neutral, title: strings.Stats_GroupTopAdmin_Promote, action: { arguments.promotePeer(peer.id) })) return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: .firstLast, context: arguments.context, peer: peer, height: .generic, aliasHandling: .standard, nameColor: .primary, nameStyle: .plain, presence: nil, text: .text(textComponents.joined(separator: ", ")), label: .none, editing: ItemListPeerItemEditing(editable: true, editing: false, revealed: revealed), revealOptions: ItemListPeerItemRevealOptions(options: options), switchValue: nil, enabled: true, highlighted: false, selectable: true, sectionId: self.section, action: { arguments.openPeer(peer.id) }, setPeerIdWithRevealedOptions: { peerId, fromPeerId in arguments.setAdminsPeerIdWithRevealedOptions(peerId, fromPeerId) }, removePeer: { _ in }) case let .topInviter(_, _, strings, dateTimeFormat, peer, topInviter, revealed): var textComponents: [String] = [] textComponents.append(strings.Stats_GroupTopInviterInvites(topInviter.inviteCount)) var options: [ItemListPeerItemRevealOption] = [] options.append(ItemListPeerItemRevealOption(type: .accent, title: strings.Stats_GroupTopPoster_History, action: { arguments.openPeerHistory(peer.id) })) options.append(ItemListPeerItemRevealOption(type: .neutral, title: strings.Stats_GroupTopPoster_Promote, action: { arguments.promotePeer(peer.id) })) return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: .firstLast, context: arguments.context, peer: peer, height: .generic, aliasHandling: .standard, nameColor: .primary, nameStyle: .plain, presence: nil, text: .text(textComponents.joined(separator: ", ")), label: .none, editing: ItemListPeerItemEditing(editable: true, editing: false, revealed: revealed), revealOptions: ItemListPeerItemRevealOptions(options: options), switchValue: nil, enabled: true, highlighted: false, selectable: true, sectionId: self.section, action: { arguments.openPeer(peer.id) }, setPeerIdWithRevealedOptions: { peerId, fromPeerId in arguments.setInvitersPeerIdWithRevealedOptions(peerId, fromPeerId) }, removePeer: { _ in }) } } } private func groupStatsControllerEntries(state: GroupStatsState, data: GroupStats?, peers: [PeerId: Peer]?, presentationData: PresentationData) -> [StatsEntry] { var entries: [StatsEntry] = [] if let data = data { let minDate = stringForDate(timestamp: data.period.minDate, strings: presentationData.strings) let maxDate = stringForDate(timestamp: data.period.maxDate, strings: presentationData.strings) let dates = "\(minDate) – \(maxDate)" entries.append(.overviewTitle(presentationData.theme, presentationData.strings.Stats_Overview, dates)) entries.append(.overview(presentationData.theme, data)) if !data.growthGraph.isEmpty { entries.append(.growthTitle(presentationData.theme, presentationData.strings.Stats_GroupGrowthTitle)) entries.append(.growthGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.growthGraph, .lines)) } if !data.membersGraph.isEmpty { entries.append(.membersTitle(presentationData.theme, presentationData.strings.Stats_GroupMembersTitle)) entries.append(.membersGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.membersGraph, .lines)) } if !data.newMembersBySourceGraph.isEmpty { entries.append(.newMembersBySourceTitle(presentationData.theme, presentationData.strings.Stats_GroupNewMembersBySourceTitle)) entries.append(.newMembersBySourceGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.newMembersBySourceGraph, .bars)) } if !data.languagesGraph.isEmpty { entries.append(.languagesTitle(presentationData.theme, presentationData.strings.Stats_GroupLanguagesTitle)) entries.append(.languagesGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.languagesGraph, .pie)) } if !data.messagesGraph.isEmpty { entries.append(.messagesTitle(presentationData.theme, presentationData.strings.Stats_GroupMessagesTitle)) entries.append(.messagesGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.messagesGraph, .bars)) } if !data.actionsGraph.isEmpty { entries.append(.actionsTitle(presentationData.theme, presentationData.strings.Stats_GroupActionsTitle)) entries.append(.actionsGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.actionsGraph, .lines)) } if !data.topHoursGraph.isEmpty { entries.append(.topHoursTitle(presentationData.theme, presentationData.strings.Stats_GroupTopHoursTitle)) entries.append(.topHoursGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.topHoursGraph, .hourlyStep)) } if !data.topWeekdaysGraph.isEmpty { entries.append(.topWeekdaysTitle(presentationData.theme, presentationData.strings.Stats_GroupTopWeekdaysTitle)) entries.append(.topWeekdaysGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.topWeekdaysGraph, .pie)) } if let peers = peers { if !data.topPosters.isEmpty { entries.append(.topPostersTitle(presentationData.theme, presentationData.strings.Stats_GroupTopPostersTitle, dates)) var index: Int32 = 0 for topPoster in data.topPosters { if let peer = peers[topPoster.peerId], topPoster.messageCount > 0 { entries.append(.topPoster(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer, topPoster, topPoster.peerId == state.posterPeerIdWithRevealedOptions)) index += 1 } } } if !data.topAdmins.isEmpty { entries.append(.topAdminsTitle(presentationData.theme, presentationData.strings.Stats_GroupTopAdminsTitle, dates)) var index: Int32 = 0 for topAdmin in data.topAdmins { if let peer = peers[topAdmin.peerId], (topAdmin.deletedCount + topAdmin.kickedCount + topAdmin.bannedCount) > 0 { entries.append(.topAdmin(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer, topAdmin, topAdmin.peerId == state.adminPeerIdWithRevealedOptions)) index += 1 } } } if !data.topInviters.isEmpty { entries.append(.topInvitersTitle(presentationData.theme, presentationData.strings.Stats_GroupTopInvitersTitle, dates)) var index: Int32 = 0 for topInviter in data.topInviters { if let peer = peers[topInviter.peerId], topInviter.inviteCount > 0 { entries.append(.topInviter(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer, topInviter, topInviter.peerId == state.inviterPeerIdWithRevealedOptions)) index += 1 } } } } } return entries } private struct GroupStatsState: Equatable { let posterPeerIdWithRevealedOptions: PeerId? let adminPeerIdWithRevealedOptions: PeerId? let inviterPeerIdWithRevealedOptions: PeerId? init() { self.posterPeerIdWithRevealedOptions = nil self.adminPeerIdWithRevealedOptions = nil self.inviterPeerIdWithRevealedOptions = nil } init(posterPeerIdWithRevealedOptions: PeerId?, adminPeerIdWithRevealedOptions: PeerId?, inviterPeerIdWithRevealedOptions: PeerId?) { self.posterPeerIdWithRevealedOptions = posterPeerIdWithRevealedOptions self.adminPeerIdWithRevealedOptions = adminPeerIdWithRevealedOptions self.inviterPeerIdWithRevealedOptions = inviterPeerIdWithRevealedOptions } static func ==(lhs: GroupStatsState, rhs: GroupStatsState) -> Bool { if lhs.posterPeerIdWithRevealedOptions != rhs.posterPeerIdWithRevealedOptions { return false } if lhs.adminPeerIdWithRevealedOptions != rhs.adminPeerIdWithRevealedOptions { return false } if lhs.inviterPeerIdWithRevealedOptions != rhs.inviterPeerIdWithRevealedOptions { return false } return true } func withUpdatedPosterPeerIdWithRevealedOptions(_ posterPeerIdWithRevealedOptions: PeerId?) -> GroupStatsState { return GroupStatsState(posterPeerIdWithRevealedOptions: posterPeerIdWithRevealedOptions, adminPeerIdWithRevealedOptions: posterPeerIdWithRevealedOptions != nil ? nil : self.adminPeerIdWithRevealedOptions, inviterPeerIdWithRevealedOptions: posterPeerIdWithRevealedOptions != nil ? nil : self.inviterPeerIdWithRevealedOptions) } func withUpdatedAdminPeerIdWithRevealedOptions(_ adminPeerIdWithRevealedOptions: PeerId?) -> GroupStatsState { return GroupStatsState(posterPeerIdWithRevealedOptions: adminPeerIdWithRevealedOptions != nil ? nil : self.posterPeerIdWithRevealedOptions, adminPeerIdWithRevealedOptions: adminPeerIdWithRevealedOptions, inviterPeerIdWithRevealedOptions: adminPeerIdWithRevealedOptions != nil ? nil : self.inviterPeerIdWithRevealedOptions) } func withUpdatedInviterPeerIdWithRevealedOptions(_ inviterPeerIdWithRevealedOptions: PeerId?) -> GroupStatsState { return GroupStatsState(posterPeerIdWithRevealedOptions: inviterPeerIdWithRevealedOptions != nil ? nil : self.posterPeerIdWithRevealedOptions, adminPeerIdWithRevealedOptions: inviterPeerIdWithRevealedOptions != nil ? nil : self.adminPeerIdWithRevealedOptions, inviterPeerIdWithRevealedOptions: inviterPeerIdWithRevealedOptions) } } public func groupStatsController(context: AccountContext, peerId: PeerId, cachedPeerData: CachedPeerData) -> ViewController { let statePromise = ValuePromise(GroupStatsState()) let stateValue = Atomic(value: GroupStatsState()) let updateState: ((GroupStatsState) -> GroupStatsState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } let actionsDisposable = DisposableSet() let dataPromise = Promise(nil) let peersPromise = Promise<[PeerId: Peer]?>(nil) var datacenterId: Int32 = 0 if let cachedData = cachedPeerData as? CachedChannelData { datacenterId = cachedData.statsDatacenterId } var openPeerImpl: ((PeerId) -> Void)? var openPeerHistoryImpl: ((PeerId) -> Void)? var openPeerAdminActionsImpl: ((PeerId) -> Void)? var promotePeerImpl: ((PeerId) -> Void)? let peerView = Promise() peerView.set(context.account.viewTracker.peerView(peerId, updateData: true)) let statsContext = GroupStatsContext(postbox: context.account.postbox, network: context.account.network, datacenterId: datacenterId, peerId: peerId) let dataSignal: Signal = statsContext.state |> map { state in return state.stats } |> afterNext({ [weak statsContext] stats in if let statsContext = statsContext, let stats = stats { if case .OnDemand = stats.topWeekdaysGraph { statsContext.loadGrowthGraph() statsContext.loadMembersGraph() statsContext.loadNewMembersBySourceGraph() statsContext.loadLanguagesGraph() statsContext.loadMessagesGraph() statsContext.loadActionsGraph() statsContext.loadTopHoursGraph() statsContext.loadTopWeekdaysGraph() } } }) dataPromise.set(.single(nil) |> then(dataSignal)) peersPromise.set(.single(nil) |> then(dataPromise.get() |> filter { value in return value != nil } |> take(1) |> map { stats -> [PeerId]? in guard let stats = stats else { return nil } var peerIds = Set() peerIds.formUnion(stats.topPosters.map { $0.peerId }) peerIds.formUnion(stats.topAdmins.map { $0.peerId }) peerIds.formUnion(stats.topInviters.map { $0.peerId }) return Array(peerIds) } |> mapToSignal { peerIds -> Signal<[PeerId: Peer]?, NoError> in return context.account.postbox.transaction { transaction -> [PeerId: Peer]? in var peers: [PeerId: Peer] = [:] if let peerIds = peerIds { for peerId in peerIds { if let peer = transaction.getPeer(peerId) { peers[peerId] = peer } } } return peers } })) let arguments = GroupStatsControllerArguments(context: context, loadDetailedGraph: { graph, x -> Signal in return statsContext.loadDetailedGraph(graph, x: x) }, openPeer: { peerId in openPeerImpl?(peerId) }, openPeerHistory: { peerId in openPeerHistoryImpl?(peerId) }, openPeerAdminActions: { peerId in openPeerAdminActionsImpl?(peerId) }, promotePeer: { peerId in promotePeerImpl?(peerId) }, setPostersPeerIdWithRevealedOptions: { peerId, fromPeerId in updateState { state in if (peerId == nil && fromPeerId == state.posterPeerIdWithRevealedOptions) || (peerId != nil && fromPeerId == nil) { return state.withUpdatedPosterPeerIdWithRevealedOptions(peerId) } else { return state } } }, setAdminsPeerIdWithRevealedOptions: { peerId, fromPeerId in updateState { state in if (peerId == nil && fromPeerId == state.adminPeerIdWithRevealedOptions) || (peerId != nil && fromPeerId == nil) { return state.withUpdatedAdminPeerIdWithRevealedOptions(peerId) } else { return state } } }, setInvitersPeerIdWithRevealedOptions: { peerId, fromPeerId in updateState { state in if (peerId == nil && fromPeerId == state.inviterPeerIdWithRevealedOptions) || (peerId != nil && fromPeerId == nil) { return state.withUpdatedInviterPeerIdWithRevealedOptions(peerId) } else { return state } } }) let longLoadingSignal: Signal = .single(false) |> then(.single(true) |> delay(2.0, queue: Queue.mainQueue())) let previousData = Atomic(value: nil) let signal = combineLatest(statePromise.get(), context.sharedContext.presentationData, dataPromise.get(), peersPromise.get(), longLoadingSignal) |> deliverOnMainQueue |> map { state, presentationData, data, peers, longLoading -> (ItemListControllerState, (ItemListNodeState, Any)) in let previous = previousData.swap(data) var emptyStateItem: ItemListControllerEmptyStateItem? if data == nil { if longLoading { emptyStateItem = StatsEmptyStateItem(theme: presentationData.theme, strings: presentationData.strings) } else { emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme) } } 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: groupStatsControllerEntries(state: state, data: data, peers: peers, presentationData: presentationData), style: .blocks, emptyStateItem: emptyStateItem, crossfadeState: previous == nil, animateChanges: false) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() let _ = statsContext.state } let controller = ItemListController(context: context, state: signal) controller.contentOffsetChanged = { [weak controller] _, _ in controller?.forEachItemNode({ itemNode in if let itemNode = itemNode as? StatsGraphItemNode { itemNode.resetInteraction() } }) } controller.didDisappear = { [weak controller] _ in controller?.clearItemNodesHighlight(animated: true) } openPeerImpl = { [weak controller] peerId in if let navigationController = controller?.navigationController as? NavigationController { let _ = (context.account.postbox.loadedPeerWithId(peerId) |> take(1) |> deliverOnMainQueue).start(next: { peer in if let controller = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) { navigationController.pushViewController(controller) } }) } } openPeerHistoryImpl = { [weak controller] participantPeerId in if let navigationController = controller?.navigationController as? NavigationController { let _ = (context.account.postbox.loadedPeerWithId(participantPeerId) |> take(1) |> deliverOnMainQueue).start(next: { peer in context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, chatController: nil, context: context, chatLocation: .peer(peerId), subject: nil, botStart: nil, updateTextInputState: nil, activateInput: false, keepStack: .always, useExisting: false, purposefulAction: nil, scrollToEndIfExists: false, activateMessageSearch: (.member(peer), ""), animated: true)) }) } } openPeerAdminActionsImpl = { [weak controller] participantPeerId in if let navigationController = controller?.navigationController as? NavigationController { let _ = (context.account.postbox.loadedPeerWithId(peerId) |> take(1) |> deliverOnMainQueue).start(next: { peer in let controller = context.sharedContext.makeChatRecentActionsController(context: context, peer: peer) navigationController.pushViewController(controller) }) } } promotePeerImpl = { [weak controller] participantPeerId in if let navigationController = controller?.navigationController as? NavigationController { let _ = (fetchChannelParticipant(account: context.account, peerId: peerId, participantId: participantPeerId) |> take(1) |> deliverOnMainQueue).start(next: { participant in if let participant = participant, let controller = context.sharedContext.makeChannelAdminController(context: context, peerId: peerId, adminId: participantPeerId, initialParticipant: participant) { navigationController.pushViewController(controller) } }) } } return controller }