import Foundation import UIKit import Display import AsyncDisplayKit import SwiftSignalKit import Postbox import TelegramCore import TelegramPresentationData import TelegramUIPreferences import TelegramStringFormatting import ItemListUI import PresentationDataUtils import AccountContext import PresentationDataUtils import AppBundle import GraphUI import ContextUI import ItemListPeerItem import InviteLinksUI import UndoUI import ShareController import ItemListPeerActionItem import PremiumUI import StoryContainerScreen import TelegramNotices import ComponentFlow import BoostLevelIconComponent import StarsWithdrawalScreen private let initialBoostersDisplayedLimit: Int32 = 5 private let initialTransactionsDisplayedLimit: Int32 = 5 private final class ChannelStatsControllerArguments { let context: AccountContext let loadDetailedGraph: (StatsGraph, Int64) -> Signal let openPostStats: (EnginePeer, StatsPostItem) -> Void let openStory: (EngineStoryItem, UIView) -> Void let contextAction: (MessageId, ASDisplayNode, ContextGesture?) -> Void let copyBoostLink: (String) -> Void let shareBoostLink: (String) -> Void let openBoost: (ChannelBoostersContext.State.Boost) -> Void let expandBoosters: () -> Void let openGifts: () -> Void let createPrepaidGiveaway: (PrepaidGiveaway) -> Void let updateGiftsSelected: (Bool) -> Void let updateStarsSelected: (Bool) -> Void let requestTonWithdraw: () -> Void let requestStarsWithdraw: () -> Void let showTimeoutTooltip: (Int32) -> Void let buyAds: () -> Void let openMonetizationIntro: () -> Void let openMonetizationInfo: () -> Void let openTonTransaction: (RevenueStatsTransactionsContext.State.Transaction) -> Void let openStarsTransaction: (StarsContext.State.Transaction) -> Void let expandTransactions: (Bool) -> Void let updateCpmEnabled: (Bool) -> Void let presentCpmLocked: () -> Void let dismissInput: () -> Void init(context: AccountContext, loadDetailedGraph: @escaping (StatsGraph, Int64) -> Signal, openPostStats: @escaping (EnginePeer, StatsPostItem) -> Void, openStory: @escaping (EngineStoryItem, UIView) -> Void, contextAction: @escaping (MessageId, ASDisplayNode, ContextGesture?) -> Void, copyBoostLink: @escaping (String) -> Void, shareBoostLink: @escaping (String) -> Void, openBoost: @escaping (ChannelBoostersContext.State.Boost) -> Void, expandBoosters: @escaping () -> Void, openGifts: @escaping () -> Void, createPrepaidGiveaway: @escaping (PrepaidGiveaway) -> Void, updateGiftsSelected: @escaping (Bool) -> Void, updateStarsSelected: @escaping (Bool) -> Void, requestTonWithdraw: @escaping () -> Void, requestStarsWithdraw: @escaping () -> Void, showTimeoutTooltip: @escaping (Int32) -> Void, buyAds: @escaping () -> Void, openMonetizationIntro: @escaping () -> Void, openMonetizationInfo: @escaping () -> Void, openTonTransaction: @escaping (RevenueStatsTransactionsContext.State.Transaction) -> Void, openStarsTransaction: @escaping (StarsContext.State.Transaction) -> Void, expandTransactions: @escaping (Bool) -> Void, updateCpmEnabled: @escaping (Bool) -> Void, presentCpmLocked: @escaping () -> Void, dismissInput: @escaping () -> Void) { self.context = context self.loadDetailedGraph = loadDetailedGraph self.openPostStats = openPostStats self.openStory = openStory self.contextAction = contextAction self.copyBoostLink = copyBoostLink self.shareBoostLink = shareBoostLink self.openBoost = openBoost self.expandBoosters = expandBoosters self.openGifts = openGifts self.createPrepaidGiveaway = createPrepaidGiveaway self.updateGiftsSelected = updateGiftsSelected self.updateStarsSelected = updateStarsSelected self.requestTonWithdraw = requestTonWithdraw self.requestStarsWithdraw = requestStarsWithdraw self.showTimeoutTooltip = showTimeoutTooltip self.buyAds = buyAds self.openMonetizationIntro = openMonetizationIntro self.openMonetizationInfo = openMonetizationInfo self.openTonTransaction = openTonTransaction self.openStarsTransaction = openStarsTransaction self.expandTransactions = expandTransactions self.updateCpmEnabled = updateCpmEnabled self.presentCpmLocked = presentCpmLocked self.dismissInput = dismissInput } } private enum StatsSection: Int32 { case overview case growth case followers case notifications case viewsByHour case viewsBySource case followersBySource case languages case postInteractions case instantPageInteractions case reactionsByEmotion case storyInteractions case storyReactionsByEmotion case recentPosts case boostLevel case boostOverview case boostPrepaid case boosters case boostLink case boostGifts case adsHeader case adsImpressions case adsTonRevenue case adsStarsRevenue case adsProceeds case adsTonBalance case adsStarsBalance case adsTransactions case adsCpm } enum StatsPostItem: Equatable { static func == (lhs: StatsPostItem, rhs: StatsPostItem) -> Bool { switch lhs { case let .message(lhsMessage): if case let .message(rhsMessage) = rhs { return lhsMessage.id == rhsMessage.id } else { return false } case let .story(lhsPeer, lhsStory): if case let .story(rhsPeer, rhsStory) = rhs, lhsPeer == rhsPeer, lhsStory == rhsStory { return true } else { return false } } } case message(Message) case story(EnginePeer, EngineStoryItem) var isStory: Bool { if case .story = self { return true } else { return false } } var timestamp: Int32 { switch self { case let .message(message): return message.timestamp case let .story(_, story): return story.timestamp } } } private enum StatsEntry: ItemListNodeEntry { case overviewTitle(PresentationTheme, String, String) case overview(PresentationTheme, ChannelStats) case growthTitle(PresentationTheme, String) case growthGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType) case followersTitle(PresentationTheme, String) case followersGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType) case notificationsTitle(PresentationTheme, String) case notificationsGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType) case viewsByHourTitle(PresentationTheme, String) case viewsByHourGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType) case viewsBySourceTitle(PresentationTheme, String) case viewsBySourceGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType) case followersBySourceTitle(PresentationTheme, String) case followersBySourceGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType) case languagesTitle(PresentationTheme, String) case languagesGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType) case postInteractionsTitle(PresentationTheme, String) case postInteractionsGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType) case reactionsByEmotionTitle(PresentationTheme, String) case reactionsByEmotionGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType) 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, Peer, StatsPostItem, ChannelStatsPostInteractions) case boostLevel(PresentationTheme, Int32, Int32, CGFloat) case boostOverviewTitle(PresentationTheme, String) case boostOverview(PresentationTheme, ChannelBoostStatus, Bool) case boostPrepaidTitle(PresentationTheme, String) case boostPrepaid(Int32, PresentationTheme, String, String, PrepaidGiveaway) case boostPrepaidInfo(PresentationTheme, String) case boostersTitle(PresentationTheme, String) case boostersPlaceholder(PresentationTheme, String) case boosterTabs(PresentationTheme, String, String, Bool) case booster(Int32, PresentationTheme, PresentationDateTimeFormat, ChannelBoostersContext.State.Boost) case boostersExpand(PresentationTheme, String) case boostersInfo(PresentationTheme, String) case boostLinkTitle(PresentationTheme, String) case boostLink(PresentationTheme, String) case boostLinkInfo(PresentationTheme, String) case boostGifts(PresentationTheme, String) case boostGiftsInfo(PresentationTheme, String) case adsHeader(PresentationTheme, String) case adsImpressionsTitle(PresentationTheme, String) case adsImpressionsGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType) case adsTonRevenueTitle(PresentationTheme, String) case adsTonRevenueGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType, Double) case adsStarsRevenueTitle(PresentationTheme, String) case adsStarsRevenueGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType, Double) case adsProceedsTitle(PresentationTheme, String) case adsProceedsOverview(PresentationTheme, RevenueStats?, StarsRevenueStats?) case adsProceedsInfo(PresentationTheme, String) case adsTonBalanceTitle(PresentationTheme, String) case adsTonBalance(PresentationTheme, RevenueStats, Bool, Bool) case adsTonBalanceInfo(PresentationTheme, String) case adsStarsBalanceTitle(PresentationTheme, String) case adsStarsBalance(PresentationTheme, StarsRevenueStats, Bool, Bool, Int32?) case adsStarsBalanceInfo(PresentationTheme, String) case adsTransactionsTitle(PresentationTheme, String) case adsTransactionsTabs(PresentationTheme, String, String, Bool) case adsTransaction(Int32, PresentationTheme, RevenueStatsTransactionsContext.State.Transaction) case adsStarsTransaction(Int32, PresentationTheme, StarsContext.State.Transaction) case adsTransactionsExpand(PresentationTheme, String, Bool) case adsCpmToggle(PresentationTheme, String, Int32, Bool?) case adsCpmInfo(PresentationTheme, String) var section: ItemListSectionId { switch self { case .overviewTitle, .overview: return StatsSection.overview.rawValue case .growthTitle, .growthGraph: return StatsSection.growth.rawValue case .followersTitle, .followersGraph: return StatsSection.followers.rawValue case .notificationsTitle, .notificationsGraph: return StatsSection.notifications.rawValue case .viewsByHourTitle, .viewsByHourGraph: return StatsSection.viewsByHour.rawValue case .viewsBySourceTitle, .viewsBySourceGraph: return StatsSection.viewsBySource.rawValue case .followersBySourceTitle, .followersBySourceGraph: return StatsSection.followersBySource.rawValue case .languagesTitle, .languagesGraph: return StatsSection.languages.rawValue case .postInteractionsTitle, .postInteractionsGraph: return StatsSection.postInteractions.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: return StatsSection.boostOverview.rawValue case .boostPrepaidTitle, .boostPrepaid, .boostPrepaidInfo: return StatsSection.boostPrepaid.rawValue case .boostersTitle, .boostersPlaceholder, .boosterTabs, .booster, .boostersExpand, .boostersInfo: return StatsSection.boosters.rawValue case .boostLinkTitle, .boostLink, .boostLinkInfo: return StatsSection.boostLink.rawValue case .boostGifts, .boostGiftsInfo: return StatsSection.boostGifts.rawValue case .adsHeader: return StatsSection.adsHeader.rawValue case .adsImpressionsTitle, .adsImpressionsGraph: return StatsSection.adsImpressions.rawValue case .adsTonRevenueTitle, .adsTonRevenueGraph: return StatsSection.adsTonRevenue.rawValue case .adsStarsRevenueTitle, .adsStarsRevenueGraph: return StatsSection.adsStarsRevenue.rawValue case .adsProceedsTitle, .adsProceedsOverview, .adsProceedsInfo: return StatsSection.adsProceeds.rawValue case .adsTonBalanceTitle, .adsTonBalance, .adsTonBalanceInfo: return StatsSection.adsTonBalance.rawValue case .adsStarsBalanceTitle, .adsStarsBalance, .adsStarsBalanceInfo: return StatsSection.adsStarsBalance.rawValue case .adsTransactionsTitle, .adsTransactionsTabs, .adsTransaction, .adsStarsTransaction, .adsTransactionsExpand: return StatsSection.adsTransactions.rawValue case .adsCpmToggle, .adsCpmInfo: return StatsSection.adsCpm.rawValue } } var stableId: Int32 { switch self { case .overviewTitle: return 0 case .overview: return 1 case .growthTitle: return 2 case .growthGraph: return 3 case .followersTitle: return 4 case .followersGraph: return 5 case .notificationsTitle: return 6 case .notificationsGraph: return 7 case .viewsByHourTitle: return 8 case .viewsByHourGraph: return 9 case .viewsBySourceTitle: return 10 case .viewsBySourceGraph: return 11 case .followersBySourceTitle: return 12 case .followersBySourceGraph: return 13 case .languagesTitle: return 14 case .languagesGraph: return 15 case .postInteractionsTitle: return 16 case .postInteractionsGraph: return 17 case .instantPageInteractionsTitle: 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 27 + index case .boostLevel: return 2000 case .boostOverviewTitle: return 2001 case .boostOverview: return 2002 case .boostPrepaidTitle: return 2003 case let .boostPrepaid(index, _, _, _, _): return 2004 + index case .boostPrepaidInfo: return 2100 case .boostersTitle: return 2101 case .boostersPlaceholder: return 2102 case .boosterTabs: return 2103 case let .booster(index, _, _, _): return 2104 + index case .boostersExpand: return 10000 case .boostersInfo: return 10001 case .boostLinkTitle: return 10002 case .boostLink: return 10003 case .boostLinkInfo: return 10004 case .boostGifts: return 10005 case .boostGiftsInfo: return 10006 case .adsHeader: return 20000 case .adsImpressionsTitle: return 20001 case .adsImpressionsGraph: return 20002 case .adsTonRevenueTitle: return 20003 case .adsTonRevenueGraph: return 20004 case .adsStarsRevenueTitle: return 20005 case .adsStarsRevenueGraph: return 20006 case .adsProceedsTitle: return 20007 case .adsProceedsOverview: return 20008 case .adsProceedsInfo: return 20009 case .adsTonBalanceTitle: return 20010 case .adsTonBalance: return 20011 case .adsTonBalanceInfo: return 20012 case .adsStarsBalanceTitle: return 20013 case .adsStarsBalance: return 20014 case .adsStarsBalanceInfo: return 20015 case .adsTransactionsTitle: return 20016 case .adsTransactionsTabs: return 20017 case let .adsTransaction(index, _, _): return 20018 + index case let .adsStarsTransaction(index, _, _): return 30017 + index case .adsTransactionsExpand: return 40000 case .adsCpmToggle: return 40001 case .adsCpmInfo: return 40002 } } 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 .followersTitle(lhsTheme, lhsText): if case let .followersTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .followersGraph(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsGraph, lhsType): if case let .followersGraph(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsGraph, rhsType) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsGraph == rhsGraph, lhsType == rhsType { return true } else { return false } case let .notificationsTitle(lhsTheme, lhsText): if case let .notificationsTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .notificationsGraph(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsGraph, lhsType): if case let .notificationsGraph(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsGraph, rhsType) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsGraph == rhsGraph, lhsType == rhsType { return true } else { return false } case let .viewsByHourTitle(lhsTheme, lhsText): if case let .viewsByHourTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .viewsByHourGraph(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsGraph, lhsType): if case let .viewsByHourGraph(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsGraph, rhsType) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsGraph == rhsGraph, lhsType == rhsType { return true } else { return false } case let .viewsBySourceTitle(lhsTheme, lhsText): if case let .viewsBySourceTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .viewsBySourceGraph(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsGraph, lhsType): if case let .viewsBySourceGraph(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsGraph, rhsType) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsGraph == rhsGraph, lhsType == rhsType { return true } else { return false } case let .followersBySourceTitle(lhsTheme, lhsText): if case let .followersBySourceTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .followersBySourceGraph(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsGraph, lhsType): if case let .followersBySourceGraph(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 .postInteractionsTitle(lhsTheme, lhsText): if case let .postInteractionsTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .postInteractionsGraph(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsGraph, lhsType): if case let .postInteractionsGraph(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsGraph, rhsType) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsGraph == rhsGraph, lhsType == rhsType { return true } else { return false } case let .postsTitle(lhsTheme, lhsText): if case let .postsTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .instantPageInteractionsTitle(lhsTheme, lhsText): if case let .instantPageInteractionsTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .instantPageInteractionsGraph(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsGraph, lhsType): if case let .instantPageInteractionsGraph(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsGraph, rhsType) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsGraph == rhsGraph, lhsType == rhsType { return true } 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, lhsPeer, lhsPost, lhsInteractions): if case let .post(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsPeer, rhsPost, rhsInteractions) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, arePeersEqual(lhsPeer, rhsPeer), lhsPost == rhsPost, 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 } else { return false } case let .boostOverviewTitle(lhsTheme, lhsText): if case let .boostOverviewTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .boostOverview(lhsTheme, lhsStats, lhsIsGroup): if case let .boostOverview(rhsTheme, rhsStats, rhsIsGroup) = rhs, lhsTheme === rhsTheme, lhsStats == rhsStats, lhsIsGroup == rhsIsGroup { return true } else { return false } case let .boostPrepaidTitle(lhsTheme, lhsText): if case let .boostPrepaidTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .boostPrepaid(lhsIndex, lhsTheme, lhsTitle, lhsSubtitle, lhsPrepaidGiveaway): if case let .boostPrepaid(rhsIndex, rhsTheme, rhsTitle, rhsSubtitle, rhsPrepaidGiveaway) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsPrepaidGiveaway == rhsPrepaidGiveaway { return true } else { return false } case let .boostPrepaidInfo(lhsTheme, lhsText): if case let .boostPrepaidInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .boostersTitle(lhsTheme, lhsText): if case let .boostersTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .boostersPlaceholder(lhsTheme, lhsText): if case let .boostersPlaceholder(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .boosterTabs(lhsTheme, lhsBoostText, lhsGiftText, lhsGiftSelected): if case let .boosterTabs(rhsTheme, rhsBoostText, rhsGiftText, rhsGiftSelected) = rhs, lhsTheme === rhsTheme, lhsBoostText == rhsBoostText, lhsGiftText == rhsGiftText, lhsGiftSelected == rhsGiftSelected { return true } else { return false } case let .booster(lhsIndex, lhsTheme, lhsDateTimeFormat, lhsBoost): if case let .booster(rhsIndex, rhsTheme, rhsDateTimeFormat, rhsBoost) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsBoost == rhsBoost { return true } else { return false } case let .boostersExpand(lhsTheme, lhsText): if case let .boostersExpand(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .boostersInfo(lhsTheme, lhsText): if case let .boostersInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .boostLinkTitle(lhsTheme, lhsText): if case let .boostLinkTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .boostLink(lhsTheme, lhsText): if case let .boostLink(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .boostLinkInfo(lhsTheme, lhsText): if case let .boostLinkInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .boostGifts(lhsTheme, lhsText): if case let .boostGifts(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .boostGiftsInfo(lhsTheme, lhsText): if case let .boostGiftsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .adsHeader(lhsTheme, lhsText): if case let .adsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .adsImpressionsTitle(lhsTheme, lhsText): if case let .adsImpressionsTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .adsImpressionsGraph(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsGraph, lhsType): if case let .adsImpressionsGraph(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsGraph, rhsType) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsGraph == rhsGraph, lhsType == rhsType { return true } else { return false } case let .adsTonRevenueTitle(lhsTheme, lhsText): if case let .adsTonRevenueTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .adsTonRevenueGraph(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsGraph, lhsType, lhsRate): if case let .adsTonRevenueGraph(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsGraph, rhsType, rhsRate) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsGraph == rhsGraph, lhsType == rhsType, lhsRate == rhsRate { return true } else { return false } case let .adsStarsRevenueTitle(lhsTheme, lhsText): if case let .adsStarsRevenueTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .adsStarsRevenueGraph(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsGraph, lhsType, lhsRate): if case let .adsStarsRevenueGraph(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsGraph, rhsType, rhsRate) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsGraph == rhsGraph, lhsType == rhsType, lhsRate == rhsRate { return true } else { return false } case let .adsProceedsTitle(lhsTheme, lhsText): if case let .adsProceedsTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .adsProceedsOverview(lhsTheme, lhsStatus, lhsStarsStatus): if case let .adsProceedsOverview(rhsTheme, rhsStatus, rhsStarsStatus) = rhs, lhsTheme === rhsTheme, lhsStatus == rhsStatus, lhsStarsStatus == rhsStarsStatus { return true } else { return false } case let .adsProceedsInfo(lhsTheme, lhsText): if case let .adsProceedsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .adsTonBalanceTitle(lhsTheme, lhsText): if case let .adsTonBalanceTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .adsTonBalance(lhsTheme, lhsStats, lhsCanWithdraw, lhsIsEnabled): if case let .adsTonBalance(rhsTheme, rhsStats, rhsCanWithdraw, rhsIsEnabled) = rhs, lhsTheme === rhsTheme, lhsStats == rhsStats, lhsCanWithdraw == rhsCanWithdraw, lhsIsEnabled == rhsIsEnabled { return true } else { return false } case let .adsTonBalanceInfo(lhsTheme, lhsText): if case let .adsTonBalanceInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .adsStarsBalanceTitle(lhsTheme, lhsText): if case let .adsStarsBalanceTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .adsStarsBalance(lhsTheme, lhsStats, lhsCanWithdraw, lhsIsEnabled, lhsCooldownUntilTimestamp): if case let .adsStarsBalance(rhsTheme, rhsStats, rhsCanWithdraw, rhsIsEnabled, rhsCooldownUntilTimestamp) = rhs, lhsTheme === rhsTheme, lhsStats == rhsStats, lhsCanWithdraw == rhsCanWithdraw, lhsIsEnabled == rhsIsEnabled, lhsCooldownUntilTimestamp == rhsCooldownUntilTimestamp { return true } else { return false } case let .adsStarsBalanceInfo(lhsTheme, lhsText): if case let .adsStarsBalanceInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .adsTransactionsTitle(lhsTheme, lhsText): if case let .adsTransactionsTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .adsTransactionsTabs(lhsTheme, lhsTonText, lhsStarsText, lhsStarsSelected): if case let .adsTransactionsTabs(rhsTheme, rhsTonText, rhsStarsText, rhsStarsSelected) = rhs, lhsTheme === rhsTheme, lhsTonText == rhsTonText, lhsStarsText == rhsStarsText, lhsStarsSelected == rhsStarsSelected { return true } else { return false } case let .adsTransaction(lhsIndex, lhsTheme, lhsTransaction): if case let .adsTransaction(rhsIndex, rhsTheme, rhsTransaction) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsTransaction == rhsTransaction { return true } else { return false } case let .adsStarsTransaction(lhsIndex, lhsTheme, lhsTransaction): if case let .adsStarsTransaction(rhsIndex, rhsTheme, rhsTransaction) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsTransaction == rhsTransaction { return true } else { return false } case let .adsTransactionsExpand(lhsTheme, lhsText, lhsStars): if case let .adsTransactionsExpand(rhsTheme, rhsText, rhsStars) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsStars == rhsStars { return true } else { return false } case let .adsCpmToggle(lhsTheme, lhsText, lhsMinLevel, lhsValue): if case let .adsCpmToggle(rhsTheme, rhsText, rhsMinLevel, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsMinLevel == rhsMinLevel, lhsValue == rhsValue { return true } else { return false } case let .adsCpmInfo(lhsTheme, lhsText): if case let .adsCpmInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { 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! ChannelStatsControllerArguments 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 .followersTitle(_, text), let .notificationsTitle(_, text), let .viewsByHourTitle(_, text), let .viewsBySourceTitle(_, text), let .followersBySourceTitle(_, text), let .languagesTitle(_, text), let .postInteractionsTitle(_, 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), let .boostLinkTitle(_, text), let .adsImpressionsTitle(_, text), let .adsTonRevenueTitle(_, text), let .adsStarsRevenueTitle(_, text), let .adsProceedsTitle(_, text), let .adsTonBalanceTitle(_, text), let .adsStarsBalanceTitle(_, text), let .adsTransactionsTitle(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .boostPrepaidInfo(_, text), let .boostersInfo(_, text), let .boostLinkInfo(_, text), let .boostGiftsInfo(_, text), let .adsCpmInfo(_, text), let .adsProceedsInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) case let .overview(_, stats): return StatsOverviewItem(context: arguments.context, presentationData: presentationData, isGroup: false, stats: stats, sectionId: self.section, style: .blocks) case let .growthGraph(_, _, _, graph, type), let .followersGraph(_, _, _, graph, type), let .notificationsGraph(_, _, _, graph, type), let .viewsByHourGraph(_, _, _, graph, type), let .viewsBySourceGraph(_, _, _, graph, type), let .followersBySourceGraph(_, _, _, graph, type), let .languagesGraph(_, _, _, graph, type), let .reactionsByEmotionGraph(_, _, _, graph, type), let .storyReactionsByEmotionGraph(_, _, _, graph, type), let .adsImpressionsGraph(_, _, _, graph, type): return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, sectionId: self.section, style: .blocks) case let .adsTonRevenueGraph(_, _, _, graph, type, rate): return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, conversionRate: rate, sectionId: self.section, style: .blocks) case let .adsStarsRevenueGraph(_, _, _, graph, type, rate): return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, conversionRate: rate, sectionId: self.section, style: .blocks) case let .postInteractionsGraph(_, _, _, 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 { completion(data) } }) }, sectionId: self.section, style: .blocks) case let .post(_, _, _, _, peer, post, interactions): return StatsMessageItem(context: arguments.context, presentationData: presentationData, peer: peer, item: post, views: interactions.views, reactions: interactions.reactions, forwards: interactions.forwards, sectionId: self.section, style: .blocks, action: { arguments.openPostStats(EnginePeer(peer), post) }, openStory: { sourceView in if case let .story(_, story) = post { arguments.openStory(story, sourceView) } }, contextAction: !post.isStory ? { node, gesture in if case let .message(message) = post { arguments.contextAction(message.id, node, gesture) } } : nil) case let .boosterTabs(_, boostText, giftText, giftSelected): return BoostsTabsItem(theme: presentationData.theme, boostsText: boostText, giftsText: giftText, selectedTab: giftSelected ? .gifts : .boosts, sectionId: self.section, selectionUpdated: { tab in arguments.updateGiftsSelected(tab == .gifts) }) case let .booster(_, _, _, boost): let count = boost.multiplier let expiresValue = stringForDate(timestamp: boost.expires, strings: presentationData.strings) var expiresString: String let durationMonths = Int32(round(Float(boost.expires - boost.date) / (86400.0 * 30.0))) let durationString = presentationData.strings.Stats_Boosts_ShortMonth("\(durationMonths)").string let title: String let icon: GiftOptionItem.Icon var label: String? if boost.flags.contains(.isGiveaway) { label = "🏆 \(presentationData.strings.Stats_Boosts_Giveaway)" } else if boost.flags.contains(.isGift) { label = "🎁 \(presentationData.strings.Stats_Boosts_Gift)" } let color: GiftOptionItem.Icon.Color if durationMonths > 11 { color = .red } else if durationMonths > 5 { color = .blue } else { color = .green } if boost.flags.contains(.isUnclaimed) { title = presentationData.strings.Stats_Boosts_Unclaimed icon = .image(color: color, name: "Premium/Unclaimed") expiresString = "\(durationString) • \(expiresValue)" } else if let peer = boost.peer { title = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) icon = .peer(peer) if let _ = label { expiresString = "\(durationString) • \(expiresValue)" } else { expiresString = presentationData.strings.Stats_Boosts_ExpiresOn(expiresValue).string } } else { expiresString = "\(durationString) • \(expiresValue)" if boost.flags.contains(.isUnclaimed) { title = presentationData.strings.Stats_Boosts_Unclaimed icon = .image(color: color, name: "Premium/Unclaimed") } else if boost.flags.contains(.isGiveaway) { if let stars = boost.stars { title = presentationData.strings.Stats_Boosts_Stars(Int32(stars)) icon = .image(color: .stars, name: "Premium/PremiumStar") expiresString = expiresValue } else { title = presentationData.strings.Stats_Boosts_ToBeDistributed icon = .image(color: color, name: "Premium/ToBeDistributed") } } else { title = "Unknown" icon = .image(color: color, name: "Premium/ToBeDistributed") } } return GiftOptionItem(presentationData: presentationData, context: arguments.context, icon: icon, title: title, titleFont: .bold, titleBadge: count > 1 ? "\(count)" : nil, subtitle: expiresString, label: label.flatMap { .semitransparent($0) }, sectionId: self.section, action: { arguments.openBoost(boost) }) case let .boostersExpand(theme, title): return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.downArrowImage(theme), title: title, sectionId: self.section, editing: false, action: { arguments.expandBoosters() }) case let .boostLevel(_, count, level, position): let inactiveText = presentationData.strings.ChannelBoost_Level("\(level)").string let activeText = presentationData.strings.ChannelBoost_Level("\(level + 1)").string return BoostLevelHeaderItem(theme: presentationData.theme, count: count, position: position, activeText: activeText, inactiveText: inactiveText, sectionId: self.section) case let .boostOverview(_, stats, isGroup): return StatsOverviewItem(context: arguments.context, presentationData: presentationData, isGroup: isGroup, stats: stats, sectionId: self.section, style: .blocks) case let .boostLink(_, link): let invite: ExportedInvitation = .link(link: link, title: nil, isPermanent: false, requestApproval: false, isRevoked: false, adminId: PeerId(0), date: 0, startDate: nil, expireDate: nil, usageLimit: nil, count: nil, requestedCount: nil, pricing: nil) return ItemListPermanentInviteLinkItem(context: arguments.context, presentationData: presentationData, invite: invite, count: 0, peers: [], displayButton: true, displayImporters: false, buttonColor: nil, sectionId: self.section, style: .blocks, copyAction: { arguments.copyBoostLink(link) }, shareAction: { arguments.shareBoostLink(link) }, contextAction: nil, viewAction: nil, tag: nil) case let .boostersPlaceholder(_, text): return ItemListPlaceholderItem(theme: presentationData.theme, text: text, sectionId: self.section, style: .blocks) case let .boostGifts(theme, title): return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.addBoostsIcon(theme), title: title, sectionId: self.section, editing: false, action: { arguments.openGifts() }) case let .boostPrepaid(_, _, title, subtitle, prepaidGiveaway): let color: GiftOptionItem.Icon.Color let icon: String var boosts: Int32 switch prepaidGiveaway.prize { case let .premium(months): switch months { case 3: color = .green case 6: color = .blue case 12: color = .red default: color = .blue } icon = "Premium/Giveaway" boosts = prepaidGiveaway.quantity * 4 case let .stars(_, boostCount): color = .stars icon = "Premium/PremiumStar" boosts = boostCount } return GiftOptionItem(presentationData: presentationData, context: arguments.context, icon: .image(color: color, name: icon), title: title, titleFont: .bold, titleBadge: "\(boosts)", subtitle: subtitle, label: nil, sectionId: self.section, action: { arguments.createPrepaidGiveaway(prepaidGiveaway) }) case let .adsHeader(_, text): return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: { _ in arguments.openMonetizationIntro() }) case let .adsProceedsOverview(_, stats, starsStats): return StatsOverviewItem(context: arguments.context, presentationData: presentationData, isGroup: false, stats: stats ?? starsStats, additionalStats: stats != nil ? starsStats : nil, sectionId: self.section, style: .blocks) case let .adsTonBalance(_, stats, canWithdraw, isEnabled): return MonetizationBalanceItem( context: arguments.context, presentationData: presentationData, stats: stats, canWithdraw: canWithdraw, isEnabled: isEnabled, actionCooldownUntilTimestamp: nil, withdrawAction: { arguments.requestTonWithdraw() }, buyAdsAction: nil, sectionId: self.section, style: .blocks ) case let .adsTonBalanceInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: { _ in arguments.openMonetizationInfo() }) case let .adsStarsBalance(_, stats, canWithdraw, isEnabled, cooldownUntilTimestamp): return MonetizationBalanceItem( context: arguments.context, presentationData: presentationData, stats: stats, canWithdraw: canWithdraw, isEnabled: isEnabled, actionCooldownUntilTimestamp: cooldownUntilTimestamp, withdrawAction: { var remainingCooldownSeconds: Int32 = 0 if let cooldownUntilTimestamp { remainingCooldownSeconds = cooldownUntilTimestamp - Int32(Date().timeIntervalSince1970) remainingCooldownSeconds = max(0, remainingCooldownSeconds) if remainingCooldownSeconds > 0 { arguments.showTimeoutTooltip(cooldownUntilTimestamp) } else { arguments.requestStarsWithdraw() } } else { arguments.requestStarsWithdraw() } }, buyAdsAction: canWithdraw ? { arguments.buyAds() } : nil, sectionId: self.section, style: .blocks ) case let .adsStarsBalanceInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: { _ in arguments.openMonetizationInfo() }) case let .adsTransactionsTabs(_, tonText, starsText, starsSelected): return BoostsTabsItem(theme: presentationData.theme, boostsText: tonText, giftsText: starsText, selectedTab: starsSelected ? .gifts : .boosts, sectionId: self.section, selectionUpdated: { tab in arguments.updateStarsSelected(tab == .gifts) }) case let .adsTransaction(_, theme, transaction): let font = Font.with(size: floor(presentationData.fontSize.itemListBaseFontSize)) let smallLabelFont = Font.with(size: floor(presentationData.fontSize.itemListBaseFontSize / 17.0 * 13.0)) var labelColor = theme.list.itemDisclosureActions.constructive.fillColor let title: NSAttributedString let detailText: String var detailColor: ItemListDisclosureItemDetailLabelColor = .generic switch transaction { case let .proceeds(_, fromDate, toDate): title = NSAttributedString(string: presentationData.strings.Monetization_Transaction_Proceeds, font: font, textColor: theme.list.itemPrimaryTextColor) let fromDateString = stringForMediumCompactDate(timestamp: fromDate, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, withTime: false) let toDateString = stringForMediumCompactDate(timestamp: toDate, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, withTime: false) if fromDateString == toDateString { detailText = stringForMediumCompactDate(timestamp: toDate, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, withTime: true) } else { detailText = "\(fromDateString) – \(toDateString)" } case let .withdrawal(status, _, date, provider, _, _): title = NSAttributedString(string: presentationData.strings.Monetization_Transaction_Withdrawal(provider).string, font: font, textColor: theme.list.itemPrimaryTextColor) labelColor = theme.list.itemDestructiveColor switch status { case .succeed: detailText = stringForMediumCompactDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat) case .failed: detailText = stringForMediumCompactDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, withTime: false) + " – \(presentationData.strings.Monetization_Transaction_Failed)" detailColor = .destructive case .pending: detailText = presentationData.strings.Monetization_Transaction_Pending } case let .refund(_, date, _): title = NSAttributedString(string: presentationData.strings.Monetization_Transaction_Refund, font: font, textColor: theme.list.itemPrimaryTextColor) detailText = stringForMediumCompactDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat) } let label = tonAmountAttributedString(formatTonAmountText(transaction.amount, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator, showPlus: true), integralFont: font, fractionalFont: smallLabelFont, color: labelColor).mutableCopy() as! NSMutableAttributedString label.insert(NSAttributedString(string: " $ ", font: font, textColor: labelColor), at: 1) if let range = label.string.range(of: "$"), let icon = generateTintedImage(image: UIImage(bundleImageName: "Ads/TonMedium"), color: labelColor) { label.addAttribute(.attachment, value: icon, range: NSRange(range, in: label.string)) label.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: label.string)) } return ItemListDisclosureItem(presentationData: presentationData, title: "", attributedTitle: title, label: "", attributedLabel: label, labelStyle: .coloredText(labelColor), additionalDetailLabel: detailText, additionalDetailLabelColor: detailColor, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: { arguments.openTonTransaction(transaction) }) case let .adsStarsTransaction(_, _, transaction): return StarsTransactionItem(context: arguments.context, presentationData: presentationData, transaction: transaction, action: { arguments.openStarsTransaction(transaction) }, sectionId: self.section, style: .blocks) case let .adsTransactionsExpand(theme, title, stars): return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.downArrowImage(theme), title: title, sectionId: self.section, editing: false, action: { arguments.expandTransactions(stars) }) case let .adsCpmToggle(_, title, minLevel, value): var badgeComponent: AnyComponent? if value == nil { badgeComponent = AnyComponent(BoostLevelIconComponent( strings: presentationData.strings, level: Int(minLevel) )) } return ItemListSwitchItem(presentationData: presentationData, title: title, titleBadgeComponent: badgeComponent, value: value == true, enableInteractiveChanges: value != nil, enabled: true, displayLocked: value == nil, sectionId: self.section, style: .blocks, updated: { updatedValue in if value != nil { arguments.updateCpmEnabled(updatedValue) } else { arguments.presentCpmLocked() } }, activatedWhileDisabled: { arguments.presentCpmLocked() }) } } } public enum ChannelStatsSection { case stats case boosts case monetization } private struct ChannelStatsControllerState: Equatable { let section: ChannelStatsSection let boostersExpanded: Bool let moreBoostersDisplayed: Int32 let giftsSelected: Bool let starsSelected: Bool let transactionsExpanded: Bool let moreTransactionsDisplayed: Int32 init() { self.section = .stats self.boostersExpanded = false self.moreBoostersDisplayed = 0 self.giftsSelected = false self.starsSelected = false self.transactionsExpanded = false self.moreTransactionsDisplayed = 0 } init(section: ChannelStatsSection, boostersExpanded: Bool, moreBoostersDisplayed: Int32, giftsSelected: Bool, starsSelected: Bool, transactionsExpanded: Bool, moreTransactionsDisplayed: Int32) { self.section = section self.boostersExpanded = boostersExpanded self.moreBoostersDisplayed = moreBoostersDisplayed self.giftsSelected = giftsSelected self.starsSelected = starsSelected self.transactionsExpanded = transactionsExpanded self.moreTransactionsDisplayed = moreTransactionsDisplayed } static func ==(lhs: ChannelStatsControllerState, rhs: ChannelStatsControllerState) -> Bool { if lhs.section != rhs.section { return false } if lhs.boostersExpanded != rhs.boostersExpanded { return false } if lhs.moreBoostersDisplayed != rhs.moreBoostersDisplayed { return false } if lhs.giftsSelected != rhs.giftsSelected { return false } if lhs.starsSelected != rhs.starsSelected { return false } if lhs.transactionsExpanded != rhs.transactionsExpanded { return false } if lhs.moreTransactionsDisplayed != rhs.moreTransactionsDisplayed { return false } return true } func withUpdatedSection(_ section: ChannelStatsSection) -> ChannelStatsControllerState { return ChannelStatsControllerState(section: section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: self.giftsSelected, starsSelected: self.starsSelected, transactionsExpanded: self.transactionsExpanded, moreTransactionsDisplayed: self.moreTransactionsDisplayed) } func withUpdatedBoostersExpanded(_ boostersExpanded: Bool) -> ChannelStatsControllerState { return ChannelStatsControllerState(section: self.section, boostersExpanded: boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: self.giftsSelected, starsSelected: self.starsSelected, transactionsExpanded: self.transactionsExpanded, moreTransactionsDisplayed: self.moreTransactionsDisplayed) } func withUpdatedMoreBoostersDisplayed(_ moreBoostersDisplayed: Int32) -> ChannelStatsControllerState { return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: moreBoostersDisplayed, giftsSelected: self.giftsSelected, starsSelected: self.starsSelected, transactionsExpanded: self.transactionsExpanded, moreTransactionsDisplayed: self.moreTransactionsDisplayed) } func withUpdatedGiftsSelected(_ giftsSelected: Bool) -> ChannelStatsControllerState { return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: giftsSelected, starsSelected: self.starsSelected, transactionsExpanded: self.transactionsExpanded, moreTransactionsDisplayed: self.moreTransactionsDisplayed) } func withUpdatedStarsSelected(_ starsSelected: Bool) -> ChannelStatsControllerState { return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: self.giftsSelected, starsSelected: starsSelected, transactionsExpanded: self.transactionsExpanded, moreTransactionsDisplayed: self.moreTransactionsDisplayed) } func withUpdatedTransactionsExpanded(_ transactionsExpanded: Bool) -> ChannelStatsControllerState { return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: self.giftsSelected, starsSelected: self.starsSelected, transactionsExpanded: transactionsExpanded, moreTransactionsDisplayed: self.moreTransactionsDisplayed) } func withUpdatedMoreTransactionsDisplayed(_ moreTransactionsDisplayed: Int32) -> ChannelStatsControllerState { return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: self.giftsSelected, starsSelected: self.starsSelected, transactionsExpanded: self.transactionsExpanded, moreTransactionsDisplayed: moreTransactionsDisplayed) } } private func statsEntries( presentationData: PresentationData, data: ChannelStats, peer: EnginePeer?, messages: [Message]?, stories: StoryListContext.State?, interactions: [ChannelStatsPostInteractions.PostId: ChannelStatsPostInteractions]? ) -> [StatsEntry] { var entries: [StatsEntry] = [] let minDate = stringForDate(timestamp: data.period.minDate, strings: presentationData.strings) let maxDate = stringForDate(timestamp: data.period.maxDate, strings: presentationData.strings) entries.append(.overviewTitle(presentationData.theme, presentationData.strings.Stats_Overview, "\(minDate) – \(maxDate)")) entries.append(.overview(presentationData.theme, data)) if !data.growthGraph.isEmpty { entries.append(.growthTitle(presentationData.theme, presentationData.strings.Stats_GrowthTitle)) entries.append(.growthGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.growthGraph, .lines)) } if !data.followersGraph.isEmpty { entries.append(.followersTitle(presentationData.theme, presentationData.strings.Stats_FollowersTitle)) entries.append(.followersGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.followersGraph, .lines)) } if !data.muteGraph.isEmpty { entries.append(.notificationsTitle(presentationData.theme, presentationData.strings.Stats_NotificationsTitle)) entries.append(.notificationsGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.muteGraph, .lines)) } if !data.topHoursGraph.isEmpty { entries.append(.viewsByHourTitle(presentationData.theme, presentationData.strings.Stats_ViewsByHoursTitle)) entries.append(.viewsByHourGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.topHoursGraph, .hourlyStep)) } if !data.viewsBySourceGraph.isEmpty { entries.append(.viewsBySourceTitle(presentationData.theme, presentationData.strings.Stats_ViewsBySourceTitle)) entries.append(.viewsBySourceGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.viewsBySourceGraph, .bars)) } if !data.newFollowersBySourceGraph.isEmpty { entries.append(.followersBySourceTitle(presentationData.theme, presentationData.strings.Stats_FollowersBySourceTitle)) entries.append(.followersBySourceGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.newFollowersBySourceGraph, .bars)) } if !data.languagesGraph.isEmpty { entries.append(.languagesTitle(presentationData.theme, presentationData.strings.Stats_LanguagesTitle)) entries.append(.languagesGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.languagesGraph, .pie)) } if !data.interactionsGraph.isEmpty { entries.append(.postInteractionsTitle(presentationData.theme, presentationData.strings.Stats_InteractionsTitle)) entries.append(.postInteractionsGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.interactionsGraph, .twoAxisStep)) } if !data.instantPageInteractionsGraph.isEmpty { entries.append(.instantPageInteractionsTitle(presentationData.theme, presentationData.strings.Stats_InstantViewInteractionsTitle)) 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 peer, let interactions { var posts: [StatsPostItem] = [] if let messages { for message in messages { if let _ = interactions[.message(id: message.id)] { posts.append(.message(message)) } } } if let stories { for story in stories.items { if let _ = interactions[.story(peerId: peer.id, id: story.storyItem.id)] { posts.append(.story(peer, story.storyItem)) } } } posts.sort(by: { $0.timestamp > $1.timestamp }) if !posts.isEmpty { entries.append(.postsTitle(presentationData.theme, presentationData.strings.Stats_PostsTitle)) var index: Int32 = 0 for post in posts { switch post { case let .message(message): if let interactions = interactions[.message(id: message.id)] { entries.append(.post(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer._asPeer(), post, interactions)) } case let .story(_, story): if let interactions = interactions[.story(peerId: peer.id, id: story.id)] { entries.append(.post(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer._asPeer(), post, interactions)) } } index += 1 } } } return entries } private func boostsEntries( presentationData: PresentationData, state: ChannelStatsControllerState, isGroup: Bool, boostData: ChannelBoostStatus, boostsOnly: Bool, boostersState: ChannelBoostersContext.State?, giftsState: ChannelBoostersContext.State?, giveawayAvailable: Bool ) -> [StatsEntry] { var entries: [StatsEntry] = [] if !boostsOnly { let progress: CGFloat if let nextLevelBoosts = boostData.nextLevelBoosts { progress = CGFloat(boostData.boosts - boostData.currentLevelBoosts) / CGFloat(nextLevelBoosts - boostData.currentLevelBoosts) } else { progress = 1.0 } entries.append(.boostLevel(presentationData.theme, Int32(boostData.boosts), Int32(boostData.level), progress)) } entries.append(.boostOverviewTitle(presentationData.theme, presentationData.strings.Stats_Boosts_OverviewHeader)) entries.append(.boostOverview(presentationData.theme, boostData, isGroup)) if !boostData.prepaidGiveaways.isEmpty { entries.append(.boostPrepaidTitle(presentationData.theme, presentationData.strings.Stats_Boosts_PrepaidGiveawaysTitle)) var i: Int32 = 0 for giveaway in boostData.prepaidGiveaways { let title: String let text: String switch giveaway.prize { case let .premium(months): title = presentationData.strings.Stats_Boosts_PrepaidGiveawayCount(giveaway.quantity) text = presentationData.strings.Stats_Boosts_PrepaidGiveawayMonths("\(months)").string case let .stars(stars, _): title = presentationData.strings.Stats_Boosts_Stars(Int32(stars)) text = presentationData.strings.Stats_Boosts_StarsWinners(giveaway.quantity) } entries.append(.boostPrepaid(i, presentationData.theme, title, text, giveaway)) i += 1 } entries.append(.boostPrepaidInfo(presentationData.theme, presentationData.strings.Stats_Boosts_PrepaidGiveawaysInfo)) } let boostersTitle: String let boostersPlaceholder: String? let boostersFooter: String? if let boostersState, boostersState.count > 0 { boostersTitle = presentationData.strings.Stats_Boosts_Boosts(boostersState.count) boostersPlaceholder = nil boostersFooter = isGroup ? presentationData.strings.Stats_Boosts_Group_BoostersInfo : presentationData.strings.Stats_Boosts_BoostersInfo } else { boostersTitle = presentationData.strings.Stats_Boosts_BoostsNone boostersPlaceholder = isGroup ? presentationData.strings.Stats_Boosts_Group_NoBoostersYet : presentationData.strings.Stats_Boosts_NoBoostersYet boostersFooter = nil } entries.append(.boostersTitle(presentationData.theme, boostersTitle)) if let boostersPlaceholder { entries.append(.boostersPlaceholder(presentationData.theme, boostersPlaceholder)) } var boostsCount: Int32 = 0 if let boostersState { boostsCount = boostersState.count } var giftsCount: Int32 = 0 if let giftsState { giftsCount = giftsState.count } if boostsCount > 0 && giftsCount > 0 && boostsCount != giftsCount { entries.append(.boosterTabs(presentationData.theme, presentationData.strings.Stats_Boosts_TabBoosts(boostsCount), presentationData.strings.Stats_Boosts_TabGifts(giftsCount), state.giftsSelected)) } let selectedState: ChannelBoostersContext.State? if state.giftsSelected { selectedState = giftsState } else { selectedState = boostersState } if let selectedState { var boosterIndex: Int32 = 0 var boosters: [ChannelBoostersContext.State.Boost] = selectedState.boosts var limit: Int32 if state.boostersExpanded { limit = 25 + state.moreBoostersDisplayed } else { limit = initialBoostersDisplayedLimit } boosters = Array(boosters.prefix(Int(limit))) for booster in boosters { entries.append(.booster(boosterIndex, presentationData.theme, presentationData.dateTimeFormat, booster)) boosterIndex += 1 } let totalBoostsCount = boosters.reduce(Int32(0)) { partialResult, boost in return partialResult + boost.multiplier } if totalBoostsCount < selectedState.count { let moreCount: Int32 if !state.boostersExpanded { moreCount = min(80, selectedState.count - totalBoostsCount) } else { moreCount = min(200, selectedState.count - totalBoostsCount) } entries.append(.boostersExpand(presentationData.theme, presentationData.strings.Stats_Boosts_ShowMoreBoosts(moreCount))) } } if let boostersFooter { entries.append(.boostersInfo(presentationData.theme, boostersFooter)) } entries.append(.boostLinkTitle(presentationData.theme, presentationData.strings.Stats_Boosts_LinkHeader)) entries.append(.boostLink(presentationData.theme, boostData.url)) entries.append(.boostLinkInfo(presentationData.theme, isGroup ? presentationData.strings.Stats_Boosts_Group_LinkInfo : presentationData.strings.Stats_Boosts_LinkInfo)) if giveawayAvailable { entries.append(.boostGifts(presentationData.theme, presentationData.strings.Stats_Boosts_GetBoosts)) entries.append(.boostGiftsInfo(presentationData.theme, isGroup ? presentationData.strings.Stats_Boosts_Group_GetBoostsInfo : presentationData.strings.Stats_Boosts_GetBoostsInfo)) } return entries } private func monetizationEntries( presentationData: PresentationData, state: ChannelStatsControllerState, peer: EnginePeer?, data: RevenueStats, boostData: ChannelBoostStatus?, transactionsInfo: RevenueStatsTransactionsContext.State, starsData: StarsRevenueStats?, starsTransactionsInfo: StarsTransactionsContext.State, adsRestricted: Bool, premiumConfiguration: PremiumConfiguration, monetizationConfiguration: MonetizationConfiguration, canViewRevenue: Bool, canViewStarsRevenue: Bool ) -> [StatsEntry] { var entries: [StatsEntry] = [] if canViewRevenue { entries.append(.adsHeader(presentationData.theme, presentationData.strings.Monetization_Header)) if !data.topHoursGraph.isEmpty { entries.append(.adsImpressionsTitle(presentationData.theme, presentationData.strings.Monetization_ImpressionsTitle)) entries.append(.adsImpressionsGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.topHoursGraph, .hourlyStep)) } if !data.revenueGraph.isEmpty { entries.append(.adsTonRevenueTitle(presentationData.theme, presentationData.strings.Monetization_AdRevenueTitle)) entries.append(.adsTonRevenueGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.revenueGraph, .currency, data.usdRate)) } } if canViewStarsRevenue { if let starsData, !starsData.revenueGraph.isEmpty { entries.append(.adsStarsRevenueTitle(presentationData.theme, presentationData.strings.Monetization_StarsRevenueTitle)) entries.append(.adsStarsRevenueGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, starsData.revenueGraph, .stars, starsData.usdRate)) } } entries.append(.adsProceedsTitle(presentationData.theme, presentationData.strings.Monetization_StarsProceeds_Title)) entries.append(.adsProceedsOverview(presentationData.theme, canViewRevenue ? data : nil, canViewStarsRevenue ? starsData : nil)) let hasTonBalance = data.balances.overallRevenue > 0 let hasStarsBalance = (starsData?.balances.overallRevenue ?? 0) > 0 let proceedsInfo: String if (canViewStarsRevenue && hasStarsBalance) && (canViewRevenue && hasTonBalance) { proceedsInfo = presentationData.strings.Monetization_Proceeds_TonAndStars_Info } else if canViewStarsRevenue && hasStarsBalance { proceedsInfo = presentationData.strings.Monetization_Proceeds_Stars_Info } else { proceedsInfo = presentationData.strings.Monetization_Proceeds_Ton_Info } entries.append(.adsProceedsInfo(presentationData.theme, proceedsInfo)) var isCreator = false if let peer, case let .channel(channel) = peer, channel.flags.contains(.isCreator) { isCreator = true } if canViewRevenue { entries.append(.adsTonBalanceTitle(presentationData.theme, presentationData.strings.Monetization_TonBalanceTitle)) entries.append(.adsTonBalance(presentationData.theme, data, isCreator && data.balances.availableBalance > 0, data.balances.withdrawEnabled)) if isCreator { let withdrawalInfoText: String if data.balances.availableBalance == 0 { withdrawalInfoText = presentationData.strings.Monetization_Balance_ZeroInfo } else if monetizationConfiguration.withdrawalAvailable { withdrawalInfoText = presentationData.strings.Monetization_Balance_AvailableInfo } else { withdrawalInfoText = presentationData.strings.Monetization_Balance_ComingLaterInfo } entries.append(.adsTonBalanceInfo(presentationData.theme, withdrawalInfoText)) } } if canViewStarsRevenue { if let starsData, starsData.balances.overallRevenue > 0 { entries.append(.adsStarsBalanceTitle(presentationData.theme, presentationData.strings.Monetization_StarsBalanceTitle)) entries.append(.adsStarsBalance(presentationData.theme, starsData, isCreator && starsData.balances.availableBalance > 0, starsData.balances.withdrawEnabled, starsData.balances.nextWithdrawalTimestamp)) entries.append(.adsStarsBalanceInfo(presentationData.theme, presentationData.strings.Monetization_Balance_StarsInfo)) } } var addedTransactionsTabs = false if !transactionsInfo.transactions.isEmpty && !starsTransactionsInfo.transactions.isEmpty && canViewRevenue && canViewStarsRevenue { addedTransactionsTabs = true entries.append(.adsTransactionsTabs(presentationData.theme, presentationData.strings.Monetization_TonTransactions, presentationData.strings.Monetization_StarsTransactions, state.starsSelected)) } var displayTonTransactions = false if canViewRevenue && !transactionsInfo.transactions.isEmpty && (starsTransactionsInfo.transactions.isEmpty || !state.starsSelected) { displayTonTransactions = true } var displayStarsTransactions = false if canViewStarsRevenue && !starsTransactionsInfo.transactions.isEmpty && (transactionsInfo.transactions.isEmpty || state.starsSelected) { displayStarsTransactions = true } if displayTonTransactions { if !addedTransactionsTabs { entries.append(.adsTransactionsTitle(presentationData.theme, presentationData.strings.Monetization_TonTransactions.uppercased())) } var transactions = transactionsInfo.transactions var limit: Int32 if state.transactionsExpanded { limit = 25 + state.moreTransactionsDisplayed } else { limit = initialTransactionsDisplayedLimit } transactions = Array(transactions.prefix(Int(limit))) var i: Int32 = 0 for transaction in transactions { entries.append(.adsTransaction(i, presentationData.theme, transaction)) i += 1 } if transactions.count < transactionsInfo.count { let moreCount: Int32 if !state.transactionsExpanded { moreCount = min(20, transactionsInfo.count - Int32(transactions.count)) } else { moreCount = min(50, transactionsInfo.count - Int32(transactions.count)) } entries.append(.adsTransactionsExpand(presentationData.theme, presentationData.strings.Monetization_Transaction_ShowMoreTransactions(moreCount), false)) } } if displayStarsTransactions { if !addedTransactionsTabs { entries.append(.adsTransactionsTitle(presentationData.theme, presentationData.strings.Monetization_StarsTransactions.uppercased())) } var transactions = starsTransactionsInfo.transactions var limit: Int32 if state.transactionsExpanded { limit = 25 + state.moreTransactionsDisplayed } else { limit = initialTransactionsDisplayedLimit } transactions = Array(transactions.prefix(Int(limit))) var i: Int32 = 0 for transaction in transactions { entries.append(.adsStarsTransaction(i, presentationData.theme, transaction)) i += 1 } if starsTransactionsInfo.canLoadMore || starsTransactionsInfo.transactions.count > transactions.count { let moreCount: Int32 if !state.transactionsExpanded { moreCount = min(20, Int32(starsTransactionsInfo.transactions.count - transactions.count)) } else { moreCount = min(50, Int32(starsTransactionsInfo.transactions.count - transactions.count)) } entries.append(.adsTransactionsExpand(presentationData.theme, presentationData.strings.Monetization_Transaction_ShowMoreTransactions(moreCount), true)) } } if isCreator && canViewRevenue { var switchOffAdds: Bool? = nil if let boostData, boostData.level >= premiumConfiguration.minChannelRestrictAdsLevel { switchOffAdds = adsRestricted } entries.append(.adsCpmToggle(presentationData.theme, presentationData.strings.Monetization_SwitchOffAds, premiumConfiguration.minChannelRestrictAdsLevel, switchOffAdds)) entries.append(.adsCpmInfo(presentationData.theme, presentationData.strings.Monetization_SwitchOffAdsInfo)) } return entries } private func channelStatsControllerEntries( presentationData: PresentationData, state: ChannelStatsControllerState, peer: EnginePeer?, data: ChannelStats?, messages: [Message]?, stories: StoryListContext.State?, interactions: [ChannelStatsPostInteractions.PostId: ChannelStatsPostInteractions]?, boostData: ChannelBoostStatus?, boostersState: ChannelBoostersContext.State?, giftsState: ChannelBoostersContext.State?, giveawayAvailable: Bool, isGroup: Bool, boostsOnly: Bool, revenueState: RevenueStats?, revenueTransactions: RevenueStatsTransactionsContext.State, starsState: StarsRevenueStats?, starsTransactions: StarsTransactionsContext.State, adsRestricted: Bool, premiumConfiguration: PremiumConfiguration, monetizationConfiguration: MonetizationConfiguration, canViewRevenue: Bool, canViewStarsRevenue: Bool ) -> [StatsEntry] { switch state.section { case .stats: if let data { return statsEntries( presentationData: presentationData, data: data, peer: peer, messages: messages, stories: stories, interactions: interactions ) } case .boosts: if let boostData { return boostsEntries( presentationData: presentationData, state: state, isGroup: isGroup, boostData: boostData, boostsOnly: boostsOnly, boostersState: boostersState, giftsState: giftsState, giveawayAvailable: giveawayAvailable ) } case .monetization: if let revenueState { return monetizationEntries( presentationData: presentationData, state: state, peer: peer, data: revenueState, boostData: boostData, transactionsInfo: revenueTransactions, starsData: starsState, starsTransactionsInfo: starsTransactions, adsRestricted: adsRestricted, premiumConfiguration: premiumConfiguration, monetizationConfiguration: monetizationConfiguration, canViewRevenue: canViewRevenue, canViewStarsRevenue: canViewStarsRevenue ) } } return [] } public func channelStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: PeerId, section: ChannelStatsSection = .stats, boostStatus: ChannelBoostStatus? = nil, boostStatusUpdated: ((ChannelBoostStatus) -> Void)? = nil) -> ViewController { let statePromise = ValuePromise(ChannelStatsControllerState(section: section, boostersExpanded: false, moreBoostersDisplayed: 0, giftsSelected: false, starsSelected: false, transactionsExpanded: false, moreTransactionsDisplayed: 0), ignoreRepeated: true) let stateValue = Atomic(value: ChannelStatsControllerState(section: section, boostersExpanded: false, moreBoostersDisplayed: 0, giftsSelected: false, starsSelected: false, transactionsExpanded: false, moreTransactionsDisplayed: 0)) let updateState: ((ChannelStatsControllerState) -> ChannelStatsControllerState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) let monetizationConfiguration = MonetizationConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) var openPostStatsImpl: ((EnginePeer, StatsPostItem) -> Void)? var openStoryImpl: ((EngineStoryItem, UIView) -> Void)? var contextActionImpl: ((MessageId, ASDisplayNode, ContextGesture?) -> Void)? let actionsDisposable = DisposableSet() let dataPromise = Promise(nil) let messagesPromise = Promise(nil) let withdrawalDisposable = MetaDisposable() actionsDisposable.add(withdrawalDisposable) let storiesPromise = Promise() let statsContext = ChannelStatsContext(postbox: context.account.postbox, network: context.account.network, 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.interactionsGraph { statsContext.loadInteractionsGraph() statsContext.loadMuteGraph() statsContext.loadTopHoursGraph() statsContext.loadNewFollowersBySourceGraph() statsContext.loadViewsBySourceGraph() statsContext.loadLanguagesGraph() statsContext.loadInstantPageInteractionsGraph() statsContext.loadReactionsByEmotionGraph() statsContext.loadStoryInteractionsGraph() statsContext.loadStoryReactionsByEmotionGraph() } } }) dataPromise.set(.single(nil) |> then(dataSignal)) let boostDataPromise = Promise() boostDataPromise.set(.single(boostStatus) |> then(context.engine.peers.getChannelBoostStatus(peerId: peerId))) actionsDisposable.add((boostDataPromise.get() |> deliverOnMainQueue).start(next: { boostStatus in if let boostStatus, let boostStatusUpdated { boostStatusUpdated(boostStatus) } })) let boostsContext = ChannelBoostersContext(account: context.account, peerId: peerId, gift: false) let giftsContext = ChannelBoostersContext(account: context.account, peerId: peerId, gift: true) let revenueContext = RevenueStatsContext(account: context.account, peerId: peerId) let revenueState = Promise() revenueState.set(.single(nil) |> then(revenueContext.state |> map(Optional.init))) let starsContext = context.engine.payments.peerStarsRevenueContext(peerId: peerId) let starsState = Promise() starsState.set(.single(nil) |> then(starsContext.state |> map(Optional.init))) let revenueTransactions = RevenueStatsTransactionsContext(account: context.account, peerId: peerId) let starsTransactions = context.engine.payments.peerStarsTransactionsContext(subject: .peer(peerId), mode: .all) starsTransactions.loadMore() var dismissAllTooltipsImpl: (() -> Void)? var presentImpl: ((ViewController) -> Void)? var pushImpl: ((ViewController) -> Void)? var dismissImpl: (() -> Void)? var navigateToChatImpl: ((EnginePeer) -> Void)? var navigateToMessageImpl: ((EngineMessage.Id) -> Void)? var openBoostImpl: ((Bool) -> Void)? var openTonTransactionImpl: ((RevenueStatsTransactionsContext.State.Transaction) -> Void)? var openStarsTransactionImpl: ((StarsContext.State.Transaction) -> Void)? var requestTonWithdrawImpl: (() -> Void)? var requestStarsWithdrawImpl: (() -> Void)? var showTimeoutTooltipImpl: ((Int32) -> Void)? var buyAdsImpl: (() -> Void)? var updateStatusBarImpl: ((StatusBarStyle) -> Void)? var dismissInputImpl: (() -> Void)? let arguments = ChannelStatsControllerArguments(context: context, loadDetailedGraph: { graph, x -> Signal in return statsContext.loadDetailedGraph(graph, x: x) }, openPostStats: { peer, item in openPostStatsImpl?(peer, item) }, openStory: { story, sourceView in openStoryImpl?(story, sourceView) }, contextAction: { messageId, node, gesture in contextActionImpl?(messageId, node, gesture) }, copyBoostLink: { link in UIPasteboard.general.string = link let presentationData = context.sharedContext.currentPresentationData.with { $0 } presentImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.ChannelBoost_BoostLinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false })) }, shareBoostLink: { link in let shareController = ShareController(context: context, subject: .url(link), updatedPresentationData: updatedPresentationData) shareController.completed = { peerIds in let _ = (context.engine.data.get( EngineDataList( peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) ) ) |> deliverOnMainQueue).start(next: { peerList in let peers = peerList.compactMap { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 } let text: String var savedMessages = false if peerIds.count == 1, let peerId = peerIds.first, peerId == context.account.peerId { text = presentationData.strings.ChannelBoost_BoostLinkForwardTooltip_SavedMessages_One savedMessages = true } else { if peers.count == 1, let peer = peers.first { let peerName = peer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) text = presentationData.strings.ChannelBoost_BoostLinkForwardTooltip_Chat_One(peerName).string } else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last { let firstPeerName = firstPeer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) let secondPeerName = secondPeer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) text = presentationData.strings.ChannelBoost_BoostLinkForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string } else if let peer = peers.first { let peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) text = presentationData.strings.ChannelBoost_BoostLinkForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string } else { text = "" } } presentImpl?(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { action in if savedMessages, action == .info { let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) |> deliverOnMainQueue).start(next: { peer in guard let peer else { return } navigateToChatImpl?(peer) }) } return false })) }) } shareController.actionCompleted = { let presentationData = context.sharedContext.currentPresentationData.with { $0 } presentImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.ChannelBoost_BoostLinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false })) } presentImpl?(shareController) }, openBoost: { boost in dismissAllTooltipsImpl?() if let peer = boost.peer, !boost.flags.contains(.isGiveaway) && !boost.flags.contains(.isGift) { navigateToChatImpl?(peer) return } if boost.peer == nil, boost.flags.contains(.isGiveaway) && !boost.flags.contains(.isUnclaimed) { if let _ = boost.stars { let controller = context.sharedContext.makeStarsGiveawayBoostScreen(context: context, peerId: peerId, boost: boost) pushImpl?(controller) } else { let presentationData = context.sharedContext.currentPresentationData.with { $0 } presentImpl?(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.Stats_Boosts_TooltipToBeDistributed, timeout: nil, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false })) } return } let controller = PremiumGiftCodeScreen( context: context, subject: .boost(peerId, boost), action: {}, openPeer: { peer in navigateToChatImpl?(peer) }, openMessage: { messageId in navigateToMessageImpl?(messageId) }) pushImpl?(controller) }, expandBoosters: { var giftsSelected = false updateState { state in giftsSelected = state.giftsSelected if state.boostersExpanded { return state.withUpdatedMoreBoostersDisplayed(state.moreBoostersDisplayed + 50) } else { return state.withUpdatedBoostersExpanded(true) } } if giftsSelected { giftsContext.loadMore() } else { boostsContext.loadMore() } }, openGifts: { let controller = createGiveawayController(context: context, peerId: peerId, subject: .generic) pushImpl?(controller) }, createPrepaidGiveaway: { prepaidGiveaway in let controller = createGiveawayController(context: context, peerId: peerId, subject: .prepaid(prepaidGiveaway)) pushImpl?(controller) }, updateGiftsSelected: { selected in updateState { $0.withUpdatedGiftsSelected(selected).withUpdatedBoostersExpanded(false) } }, updateStarsSelected: { selected in updateState { $0.withUpdatedStarsSelected(selected).withUpdatedTransactionsExpanded(false) } }, requestTonWithdraw: { requestTonWithdrawImpl?() }, requestStarsWithdraw: { requestStarsWithdrawImpl?() }, showTimeoutTooltip: { timestamp in showTimeoutTooltipImpl?(timestamp) }, buyAds: { buyAdsImpl?() }, openMonetizationIntro: { let controller = MonetizationIntroScreen(context: context, openMore: {}) pushImpl?(controller) }, openMonetizationInfo: { let presentationData = context.sharedContext.currentPresentationData.with { $0 } context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: presentationData.strings.Monetization_BalanceInfo_URL, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {}) }, openTonTransaction: { transaction in openTonTransactionImpl?(transaction) }, openStarsTransaction: { transaction in openStarsTransactionImpl?(transaction) }, expandTransactions: { stars in updateState { state in if state.transactionsExpanded { return state.withUpdatedMoreTransactionsDisplayed(state.moreTransactionsDisplayed + 50) } else { return state.withUpdatedTransactionsExpanded(true) } } if stars { starsTransactions.loadMore() } else { revenueTransactions.loadMore() } }, updateCpmEnabled: { value in let _ = context.engine.peers.updateChannelRestrictAdMessages(peerId: peerId, restricted: value).start() }, presentCpmLocked: { let _ = combineLatest( queue: Queue.mainQueue(), context.engine.peers.getChannelBoostStatus(peerId: peerId), context.engine.peers.getMyBoostStatus() ).startStandalone(next: { boostStatus, myBoostStatus in guard let boostStatus, let myBoostStatus else { return } boostDataPromise.set(.single(boostStatus)) let controller = context.sharedContext.makePremiumBoostLevelsController(context: context, peerId: peerId, subject: .noAds, boostStatus: boostStatus, myBoostStatus: myBoostStatus, forceDark: false, openStats: nil) pushImpl?(controller) }) }, dismissInput: { dismissInputImpl?() }) let messageView = context.account.viewTracker.aroundMessageHistoryViewForLocation(.peer(peerId: peerId, threadId: nil), index: .upperBound, anchorIndex: .upperBound, count: 200, fixedCombinedReadStates: nil) |> map { messageHistoryView, _, _ -> MessageHistoryView? in return messageHistoryView } messagesPromise.set(.single(nil) |> then(messageView)) let storyList = PeerStoryListContext(account: context.account, peerId: peerId, isArchived: false) storyList.loadMore() storiesPromise.set( .single(nil) |> then( storyList.state |> map(Optional.init) ) ) let peer = Promise() peer.set(context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))) let canViewStatsValue = Atomic(value: true) let peerData = context.engine.data.get( TelegramEngine.EngineData.Item.Peer.CanViewStats(id: peerId), TelegramEngine.EngineData.Item.Peer.AdsRestricted(id: peerId), TelegramEngine.EngineData.Item.Peer.CanViewRevenue(id: peerId), TelegramEngine.EngineData.Item.Peer.CanViewStarsRevenue(id: peerId) ) let longLoadingSignal: Signal = .single(false) |> then(.single(true) |> delay(2.0, queue: Queue.mainQueue())) let previousData = Atomic(value: nil) let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData let signal = combineLatest( presentationData, statePromise.get(), peer.get(), dataPromise.get(), messagesPromise.get(), storiesPromise.get(), boostDataPromise.get(), boostsContext.state, giftsContext.state, revenueState.get(), revenueTransactions.state, starsState.get(), starsTransactions.state, peerData, longLoadingSignal ) |> deliverOnMainQueue |> map { presentationData, state, peer, data, messageView, stories, boostData, boostersState, giftsState, revenueState, revenueTransactions, starsState, starsTransactions, peerData, longLoading -> (ItemListControllerState, (ItemListNodeState, Any)) in let (canViewStats, adsRestricted, canViewRevenue, canViewStarsRevenue) = peerData let _ = canViewStatsValue.swap(canViewStats) var isGroup = false if let peer, case let .channel(channel) = peer, case .group = channel.info { isGroup = true } let previous = previousData.swap(data) var emptyStateItem: ItemListControllerEmptyStateItem? switch state.section { case .stats: if data == nil { if longLoading { emptyStateItem = StatsEmptyStateItem(context: context, theme: presentationData.theme, strings: presentationData.strings) } else { emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme) } } case .boosts: if boostData == nil { emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme) } case .monetization: if revenueState?.stats == nil { emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme) } } var existingGroupingKeys = Set() var idsToFilter = Set() var messages = messageView?.entries.map { $0.message } ?? [] for message in messages { if let groupingKey = message.groupingKey { if existingGroupingKeys.contains(groupingKey) { idsToFilter.insert(message.id) } else { existingGroupingKeys.insert(groupingKey) } } } messages = messages.filter { !idsToFilter.contains($0.id) }.sorted(by: { (lhsMessage, rhsMessage) -> Bool in return lhsMessage.timestamp > rhsMessage.timestamp }) let interactions = data?.postInteractions.reduce([ChannelStatsPostInteractions.PostId : ChannelStatsPostInteractions]()) { (map, interactions) -> [ChannelStatsPostInteractions.PostId : ChannelStatsPostInteractions] in var map = map map[interactions.postId] = interactions return map } var title: ItemListControllerTitle var headerItem: BoostHeaderItem? var leftNavigationButton: ItemListNavigationButton? var boostsOnly = false if section == .boosts { title = .text("") let headerTitle = isGroup ? presentationData.strings.GroupBoost_Title : presentationData.strings.ChannelBoost_Title let headerText = isGroup ? presentationData.strings.GroupBoost_Info : presentationData.strings.ChannelBoost_Info headerItem = BoostHeaderItem(context: context, theme: presentationData.theme, strings: presentationData.strings, status: boostData, title: headerTitle, text: headerText, openBoost: { openBoostImpl?(false) }, createGiveaway: { arguments.openGifts() }, openFeatures: { openBoostImpl?(true) }, back: { dismissImpl?() }, updateStatusBar: { style in updateStatusBarImpl?(style) }) leftNavigationButton = ItemListNavigationButton(content: .none, style: .regular, enabled: false, action: {}) boostsOnly = true } else { var index: Int switch state.section { case .stats: index = 0 case .boosts: if canViewStats { index = 1 } else { index = 0 } case .monetization: if canViewStats { index = 2 } else { index = 1 } } var tabs: [String] = [] if canViewStats { tabs.append(presentationData.strings.Stats_Statistics) } tabs.append(presentationData.strings.Stats_Boosts) if canViewRevenue || canViewStarsRevenue { tabs.append(presentationData.strings.Stats_Monetization) } title = .textWithTabs(peer?.compactDisplayTitle ?? "", tabs, index) } let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: title, leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: channelStatsControllerEntries(presentationData: presentationData, state: state, peer: peer, data: data, messages: messages, stories: stories, interactions: interactions, boostData: boostData, boostersState: boostersState, giftsState: giftsState, giveawayAvailable: premiumConfiguration.giveawayGiftsPurchaseAvailable, isGroup: isGroup, boostsOnly: boostsOnly, revenueState: revenueState?.stats, revenueTransactions: revenueTransactions, starsState: starsState?.stats, starsTransactions: starsTransactions, adsRestricted: adsRestricted, premiumConfiguration: premiumConfiguration, monetizationConfiguration: monetizationConfiguration, canViewRevenue: canViewRevenue, canViewStarsRevenue: canViewStarsRevenue), style: .blocks, emptyStateItem: emptyStateItem, headerItem: headerItem, crossfadeState: previous == nil, animateChanges: false) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() let _ = statsContext.state let _ = storyList.state let _ = revenueContext.state let _ = starsContext.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.titleControlValueChanged = { value in updateState { state in let canViewStats = canViewStatsValue.with { $0 } let section: ChannelStatsSection switch value { case 0: if canViewStats { section = .stats } else { section = .boosts } case 1: if canViewStats { section = .boosts } else { section = .monetization } case 2: section = .monetization let _ = (ApplicationSpecificNotice.monetizationIntroDismissed(accountManager: context.sharedContext.accountManager) |> deliverOnMainQueue).start(next: { dismissed in if !dismissed { arguments.openMonetizationIntro() let _ = ApplicationSpecificNotice.setMonetizationIntroDismissed(accountManager: context.sharedContext.accountManager).start() } }) default: section = .stats } return state.withUpdatedSection(section) } } controller.didDisappear = { [weak controller] _ in controller?.clearItemNodesHighlight(animated: true) } openPostStatsImpl = { [weak controller] peer, post in let subject: StatsSubject switch post { case let .message(message): subject = .message(id: message.id) case let .story(_, story): subject = .story(peerId: peerId, id: story.id, item: story, fromStory: false) } controller?.push(messageStatsController(context: context, subject: subject)) } openStoryImpl = { [weak controller] story, sourceView in let storyContent = SingleStoryContentContextImpl(context: context, storyId: StoryId(peerId: peerId, id: story.id), storyItem: story, readGlobally: false) let _ = (storyContent.state |> take(1) |> deliverOnMainQueue).startStandalone(next: { [weak controller, weak sourceView] _ in guard let controller, let sourceView else { return } let transitionIn = StoryContainerScreen.TransitionIn( sourceView: sourceView, sourceRect: sourceView.bounds, sourceCornerRadius: sourceView.bounds.width * 0.5, sourceIsAvatar: false ) let storyContainerScreen = StoryContainerScreen( context: context, content: storyContent, transitionIn: transitionIn, transitionOut: { [weak sourceView] peerId, storyIdValue in if let sourceView { let destinationView = sourceView return StoryContainerScreen.TransitionOut( destinationView: destinationView, transitionView: StoryContainerScreen.TransitionView( makeView: { [weak destinationView] in let parentView = UIView() if let copyView = destinationView?.snapshotContentTree(unhide: true) { parentView.addSubview(copyView) } return parentView }, updateView: { copyView, state, transition in guard let view = copyView.subviews.first else { return } let size = state.sourceSize.interpolate(to: state.destinationSize, amount: state.progress) transition.setPosition(view: view, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5)) transition.setScale(view: view, scale: size.width / state.destinationSize.width) }, insertCloneTransitionView: nil ), destinationRect: destinationView.bounds, destinationCornerRadius: destinationView.bounds.width * 0.5, destinationIsAvatar: false, completed: { [weak sourceView] in guard let sourceView else { return } sourceView.isHidden = false } ) } else { return nil } } ) controller.push(storyContainerScreen) }) } contextActionImpl = { [weak controller] messageId, sourceNode, gesture in guard let controller = controller, let sourceNode = sourceNode as? ContextExtractedContentContainingNode else { return } let presentationData = context.sharedContext.currentPresentationData.with { $0 } var items: [ContextMenuItem] = [] items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_ViewInChannel, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor) }, action: { [weak controller] c, _ in c?.dismiss(completion: { let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) |> deliverOnMainQueue).start(next: { peer in guard let peer = peer else { return } if let navigationController = controller?.navigationController as? NavigationController { context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false))) } }) }) }))) let contextController = ContextController(presentationData: presentationData, source: .extracted(ChannelStatsContextExtractedContentSource(controller: controller, sourceNode: sourceNode, keepInPlace: false)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) controller.presentInGlobalOverlay(contextController) } dismissAllTooltipsImpl = { [weak controller] in if let controller { controller.window?.forEachController({ controller in if let controller = controller as? UndoOverlayController { controller.dismiss() } }) controller.forEachController({ controller in if let controller = controller as? UndoOverlayController { controller.dismiss() } return true }) } } presentImpl = { [weak controller] c in controller?.present(c, in: .window(.root)) } pushImpl = { [weak controller] c in controller?.push(c) } dismissImpl = { [weak controller] in controller?.dismiss() } navigateToChatImpl = { [weak controller] peer in if let navigationController = controller?.navigationController as? NavigationController { context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), keepStack: .always, purposefulAction: {}, peekData: nil, forceOpenChat: true)) } } navigateToMessageImpl = { [weak controller] messageId in let _ = (context.engine.data.get( TelegramEngine.EngineData.Item.Peer.Peer(id: messageId.peerId) ) |> deliverOnMainQueue).start(next: { peer in guard let peer = peer else { return } if let navigationController = controller?.navigationController as? NavigationController { context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false), keepStack: .always, useExisting: false, purposefulAction: {}, peekData: nil)) } }) } openBoostImpl = { features in if features { let boostController = PremiumBoostLevelsScreen( context: context, peerId: peerId, mode: .features, status: nil, myBoostStatus: nil ) pushImpl?(boostController) } else { let _ = combineLatest( queue: Queue.mainQueue(), context.engine.peers.getChannelBoostStatus(peerId: peerId), context.engine.peers.getMyBoostStatus() ).startStandalone(next: { boostStatus, myBoostStatus in guard let boostStatus, let myBoostStatus else { return } boostDataPromise.set(.single(boostStatus)) let boostController = PremiumBoostLevelsScreen( context: context, peerId: peerId, mode: .owner(subject: nil), status: boostStatus, myBoostStatus: myBoostStatus, openGift: { let giveawayController = createGiveawayController(context: context, peerId: peerId, subject: .generic) pushImpl?(giveawayController) } ) boostController.boostStatusUpdated = { boostStatus, _ in boostDataPromise.set(.single(boostStatus)) } pushImpl?(boostController) }) } } requestTonWithdrawImpl = { withdrawalDisposable.set((context.engine.peers.checkChannelRevenueWithdrawalAvailability() |> deliverOnMainQueue).start(error: { error in let controller = revenueWithdrawalController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, initialError: error, present: { c, _ in presentImpl?(c) }, completion: { url in let presentationData = context.sharedContext.currentPresentationData.with { $0 } context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {}) }) presentImpl?(controller) })) } requestStarsWithdrawImpl = { withdrawalDisposable.set((context.engine.peers.checkStarsRevenueWithdrawalAvailability() |> deliverOnMainQueue).start(error: { error in switch error { case .serverProvided: return case .requestPassword: let _ = (starsContext.state |> take(1) |> deliverOnMainQueue).startStandalone(next: { state in guard let stats = state.stats else { return } let controller = context.sharedContext.makeStarsWithdrawalScreen(context: context, stats: stats, completion: { amount in let controller = confirmStarsRevenueWithdrawalController(context: context, peerId: peerId, amount: amount, present: { c, a in presentImpl?(c) }, completion: { url in let presentationData = context.sharedContext.currentPresentationData.with { $0 } context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {}) Queue.mainQueue().after(2.0) { starsContext.reload() starsTransactions.reload() } }) presentImpl?(controller) }) pushImpl?(controller) }) default: let controller = starsRevenueWithdrawalController(context: context, peerId: peerId, amount: 0, initialError: error, present: { c, a in presentImpl?(c) }, completion: { _ in }) presentImpl?(controller) } })) } var tooltipScreen: UndoOverlayController? nonisolated(unsafe) var timer: Foundation.Timer? showTimeoutTooltipImpl = { cooldownUntilTimestamp in let remainingCooldownSeconds = cooldownUntilTimestamp - Int32(Date().timeIntervalSince1970) let presentationData = context.sharedContext.currentPresentationData.with { $0 } let content: UndoOverlayContent = .universal( animation: "anim_clock", scale: 0.058, colors: [:], title: nil, text: presentationData.strings.Stars_Withdraw_Withdraw_ErrorTimeout(stringForRemainingTime(remainingCooldownSeconds)).string, customUndoText: nil, timeout: nil ) let controller = UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { _ in return true }) tooltipScreen = controller presentImpl?(controller) if remainingCooldownSeconds < 3600 { if timer == nil { timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { _ in if let tooltipScreen { let remainingCooldownSeconds = cooldownUntilTimestamp - Int32(Date().timeIntervalSince1970) let content: UndoOverlayContent = .universal( animation: "anim_clock", scale: 0.058, colors: [:], title: nil, text: presentationData.strings.Stars_Withdraw_Withdraw_ErrorTimeout(stringForRemainingTime(remainingCooldownSeconds)).string, customUndoText: nil, timeout: nil ) tooltipScreen.content = content } else { if let currentTimer = timer { timer = nil currentTimer.invalidate() } } }) } } } buyAdsImpl = { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let _ = (context.engine.peers.requestStarsRevenueAdsAccountlUrl(peerId: peerId) |> deliverOnMainQueue).startStandalone(next: { url in guard let url else { return } context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {}) }) } openTonTransactionImpl = { transaction in let _ = (peer.get() |> take(1) |> deliverOnMainQueue).start(next: { peer in guard let peer else { return } pushImpl?(TransactionInfoScreen(context: context, peer: peer, transaction: transaction, openExplorer: { url in context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: url, forceExternal: true, presentationData: context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {}) })) }) } openStarsTransactionImpl = { transaction in let _ = (peer.get() |> take(1) |> deliverOnMainQueue).start(next: { peer in guard let peer else { return } pushImpl?(context.sharedContext.makeStarsTransactionScreen(context: context, transaction: transaction, peer: peer)) }) } updateStatusBarImpl = { [weak controller] style in controller?.setStatusBarStyle(style, animated: true) } dismissInputImpl = { [weak controller] in controller?.view.endEditing(true) } return controller } final class ChannelStatsContextExtractedContentSource: ContextExtractedContentSource { var keepInPlace: Bool let ignoreContentTouches: Bool = true let blurBackground: Bool = true private let controller: ViewController private let sourceNode: ContextExtractedContentContainingNode init(controller: ViewController, sourceNode: ContextExtractedContentContainingNode, keepInPlace: Bool) { self.controller = controller self.sourceNode = sourceNode self.keepInPlace = keepInPlace } func takeView() -> ContextControllerTakeViewInfo? { return ContextControllerTakeViewInfo(containingItem: .node(self.sourceNode), contentAreaInScreenSpace: UIScreen.main.bounds) } func putBack() -> ContextControllerPutBackViewInfo? { return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds) } } private struct MonetizationConfiguration { static var defaultValue: MonetizationConfiguration { return MonetizationConfiguration(withdrawalAvailable: false) } public let withdrawalAvailable: Bool fileprivate init(withdrawalAvailable: Bool) { self.withdrawalAvailable = withdrawalAvailable } static func with(appConfiguration: AppConfiguration) -> MonetizationConfiguration { if let data = appConfiguration.data, let withdrawalAvailable = data["channel_revenue_withdrawal_enabled"] as? Bool { return MonetizationConfiguration(withdrawalAvailable: withdrawalAvailable) } else { return .defaultValue } } }