diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index b779ade300..7881062718 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -11683,6 +11683,7 @@ Sorry for the inconvenience."; "Monetization.OverviewTitle" = "PROCEEDS OVERVIEW"; "Monetization.BalanceTitle" = "AVAILABLE BALANCE"; "Monetization.BalanceInfo" = "You will be able to collect rewards using Fragment, a third-party platform used by the advertizer to pay for the ad. [Learn More >]()"; +"Monetization.BalanceInfo_URL" = "https://telegram.org"; "Monetization.BalanceWithdraw" = "Withdraw via Fragment"; "Monetization.TransactionsTitle" = "TRANSACTION HISTORY"; @@ -11695,6 +11696,8 @@ Sorry for the inconvenience."; "Monetization.Transaction.Refund" = "Refund"; "Monetization.Transaction.Pending" = "Pending"; "Monetization.Transaction.Failed" = "Not Completed"; +"Monetization.Transaction.ShowMoreTransactions_1" = "Show %@ More Transaction"; +"Monetization.Transaction.ShowMoreTransactions_any" = "Show %@ More Transactions"; "Monetization.SwitchOffAds" = "Switch off Ads"; "Monetization.SwitchOffAdsInfo" = "You will not be eligible for any rewards if you switch off ads."; @@ -11723,4 +11726,6 @@ Sorry for the inconvenience."; "Monetization.Intro.Info.Title" = "What's #TON?"; "Monetization.Intro.Info.Text" = "TON is a blockchain platform and cryptocurrency that Telegram uses for its record scalability and ultra low commissions on transactions. [Learn More >]()"; +"Monetization.Intro.Info.Text_URL" = "https://ton.org"; + "Monetization.Intro.Understood" = "Understood"; diff --git a/submodules/ItemListUI/Sources/Items/ItemListSwitchItem.swift b/submodules/ItemListUI/Sources/Items/ItemListSwitchItem.swift index 58139b0b91..82c4fc84c0 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListSwitchItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListSwitchItem.swift @@ -6,6 +6,7 @@ import SwiftSignalKit import TelegramPresentationData import SwitchNode import AppBundle +import ComponentFlow public enum ItemListSwitchItemNodeType { case regular @@ -17,6 +18,7 @@ public class ItemListSwitchItem: ListViewItem, ItemListItem { let icon: UIImage? let title: String let text: String? + let titleBadgeComponent: AnyComponent? let value: Bool let type: ItemListSwitchItemNodeType let enableInteractiveChanges: Bool @@ -31,11 +33,12 @@ public class ItemListSwitchItem: ListViewItem, ItemListItem { let activatedWhileDisabled: () -> Void public let tag: ItemListItemTag? - public init(presentationData: ItemListPresentationData, icon: UIImage? = nil, title: String, text: String? = nil, value: Bool, type: ItemListSwitchItemNodeType = .regular, enableInteractiveChanges: Bool = true, enabled: Bool = true, displayLocked: Bool = false, disableLeadingInset: Bool = false, maximumNumberOfLines: Int = 1, noCorners: Bool = false, sectionId: ItemListSectionId, style: ItemListStyle, updated: @escaping (Bool) -> Void, activatedWhileDisabled: @escaping () -> Void = {}, tag: ItemListItemTag? = nil) { + public init(presentationData: ItemListPresentationData, icon: UIImage? = nil, title: String, text: String? = nil, titleBadgeComponent: AnyComponent? = nil, value: Bool, type: ItemListSwitchItemNodeType = .regular, enableInteractiveChanges: Bool = true, enabled: Bool = true, displayLocked: Bool = false, disableLeadingInset: Bool = false, maximumNumberOfLines: Int = 1, noCorners: Bool = false, sectionId: ItemListSectionId, style: ItemListStyle, updated: @escaping (Bool) -> Void, activatedWhileDisabled: @escaping () -> Void = {}, tag: ItemListItemTag? = nil) { self.presentationData = presentationData self.icon = icon self.title = title self.text = text + self.titleBadgeComponent = titleBadgeComponent self.value = value self.type = type self.enableInteractiveChanges = enableInteractiveChanges @@ -134,6 +137,8 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode { private let switchGestureNode: ASDisplayNode private var disabledOverlayNode: ASDisplayNode? + private var titleBadgeComponentView: ComponentView? + private var lockedIconNode: ASImageNode? private let activateArea: AccessibilityAreaNode @@ -471,6 +476,32 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode { lockedIconNode.removeFromSupernode() } + if let component = item.titleBadgeComponent { + let componentView: ComponentView + if let current = strongSelf.titleBadgeComponentView { + componentView = current + } else { + componentView = ComponentView() + strongSelf.titleBadgeComponentView = componentView + } + + let badgeSize = componentView.update( + transition: .immediate, + component: component, + environment: {}, + containerSize: contentSize + ) + if let view = componentView.view { + if view.superview == nil { + strongSelf.view.addSubview(view) + } + view.frame = CGRect(origin: CGPoint(x: strongSelf.titleNode.frame.maxX + 7.0, y: floor((contentSize.height - badgeSize.height) / 2.0)), size: badgeSize) + } + } else if let componentView = strongSelf.titleBadgeComponentView { + strongSelf.titleBadgeComponentView = nil + componentView.view?.removeFromSuperview() + } + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: 44.0 + UIScreenPixel + UIScreenPixel)) } }) diff --git a/submodules/StatisticsUI/BUILD b/submodules/StatisticsUI/BUILD index 797cde9f1f..0d09b77234 100644 --- a/submodules/StatisticsUI/BUILD +++ b/submodules/StatisticsUI/BUILD @@ -39,6 +39,7 @@ swift_library( "//submodules/TextFormat", "//submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent", "//submodules/TelegramUI/Components/Stories/StoryContainerScreen", + "//submodules/TelegramUI/Components/Settings/BoostLevelIconComponent", "//submodules/TelegramUI/Components/PlainButtonComponent", "//submodules/TelegramUI/Components/EmojiTextAttachmentView", "//submodules/Components/SheetComponent", diff --git a/submodules/StatisticsUI/Sources/ChannelStatsController.swift b/submodules/StatisticsUI/Sources/ChannelStatsController.swift index 5a63322652..8c75f8e8f0 100644 --- a/submodules/StatisticsUI/Sources/ChannelStatsController.swift +++ b/submodules/StatisticsUI/Sources/ChannelStatsController.swift @@ -23,8 +23,11 @@ import ItemListPeerActionItem import PremiumUI import StoryContainerScreen import TelegramNotices +import ComponentFlow +import BoostLevelIconComponent private let initialBoostersDisplayedLimit: Int32 = 5 +private let initialTransactionsDisplayedLimit: Int32 = 5 private final class ChannelStatsControllerArguments { let context: AccountContext @@ -42,13 +45,14 @@ private final class ChannelStatsControllerArguments { let requestWithdraw: () -> Void let openMonetizationIntro: () -> Void + let openMonetizationInfo: () -> Void let openTransaction: (RevenueStatsTransactionsContext.State.Transaction) -> Void let expandTransactions: () -> 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, requestWithdraw: @escaping () -> Void, openMonetizationIntro: @escaping () -> Void, openTransaction: @escaping (RevenueStatsTransactionsContext.State.Transaction) -> Void, expandTransactions: @escaping () -> Void, updateCpmEnabled: @escaping (Bool) -> Void, presentCpmLocked: @escaping () -> Void, dismissInput: @escaping () -> 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, requestWithdraw: @escaping () -> Void, openMonetizationIntro: @escaping () -> Void, openMonetizationInfo: @escaping () -> Void, openTransaction: @escaping (RevenueStatsTransactionsContext.State.Transaction) -> Void, expandTransactions: @escaping () -> Void, updateCpmEnabled: @escaping (Bool) -> Void, presentCpmLocked: @escaping () -> Void, dismissInput: @escaping () -> Void) { self.context = context self.loadDetailedGraph = loadDetailedGraph self.openPostStats = openPostStats @@ -63,6 +67,7 @@ private final class ChannelStatsControllerArguments { self.updateGiftsSelected = updateGiftsSelected self.requestWithdraw = requestWithdraw self.openMonetizationIntro = openMonetizationIntro + self.openMonetizationInfo = openMonetizationInfo self.openTransaction = openTransaction self.expandTransactions = expandTransactions self.updateCpmEnabled = updateCpmEnabled @@ -225,8 +230,9 @@ private enum StatsEntry: ItemListNodeEntry { case adsTransactionsTitle(PresentationTheme, String) case adsTransaction(Int32, PresentationTheme, RevenueStatsTransactionsContext.State.Transaction) + case adsTransactionsExpand(PresentationTheme, String) - case adsCpmToggle(PresentationTheme, String, Bool?) + case adsCpmToggle(PresentationTheme, String, Int32, Bool?) case adsCpmInfo(PresentationTheme, String) var section: ItemListSectionId { @@ -281,7 +287,7 @@ private enum StatsEntry: ItemListNodeEntry { return StatsSection.adsProceeds.rawValue case .adsBalanceTitle, .adsBalance, .adsBalanceInfo: return StatsSection.adsBalance.rawValue - case .adsTransactionsTitle, .adsTransaction: + case .adsTransactionsTitle, .adsTransaction, .adsTransactionsExpand: return StatsSection.adsTransactions.rawValue case .adsCpmToggle, .adsCpmInfo: return StatsSection.adsCpm.rawValue @@ -404,10 +410,12 @@ private enum StatsEntry: ItemListNodeEntry { return 20010 case let .adsTransaction(index, _, _): return 20011 + index + case .adsTransactionsExpand: + return 30000 case .adsCpmToggle: - return 21000 + return 30001 case .adsCpmInfo: - return 21002 + return 30002 } } @@ -755,8 +763,14 @@ private enum StatsEntry: ItemListNodeEntry { } else { return false } - case let .adsCpmToggle(lhsTheme, lhsText, lhsValue): - if case let .adsCpmToggle(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + case let .adsTransactionsExpand(lhsTheme, lhsText): + if case let .adsTransactionsExpand(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + 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 @@ -806,7 +820,6 @@ private enum StatsEntry: ItemListNodeEntry { let .boostersInfo(_, text), let .boostLinkInfo(_, text), let .boostGiftsInfo(_, text), - let .adsBalanceInfo(_, text), let .adsCpmInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) case let .overview(_, stats): @@ -961,6 +974,10 @@ private enum StatsEntry: ItemListNodeEntry { sectionId: self.section, style: .blocks ) + case let .adsBalanceInfo(_, text): + return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: { _ in + arguments.openMonetizationInfo() + }) case let .adsTransaction(_, theme, transaction): let font = Font.regular(presentationData.fontSize.itemListBaseFontSize) let smallLabelFont = Font.regular(floor(presentationData.fontSize.itemListBaseFontSize / 17.0 * 13.0)) @@ -973,22 +990,22 @@ private enum StatsEntry: ItemListNodeEntry { switch transaction { case let .proceeds(_, fromDate, toDate): title = NSAttributedString(string: presentationData.strings.Monetization_Transaction_Proceeds, font: font, textColor: theme.list.itemPrimaryTextColor) - detailText = "\(stringForMediumDate(timestamp: fromDate, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat)) – \(stringForMediumDate(timestamp: toDate, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat))" + detailText = "\(stringForMediumCompactDate(timestamp: fromDate, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat)) – \(stringForMediumCompactDate(timestamp: toDate, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat))" 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 = stringForMediumDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat) + detailText = stringForMediumCompactDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat) case .failed: - detailText = stringForMediumDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat) + " – \(presentationData.strings.Monetization_Transaction_Failed)" + detailText = stringForMediumCompactDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat) + " – \(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 = stringForMediumDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat) + detailText = stringForMediumCompactDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat) } let label = amountAttributedString(formatBalanceText(transaction.amount, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator, showPlus: true), integralFont: font, fractionalFont: smallLabelFont, color: labelColor).mutableCopy() as! NSMutableAttributedString @@ -997,8 +1014,19 @@ private enum StatsEntry: ItemListNodeEntry { 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.openTransaction(transaction) }) - case let .adsCpmToggle(_, title, value): - return ItemListSwitchItem(presentationData: presentationData, title: title, value: value == true, enableInteractiveChanges: value != nil, enabled: true, displayLocked: value == nil, sectionId: self.section, style: .blocks, updated: { updatedValue in + case let .adsTransactionsExpand(theme, title): + return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.downArrowImage(theme), title: title, sectionId: self.section, editing: false, action: { + arguments.expandTransactions() + }) + 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 { @@ -1022,19 +1050,25 @@ private struct ChannelStatsControllerState: Equatable { let boostersExpanded: Bool let moreBoostersDisplayed: Int32 let giftsSelected: Bool + let transactionsExpanded: Bool + let moreTransactionsDisplayed: Int32 init() { self.section = .stats self.boostersExpanded = false self.moreBoostersDisplayed = 0 self.giftsSelected = false + self.transactionsExpanded = false + self.moreTransactionsDisplayed = 0 } - init(section: ChannelStatsSection, boostersExpanded: Bool, moreBoostersDisplayed: Int32, giftsSelected: Bool) { + init(section: ChannelStatsSection, boostersExpanded: Bool, moreBoostersDisplayed: Int32, giftsSelected: Bool, transactionsExpanded: Bool, moreTransactionsDisplayed: Int32) { self.section = section self.boostersExpanded = boostersExpanded self.moreBoostersDisplayed = moreBoostersDisplayed self.giftsSelected = giftsSelected + self.transactionsExpanded = transactionsExpanded + self.moreTransactionsDisplayed = moreTransactionsDisplayed } static func ==(lhs: ChannelStatsControllerState, rhs: ChannelStatsControllerState) -> Bool { @@ -1050,23 +1084,37 @@ private struct ChannelStatsControllerState: Equatable { if lhs.giftsSelected != rhs.giftsSelected { 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) + return ChannelStatsControllerState(section: section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: self.giftsSelected, transactionsExpanded: self.transactionsExpanded, moreTransactionsDisplayed: self.moreTransactionsDisplayed) } func withUpdatedBoostersExpanded(_ boostersExpanded: Bool) -> ChannelStatsControllerState { - return ChannelStatsControllerState(section: self.section, boostersExpanded: boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: self.giftsSelected) + return ChannelStatsControllerState(section: self.section, boostersExpanded: boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: self.giftsSelected, transactionsExpanded: self.transactionsExpanded, moreTransactionsDisplayed: self.moreTransactionsDisplayed) } func withUpdatedMoreBoostersDisplayed(_ moreBoostersDisplayed: Int32) -> ChannelStatsControllerState { - return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: moreBoostersDisplayed, giftsSelected: self.giftsSelected) + return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: moreBoostersDisplayed, giftsSelected: self.giftsSelected, transactionsExpanded: self.transactionsExpanded, moreTransactionsDisplayed: self.moreTransactionsDisplayed) } func withUpdatedGiftsSelected(_ giftsSelected: Bool) -> ChannelStatsControllerState { - return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: giftsSelected) + return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: giftsSelected, 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, transactionsExpanded: self.transactionsExpanded, moreTransactionsDisplayed: self.moreTransactionsDisplayed) + } + + func withUpdatedMoreTransactionsDisplayed(_ moreTransactionsDisplayed: Int32) -> ChannelStatsControllerState { + return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: self.giftsSelected, transactionsExpanded: self.transactionsExpanded, moreTransactionsDisplayed: moreTransactionsDisplayed) } } @@ -1309,9 +1357,11 @@ private func boostsEntries( private func monetizationEntries( presentationData: PresentationData, state: ChannelStatsControllerState, + peer: EnginePeer?, data: RevenueStats, boostData: ChannelBoostStatus?, - transactions: RevenueStatsTransactionsContext.State, + transactionsInfo: RevenueStatsTransactionsContext.State, + adsRestricted: Bool, animatedEmojis: [String: [StickerPackItem]], premiumConfiguration: PremiumConfiguration ) -> [StatsEntry] { @@ -1333,25 +1383,50 @@ private func monetizationEntries( entries.append(.adsProceedsTitle(presentationData.theme, presentationData.strings.Monetization_OverviewTitle)) entries.append(.adsProceedsOverview(presentationData.theme, data, diamond)) + + var withdrawalAvailable = false + if let peer, case let .channel(channel) = peer, channel.flags.contains(.isCreator) && data.availableBalance > 0 { + withdrawalAvailable = true + } entries.append(.adsBalanceTitle(presentationData.theme, presentationData.strings.Monetization_BalanceTitle)) - entries.append(.adsBalance(presentationData.theme, data, false, diamond)) + entries.append(.adsBalance(presentationData.theme, data, withdrawalAvailable, diamond)) entries.append(.adsBalanceInfo(presentationData.theme, presentationData.strings.Monetization_BalanceInfo)) - if !transactions.transactions.isEmpty { + if !transactionsInfo.transactions.isEmpty { entries.append(.adsTransactionsTitle(presentationData.theme, presentationData.strings.Monetization_TransactionsTitle)) + + 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.transactions { + 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(500, transactionsInfo.count - Int32(transactions.count)) + } + entries.append(.adsTransactionsExpand(presentationData.theme, presentationData.strings.Monetization_Transaction_ShowMoreTransactions(moreCount))) + } } var switchOffAdds: Bool? = nil if let boostData, boostData.level >= premiumConfiguration.minChannelRestrictAdsLevel { - switchOffAdds = false + switchOffAdds = adsRestricted } - entries.append(.adsCpmToggle(presentationData.theme, presentationData.strings.Monetization_SwitchOffAds, switchOffAdds)) + entries.append(.adsCpmToggle(presentationData.theme, presentationData.strings.Monetization_SwitchOffAds, premiumConfiguration.minChannelRestrictAdsLevel, switchOffAdds)) entries.append(.adsCpmInfo(presentationData.theme, presentationData.strings.Monetization_SwitchOffAdsInfo)) return entries @@ -1374,6 +1449,7 @@ private func channelStatsControllerEntries( animatedEmojis: [String: [StickerPackItem]], revenueState: RevenueStats?, revenueTransactions: RevenueStatsTransactionsContext.State, + adsRestricted: Bool, premiumConfiguration: PremiumConfiguration ) -> [StatsEntry] { switch state.section { @@ -1406,9 +1482,11 @@ private func channelStatsControllerEntries( return monetizationEntries( presentationData: presentationData, state: state, + peer: peer, data: revenueState, boostData: boostData, - transactions: revenueTransactions, + transactionsInfo: revenueTransactions, + adsRestricted: adsRestricted, animatedEmojis: animatedEmojis, premiumConfiguration: premiumConfiguration ) @@ -1418,8 +1496,8 @@ private func channelStatsControllerEntries( } 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), ignoreRepeated: true) - let stateValue = Atomic(value: ChannelStatsControllerState(section: section, boostersExpanded: false, moreBoostersDisplayed: 0, giftsSelected: false)) + let statePromise = ValuePromise(ChannelStatsControllerState(section: section, boostersExpanded: false, moreBoostersDisplayed: 0, giftsSelected: false, transactionsExpanded: false, moreTransactionsDisplayed: 0), ignoreRepeated: true) + let stateValue = Atomic(value: ChannelStatsControllerState(section: section, boostersExpanded: false, moreBoostersDisplayed: 0, giftsSelected: false, transactionsExpanded: false, moreTransactionsDisplayed: 0)) let updateState: ((ChannelStatsControllerState) -> ChannelStatsControllerState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } @@ -1471,6 +1549,8 @@ public func channelStatsController(context: AccountContext, updatedPresentationD let boostsContext = ChannelBoostersContext(account: context.account, peerId: peerId, gift: false) let giftsContext = ChannelBoostersContext(account: context.account, peerId: peerId, gift: true) let revenueContext = RevenueStatsContext(postbox: context.account.postbox, network: context.account.network, peerId: peerId) + let revenueState = Promise() + revenueState.set(.single(nil) |> then(revenueContext.state |> map(Optional.init))) let revenueTransactions = RevenueStatsTransactionsContext(account: context.account, peerId: peerId) @@ -1612,13 +1692,25 @@ public func channelStatsController(context: AccountContext, updatedPresentationD 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: {}) + }, openTransaction: { transaction in openTransactionImpl?(transaction) }, expandTransactions: { - + updateState { state in + if state.transactionsExpanded { + return state.withUpdatedMoreTransactionsDisplayed(state.moreTransactionsDisplayed + 50) + } else { + return state.withUpdatedTransactionsExpanded(true) + } + } + revenueTransactions.loadMore() }, updateCpmEnabled: { value in + let _ = context.engine.peers.updateChannelRestrictAdMessages(peerId: peerId, value: value ? .restrict(minCpm: nil) : .unrestrict).start() }, presentCpmLocked: { let _ = combineLatest( @@ -1658,6 +1750,8 @@ public func channelStatsController(context: AccountContext, updatedPresentationD let peer = Promise() peer.set(context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))) + let adsRestricted = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.AdsRestricted(id: peerId)) + let longLoadingSignal: Signal = .single(false) |> then(.single(true) |> delay(2.0, queue: Queue.mainQueue())) let previousData = Atomic(value: nil) @@ -1672,13 +1766,14 @@ public func channelStatsController(context: AccountContext, updatedPresentationD boostDataPromise.get(), boostsContext.state, giftsContext.state, - revenueContext.state, + revenueState.get(), revenueTransactions.state, + adsRestricted, longLoadingSignal, context.animatedEmojiStickers ) |> deliverOnMainQueue - |> map { presentationData, state, peer, data, messageView, stories, boostData, boostersState, giftsState, revenueState, revenueTransactions, longLoading, animatedEmojiStickers -> (ItemListControllerState, (ItemListNodeState, Any)) in + |> map { presentationData, state, peer, data, messageView, stories, boostData, boostersState, giftsState, revenueState, revenueTransactions, adsRestricted, longLoading, animatedEmojiStickers -> (ItemListControllerState, (ItemListNodeState, Any)) in var isGroup = false if let peer, case let .channel(channel) = peer, case .group = channel.info { isGroup = true @@ -1700,8 +1795,9 @@ public func channelStatsController(context: AccountContext, updatedPresentationD emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme) } case .monetization: - emptyStateItem = nil -// emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme) + if revenueState == nil { + emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme) + } } var existingGroupingKeys = Set() @@ -1762,7 +1858,7 @@ public func channelStatsController(context: AccountContext, updatedPresentationD } 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, animatedEmojis: animatedEmojiStickers, revenueState: revenueState.stats, revenueTransactions: revenueTransactions, premiumConfiguration: premiumConfiguration), style: .blocks, emptyStateItem: emptyStateItem, headerItem: headerItem, crossfadeState: previous == nil, animateChanges: false) + 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, animatedEmojis: animatedEmojiStickers, revenueState: revenueState?.stats, revenueTransactions: revenueTransactions, adsRestricted: adsRestricted, premiumConfiguration: premiumConfiguration), style: .blocks, emptyStateItem: emptyStateItem, headerItem: headerItem, crossfadeState: previous == nil, animateChanges: false) return (controllerState, (listState, arguments)) } @@ -1770,6 +1866,7 @@ public func channelStatsController(context: AccountContext, updatedPresentationD actionsDisposable.dispose() let _ = statsContext.state let _ = storyList.state + let _ = revenueContext.state } let controller = ItemListController(context: context, state: signal) diff --git a/submodules/StatisticsUI/Sources/MonetizationIntroScreen.swift b/submodules/StatisticsUI/Sources/MonetizationIntroScreen.swift index 9ace4fafcf..f2a2837892 100644 --- a/submodules/StatisticsUI/Sources/MonetizationIntroScreen.swift +++ b/submodules/StatisticsUI/Sources/MonetizationIntroScreen.swift @@ -224,6 +224,7 @@ private final class SheetContent: CombinedComponent { state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: linkColor)!, theme) } + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } let infoString = strings.Monetization_Intro_Info_Text let infoAttributedString = parseMarkdownIntoAttributedString(infoString, attributes: markdownAttributes).mutableCopy() as! NSMutableAttributedString if let range = infoAttributedString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 { @@ -234,7 +235,17 @@ private final class SheetContent: CombinedComponent { text: .plain(infoAttributedString), horizontalAlignment: .center, maximumNumberOfLines: 0, - lineSpacing: 0.2 + lineSpacing: 0.2, + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + }, + tapAction: { _, _ in + component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: strings.Monetization_Intro_Info_Text_URL, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {}) + } ), availableSize: CGSize(width: context.availableSize.width - (textSideInset + sideInset - 2.0) * 2.0, height: context.availableSize.height), transition: .immediate diff --git a/submodules/StatisticsUI/Sources/TransactionInfoScreen.swift b/submodules/StatisticsUI/Sources/TransactionInfoScreen.swift index c55888ab60..90688d1893 100644 --- a/submodules/StatisticsUI/Sources/TransactionInfoScreen.swift +++ b/submodules/StatisticsUI/Sources/TransactionInfoScreen.swift @@ -139,7 +139,7 @@ private final class SheetContent: CombinedComponent { case let .proceeds(amount, fromDate, toDate): amountString = amountAttributedString(formatBalanceText(amount, decimalSeparator: dateTimeFormat.decimalSeparator, showPlus: true), integralFont: integralFont, fractionalFont: fractionalFont, color: theme.list.itemDisclosureActions.constructive.fillColor).mutableCopy() as! NSMutableAttributedString amountString.append(NSAttributedString(string: " TON", font: fractionalFont, textColor: theme.list.itemDisclosureActions.constructive.fillColor)) - dateString = "\(stringForFullDate(timestamp: fromDate, strings: strings, dateTimeFormat: dateTimeFormat)) – \(stringForFullDate(timestamp: toDate, strings: strings, dateTimeFormat: dateTimeFormat))" + dateString = "\(stringForMediumCompactDate(timestamp: fromDate, strings: strings, dateTimeFormat: dateTimeFormat)) – \(stringForMediumCompactDate(timestamp: toDate, strings: strings, dateTimeFormat: dateTimeFormat))" titleString = strings.Monetization_TransactionInfo_Proceeds buttonTitle = strings.Common_OK explorerUrl = nil diff --git a/submodules/TelegramCore/Sources/Statistics/RevenueStatistics.swift b/submodules/TelegramCore/Sources/Statistics/RevenueStatistics.swift index 39ea5f432c..87fad96878 100644 --- a/submodules/TelegramCore/Sources/Statistics/RevenueStatistics.swift +++ b/submodules/TelegramCore/Sources/Statistics/RevenueStatistics.swift @@ -240,7 +240,9 @@ private final class RevenueStatsTransactionsContextImpl { return .complete() } let offset = lastOffset ?? 0 - let request = Api.functions.stats.getBroadcastRevenueTransactions(channel: inputChannel, offset: offset, limit: 50) + let limit: Int32 = lastOffset == nil ? 25 : 50 + + let request = Api.functions.stats.getBroadcastRevenueTransactions(channel: inputChannel, offset: offset, limit: limit) let signal: Signal if let statsDatacenterId = statsDatacenterId, account.network.datacenterId != statsDatacenterId { signal = account.network.download(datacenterId: Int(statsDatacenterId), isMedia: false, tag: nil) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift index bdcfb2e3a5..25530f9d0b 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift @@ -1812,5 +1812,33 @@ public extension TelegramEngine.EngineData.Item { } } } + + public struct AdsRestricted: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { + public typealias Result = Bool + + fileprivate var id: EnginePeer.Id + public var mapKey: EnginePeer.Id { + return self.id + } + + public init(id: EnginePeer.Id) { + self.id = id + } + + var key: PostboxViewKey { + return .cachedPeerData(peerId: self.id) + } + + func extract(view: PostboxView) -> Result { + guard let view = view as? CachedPeerDataView else { + preconditionFailure() + } + if let cachedData = view.cachedPeerData as? CachedChannelData { + return cachedData.flags.contains(.adsRestricted) + } else { + return false + } + } + } } } diff --git a/submodules/TelegramStringFormatting/Sources/DateFormat.swift b/submodules/TelegramStringFormatting/Sources/DateFormat.swift index fc3e439fa5..3d2a314fd0 100644 --- a/submodules/TelegramStringFormatting/Sources/DateFormat.swift +++ b/submodules/TelegramStringFormatting/Sources/DateFormat.swift @@ -58,6 +58,26 @@ public func getDateTimeComponents(timestamp: Int32) -> (day: Int32, month: Int32 return (timeinfo.tm_mday, timeinfo.tm_mon + 1, timeinfo.tm_year, timeinfo.tm_hour, timeinfo.tm_min) } +public func stringForMediumCompactDate(timestamp: Int32, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat) -> String { + var t: time_t = Int(timestamp) + var timeinfo = tm() + localtime_r(&t, &timeinfo); + + let day = timeinfo.tm_mday + let month = monthAtIndex(Int(timeinfo.tm_mon), strings: strings) + + let timeString = stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), dateTimeFormat: dateTimeFormat) + + let dateString: String + switch dateTimeFormat.dateFormat { + case .monthFirst: + dateString = String(format: "%@ %02d %@", month, day, timeString) + case .dayFirst: + dateString = String(format: "%02d %@ %@", day, month, timeString) + } + return dateString +} + public func stringForMediumDate(timestamp: Int32, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, withTime: Bool = true) -> String { var t: time_t = Int(timestamp) var timeinfo = tm() diff --git a/submodules/TelegramStringFormatting/Sources/PresenceStrings.swift b/submodules/TelegramStringFormatting/Sources/PresenceStrings.swift index eda8f1b4ce..6f677e221b 100644 --- a/submodules/TelegramStringFormatting/Sources/PresenceStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/PresenceStrings.swift @@ -107,7 +107,7 @@ public func stringForMonth(strings: PresentationStrings, month: Int32, ofYear ye } } -private func monthAtIndex(_ index: Int, strings: PresentationStrings) -> String { +func monthAtIndex(_ index: Int, strings: PresentationStrings) -> String { switch index { case 0: return strings.Month_ShortJanuary diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoBirthdayOverlay.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoBirthdayOverlay.swift index 103d3d82a4..40f7d38aab 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoBirthdayOverlay.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoBirthdayOverlay.swift @@ -32,6 +32,7 @@ final class PeerInfoBirthdayOverlay: ASDisplayNode { self.setupAnimations(size: size, birthday: birthday, sourceRect: sourceRect) Queue.mainQueue().after(0.1) { + HapticFeedback().success() self.view.addSubview(ConfettiView(frame: CGRect(origin: .zero, size: size))) } }