From 1322c4364ec1216856200e04a3102cc0942c28aa Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 29 Nov 2024 00:07:25 +0400 Subject: [PATCH] Stars ref --- .../Sources/AccountContext.swift | 34 +++++ submodules/ItemListUI/BUILD | 1 + .../Items/ItemListDisclosureItem.swift | 75 ++++++++++- .../Sources/ChannelStatsController.swift | 39 +++++- .../TelegramEngine/Messages/BotWebView.swift | 57 +++++++- .../Peers/TelegramEnginePeers.swift | 4 + .../Sources/ListItemComponentAdaptor.swift | 68 +++++++++- .../Sources/AffiliateProgramSetupScreen.swift | 16 ++- .../Sources/JoinAffiliateProgramScreen.swift | 118 ++++++++++------ .../Sources/TableComponent.swift | 114 ++++++++++++++-- .../PeerInfoScreenDisclosureItem.swift | 22 ++- .../Sources/PeerInfoScreen.swift | 126 +++++++++++++++--- .../Sources/StarsTransactionsScreen.swift | 44 ++++++ .../Sources/SharedAccountContext.swift | 4 + 14 files changed, 634 insertions(+), 88 deletions(-) diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 631f67622f..8ad1ae6e6e 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -881,6 +881,38 @@ public final class BotPreviewEditorTransitionOut { public protocol MiniAppListScreenInitialData: AnyObject { } +public enum JoinAffiliateProgramScreenMode { + public final class Join { + public let initialTargetPeer: EnginePeer + public let canSelectTargetPeer: Bool + public let completion: (EnginePeer) -> Void + + public init(initialTargetPeer: EnginePeer, canSelectTargetPeer: Bool, completion: @escaping (EnginePeer) -> Void) { + self.initialTargetPeer = initialTargetPeer + self.canSelectTargetPeer = canSelectTargetPeer + self.completion = completion + } + } + + public final class Active { + public let targetPeer: EnginePeer + public let link: String + public let userCount: Int + public let copyLink: () -> Void + + public init(targetPeer: EnginePeer, link: String, userCount: Int, copyLink: @escaping () -> Void) { + self.targetPeer = targetPeer + self.link = link + self.userCount = userCount + self.copyLink = copyLink + } + } + + + case join(Join) + case active(Active) +} + public protocol SharedAccountContext: AnyObject { var sharedContainerPath: String { get } var basePath: String { get } @@ -1074,6 +1106,8 @@ public protocol SharedAccountContext: AnyObject { func makeAffiliateProgramSetupScreenInitialData(context: AccountContext, peerId: EnginePeer.Id, mode: AffiliateProgramSetupScreenMode) -> Signal func makeAffiliateProgramSetupScreen(context: AccountContext, initialData: AffiliateProgramSetupScreenInitialData) -> ViewController + func makeAffiliateProgramJoinScreen(context: AccountContext, sourcePeer: EnginePeer, commissionPermille: Int32, programDuration: Int32?, mode: JoinAffiliateProgramScreenMode) -> ViewController + func makeDebugSettingsController(context: AccountContext?) -> ViewController? func navigateToCurrentCall() diff --git a/submodules/ItemListUI/BUILD b/submodules/ItemListUI/BUILD index ab4bb1f6cf..71afc19942 100644 --- a/submodules/ItemListUI/BUILD +++ b/submodules/ItemListUI/BUILD @@ -33,6 +33,7 @@ swift_library( "//submodules/TelegramUI/Components/TabSelectorComponent", "//submodules/Components/ComponentDisplayAdapters", "//submodules/TelegramUI/Components/TextNodeWithEntities", + "//submodules/TelegramUI/Components/ListItemComponentAdaptor", ], visibility = [ "//visibility:public", diff --git a/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift b/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift index cd8408de51..fe473f69e4 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift @@ -9,6 +9,7 @@ import AvatarNode import TelegramCore import AccountContext import TextNodeWithEntities +import ListItemComponentAdaptor private let avatarFont = avatarPlaceholderFont(size: 16.0) @@ -46,7 +47,7 @@ public enum ItemListDisclosureItemDetailLabelColor { case destructive } -public class ItemListDisclosureItem: ListViewItem, ItemListItem { +public class ItemListDisclosureItem: ListViewItem, ItemListItem, ListItemComponentAdaptor.ItemGenerator { let presentationData: ItemListPresentationData let icon: UIImage? let context: AccountContext? @@ -56,6 +57,7 @@ public class ItemListDisclosureItem: ListViewItem, ItemListItem { let titleColor: ItemListDisclosureItemTitleColor let titleFont: ItemListDisclosureItemTitleFont let titleIcon: UIImage? + let titleBadge: String? let enabled: Bool let label: String let attributedLabel: NSAttributedString? @@ -71,7 +73,7 @@ public class ItemListDisclosureItem: ListViewItem, ItemListItem { public let tag: ItemListItemTag? public let shimmeringIndex: Int? - public init(presentationData: ItemListPresentationData, icon: UIImage? = nil, context: AccountContext? = nil, iconPeer: EnginePeer? = nil, title: String, attributedTitle: NSAttributedString? = nil, enabled: Bool = true, titleColor: ItemListDisclosureItemTitleColor = .primary, titleFont: ItemListDisclosureItemTitleFont = .regular, titleIcon: UIImage? = nil, label: String, attributedLabel: NSAttributedString? = nil, labelStyle: ItemListDisclosureLabelStyle = .text, additionalDetailLabel: String? = nil, additionalDetailLabelColor: ItemListDisclosureItemDetailLabelColor = .generic, sectionId: ItemListSectionId, style: ItemListStyle, disclosureStyle: ItemListDisclosureStyle = .arrow, noInsets: Bool = false, action: (() -> Void)?, clearHighlightAutomatically: Bool = true, tag: ItemListItemTag? = nil, shimmeringIndex: Int? = nil) { + public init(presentationData: ItemListPresentationData, icon: UIImage? = nil, context: AccountContext? = nil, iconPeer: EnginePeer? = nil, title: String, attributedTitle: NSAttributedString? = nil, enabled: Bool = true, titleColor: ItemListDisclosureItemTitleColor = .primary, titleFont: ItemListDisclosureItemTitleFont = .regular, titleIcon: UIImage? = nil, titleBadge: String? = nil, label: String, attributedLabel: NSAttributedString? = nil, labelStyle: ItemListDisclosureLabelStyle = .text, additionalDetailLabel: String? = nil, additionalDetailLabelColor: ItemListDisclosureItemDetailLabelColor = .generic, sectionId: ItemListSectionId, style: ItemListStyle, disclosureStyle: ItemListDisclosureStyle = .arrow, noInsets: Bool = false, action: (() -> Void)?, clearHighlightAutomatically: Bool = true, tag: ItemListItemTag? = nil, shimmeringIndex: Int? = nil) { self.presentationData = presentationData self.icon = icon self.context = context @@ -81,6 +83,7 @@ public class ItemListDisclosureItem: ListViewItem, ItemListItem { self.titleColor = titleColor self.titleFont = titleFont self.titleIcon = titleIcon + self.titleBadge = titleBadge self.enabled = enabled self.labelStyle = labelStyle self.label = label @@ -140,6 +143,27 @@ public class ItemListDisclosureItem: ListViewItem, ItemListItem { self.action?() } } + + public func item() -> ListViewItem { + return self + } + + public static func ==(lhs: ItemListDisclosureItem, rhs: ItemListDisclosureItem) -> Bool { + if lhs.presentationData != rhs.presentationData { + return false + } + if lhs.context !== rhs.context { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.label != rhs.label { + return false + } + + return true + } } private let badgeFont = Font.regular(15.0) @@ -162,6 +186,9 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { let labelBadgeNode: ASImageNode let labelImageNode: ASImageNode + var titleBadgeNode: ASImageNode? + var titleBadgeTextNode: TextNode? + private let activateArea: AccessibilityAreaNode private var item: ItemListDisclosureItem? @@ -260,6 +287,8 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { let makeLabelLayout = TextNode.asyncLayout(self.labelNode) let makeAdditionalDetailLabelLayout = TextNode.asyncLayout(self.additionalDetailLabelNode) + let makeTitleBadgeTextNodeLayout = TextNode.asyncLayout(self.titleBadgeTextNode) + let currentItem = self.item let currentHasBadge = self.labelBadgeNode.image != nil @@ -374,6 +403,13 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { maxTitleWidth -= 12.0 } + var titleBadgeTextNodeLayout: (TextNodeLayout, () -> TextNode)? + if let titleBadge = item.titleBadge { + let titleBadgeTextNodeLayoutValue = makeTitleBadgeTextNodeLayout(TextNodeLayoutArguments(attributedString: item.attributedTitle ?? NSAttributedString(string: titleBadge, font: Font.medium(11.0), textColor: item.presentationData.theme.list.itemCheckColors.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + titleBadgeTextNodeLayout = titleBadgeTextNodeLayoutValue + maxTitleWidth -= 5.0 + titleBadgeTextNodeLayoutValue.0.size.width + } + let titleArguments = TextNodeLayoutArguments(attributedString: item.attributedTitle ?? NSAttributedString(string: item.title, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: item.attributedTitle != nil ? 0 : 1, truncationType: .end, constrainedSize: CGSize(width: maxTitleWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()) let (titleLayoutAndApply) = item.context == nil ? makeTitleLayout(titleArguments) : nil let (titleWithEntitiesLayoutAndApply) = item.context != nil ? makeTitleWithEntitiesLayout(titleArguments) : nil @@ -680,6 +716,41 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { additionalDetailLabelNode.removeFromSupernode() } + if let (badgeTextLayout, badgeTextApply) = titleBadgeTextNodeLayout { + let titleBadgeNode: ASImageNode + if let current = strongSelf.titleBadgeNode { + titleBadgeNode = current + } else { + titleBadgeNode = ASImageNode() + strongSelf.titleBadgeNode = titleBadgeNode + strongSelf.addSubnode(titleBadgeNode) + + titleBadgeNode.image = generateFilledRoundedRectImage(size: CGSize(width: 16.0, height: 16.0), cornerRadius: 5.0, color: item.presentationData.theme.list.itemCheckColors.fillColor)?.stretchableImage(withLeftCapWidth: 6, topCapHeight: 6) + } + + let titleBadgeTextNode = badgeTextApply() + if titleBadgeTextNode.supernode == nil { + strongSelf.addSubnode(titleBadgeTextNode) + } + let badgeSideInset: CGFloat = 5.0 + let badgeVerticalInset: CGFloat = 2.0 + let badgeSize = CGSize(width: badgeTextLayout.size.width + badgeSideInset * 2.0, height: badgeTextLayout.size.height + badgeVerticalInset * 2.0) + let titleBadgeFrame = CGRect(origin: CGPoint(x: titleFrame.maxX + 5.0, y: titleFrame.minY + floorToScreenPixels((titleFrame.height - badgeSize.height) * 0.5)), size: badgeSize) + let titleBadgeTextFrame = CGRect(origin: CGPoint(x: titleBadgeFrame.minX + badgeSideInset, y: titleBadgeFrame.minY + badgeVerticalInset), size: badgeTextLayout.size) + + titleBadgeNode.frame = titleBadgeFrame + titleBadgeTextNode.frame = titleBadgeTextFrame + } else { + if let titleBadgeTextNode = strongSelf.titleBadgeTextNode { + strongSelf.titleBadgeTextNode = nil + titleBadgeTextNode.removeFromSupernode() + } + if let titleBadgeNode = strongSelf.titleBadgeNode { + strongSelf.titleBadgeNode = nil + titleBadgeNode.removeFromSupernode() + } + } + if let titleIcon = item.titleIcon { if strongSelf.titleIconNode.supernode == nil { strongSelf.addSubnode(strongSelf.titleIconNode) diff --git a/submodules/StatisticsUI/Sources/ChannelStatsController.swift b/submodules/StatisticsUI/Sources/ChannelStatsController.swift index 1cccf07048..223d65543d 100644 --- a/submodules/StatisticsUI/Sources/ChannelStatsController.swift +++ b/submodules/StatisticsUI/Sources/ChannelStatsController.swift @@ -56,9 +56,10 @@ private final class ChannelStatsControllerArguments { let expandTransactions: (Bool) -> Void let updateCpmEnabled: (Bool) -> Void let presentCpmLocked: () -> Void + let openEarnStars: () -> 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) { + 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, openEarnStars: @escaping () -> Void, dismissInput: @escaping () -> Void) { self.context = context self.loadDetailedGraph = loadDetailedGraph self.openPostStats = openPostStats @@ -83,6 +84,7 @@ private final class ChannelStatsControllerArguments { self.expandTransactions = expandTransactions self.updateCpmEnabled = updateCpmEnabled self.presentCpmLocked = presentCpmLocked + self.openEarnStars = openEarnStars self.dismissInput = dismissInput } } @@ -119,6 +121,8 @@ private enum StatsSection: Int32 { case adsStarsBalance case adsTransactions case adsCpm + + case earnStars } enum StatsPostItem: Equatable { @@ -249,6 +253,7 @@ private enum StatsEntry: ItemListNodeEntry { case adsStarsBalance(PresentationTheme, StarsRevenueStats, Bool, Bool, Int32?) case adsStarsBalanceInfo(PresentationTheme, String) + case earnStarsInfo case adsTransactionsTitle(PresentationTheme, String) case adsTransactionsTabs(PresentationTheme, String, String, Bool) case adsTransaction(Int32, PresentationTheme, RevenueStatsTransactionsContext.State.Transaction) @@ -314,6 +319,8 @@ private enum StatsEntry: ItemListNodeEntry { return StatsSection.adsTonBalance.rawValue case .adsStarsBalanceTitle, .adsStarsBalance, .adsStarsBalanceInfo: return StatsSection.adsStarsBalance.rawValue + case .earnStarsInfo: + return StatsSection.earnStars.rawValue case .adsTransactionsTitle, .adsTransactionsTabs, .adsTransaction, .adsStarsTransaction, .adsTransactionsExpand: return StatsSection.adsTransactions.rawValue case .adsCpmToggle, .adsCpmInfo: @@ -445,14 +452,16 @@ private enum StatsEntry: ItemListNodeEntry { return 20014 case .adsStarsBalanceInfo: return 20015 - case .adsTransactionsTitle: + case .earnStarsInfo: return 20016 - case .adsTransactionsTabs: + case .adsTransactionsTitle: return 20017 + case .adsTransactionsTabs: + return 20018 case let .adsTransaction(index, _, _): - return 20018 + index + return 20019 + index case let .adsStarsTransaction(index, _, _): - return 30017 + index + return 30018 + index case .adsTransactionsExpand: return 40000 case .adsCpmToggle: @@ -830,6 +839,12 @@ private enum StatsEntry: ItemListNodeEntry { } else { return false } + case .earnStarsInfo: + if case .earnStarsInfo = rhs { + return true + } else { + return false + } case let .adsTransactionsTitle(lhsTheme, lhsText): if case let .adsTransactionsTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true @@ -1203,6 +1218,11 @@ private enum StatsEntry: ItemListNodeEntry { }, activatedWhileDisabled: { arguments.presentCpmLocked() }) + case .earnStarsInfo: + //TODO:localize + return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.earnStars, title: "Earn Stars", titleBadge: presentationData.strings.Settings_New, label: "Distribute links to mini apps and earn a share of their revenue in Stars.", labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, action: { + arguments.openEarnStars() + }) } } } @@ -1680,6 +1700,9 @@ private func monetizationEntries( if displayStarsTransactions { if !addedTransactionsTabs { + //TODO:localize + entries.append(.earnStarsInfo) + entries.append(.adsTransactionsTitle(presentationData.theme, presentationData.strings.Monetization_StarsTransactions.uppercased())) } @@ -2071,6 +2094,12 @@ public func channelStatsController( pushImpl?(controller) }) }, + openEarnStars: { + let _ = (context.sharedContext.makeAffiliateProgramSetupScreenInitialData(context: context, peerId: peerId, mode: .connectedPrograms) + |> deliverOnMainQueue).startStandalone(next: { initialData in + pushImpl?(context.sharedContext.makeAffiliateProgramSetupScreen(context: context, initialData: initialData)) + }) + }, dismissInput: { dismissInputImpl?() }) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift index d1736ad2f3..22594e5a06 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift @@ -907,8 +907,6 @@ func _internal_connectStarRefBot(account: Account, id: EnginePeer.Id, botId: Eng } } -//payments.editConnectedStarRefBot flags:# revoked:flags.0?true peer:InputPeer link:string = payments.ConnectedStarRefBots; - func _internal_removeConnectedStarRefBot(account: Account, id: EnginePeer.Id, link: String) -> Signal { return account.postbox.transaction { transaction -> Api.InputPeer? in return transaction.getPeer(id).flatMap(apiInputPeer) @@ -957,3 +955,58 @@ func _internal_removeConnectedStarRefBot(account: Account, id: EnginePeer.Id, li } } } + +func _internal_getStarRefBotConnection(account: Account, id: EnginePeer.Id, targetId: EnginePeer.Id) -> Signal { + return account.postbox.transaction { transaction -> (Api.InputUser?, Api.InputPeer?) in + return ( + transaction.getPeer(id).flatMap(apiInputUser), + transaction.getPeer(targetId).flatMap(apiInputPeer) + ) + } + |> mapToSignal { inputPeer, targetPeer -> Signal in + guard let inputPeer, let targetPeer else { + return .single(nil) + } + return account.network.request(Api.functions.payments.getConnectedStarRefBot(peer: targetPeer, bot: inputPeer)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal in + guard let result else { + return .single(nil) + } + return account.postbox.transaction { transaction -> TelegramConnectedStarRefBotList.Item? in + switch result { + case let .connectedStarRefBots(_, connectedBots, users): + updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: AccumulatedPeers(users: users)) + + if let bot = connectedBots.first { + switch bot { + case let .connectedBotStarRef(flags, url, date, botId, commissionPermille, durationMonths, participants, revenue): + let isRevoked = (flags & (1 << 1)) != 0 + if isRevoked { + return nil + } + + guard let botPeer = transaction.getPeer(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(botId))) else { + return nil + } + return TelegramConnectedStarRefBotList.Item( + peer: EnginePeer(botPeer), + url: url, + timestamp: date, + commissionPermille: commissionPermille, + durationMonths: durationMonths, + participants: participants, + revenue: revenue + ) + } + } else { + return nil + } + } + } + } + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index 0f2cba7d1d..2ccdd25a1f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -1652,6 +1652,10 @@ public extension TelegramEngine { public func removeConnectedStarRefBot(id: EnginePeer.Id, link: String) -> Signal { return _internal_removeConnectedStarRefBot(account: self.account, id: id, link: link) } + + public func getStarRefBotConnection(id: EnginePeer.Id, targetId: EnginePeer.Id) -> Signal { + return _internal_getStarRefBotConnection(account: self.account, id: id, targetId: targetId) + } } } diff --git a/submodules/TelegramUI/Components/ListItemComponentAdaptor/Sources/ListItemComponentAdaptor.swift b/submodules/TelegramUI/Components/ListItemComponentAdaptor/Sources/ListItemComponentAdaptor.swift index 0b8b5a87ea..066ed407f8 100644 --- a/submodules/TelegramUI/Components/ListItemComponentAdaptor/Sources/ListItemComponentAdaptor.swift +++ b/submodules/TelegramUI/Components/ListItemComponentAdaptor/Sources/ListItemComponentAdaptor.swift @@ -16,10 +16,12 @@ public final class ListItemComponentAdaptor: Component { private let isEqualImpl: (AnyObject) -> Bool private let itemImpl: () -> ListViewItem private let params: ListViewItemLayoutParams + private let action: (() -> Void)? public init( itemGenerator: ItemGeneratorType, - params: ListViewItemLayoutParams + params: ListViewItemLayoutParams, + action: (() -> Void)? = nil ) { self.itemGenerator = itemGenerator self.isEqualImpl = { other in @@ -33,6 +35,7 @@ public final class ListItemComponentAdaptor: Component { return itemGenerator.item() } self.params = params + self.action = action } public static func ==(lhs: ListItemComponentAdaptor, rhs: ListItemComponentAdaptor) -> Bool { @@ -42,13 +45,28 @@ public final class ListItemComponentAdaptor: Component { if lhs.params != rhs.params { return false } + if (lhs.action == nil) != (rhs.action == nil) { + return false + } return true } public final class View: UIView { + private var button: HighlightTrackingButton? public var itemNode: ListViewItemNode? + private var component: ListItemComponentAdaptor? + + @objc private func pressed() { + guard let component = self.component else { + return + } + component.action?() + } + func update(component: ListItemComponentAdaptor, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + let item = component.itemImpl() if let itemNode = self.itemNode { @@ -84,7 +102,32 @@ public final class ListItemComponentAdaptor: Component { apply(ListViewItemApply(isOnScreen: true)) } ) + if let resultSize { + itemNode.isUserInteractionEnabled = component.action == nil + if component.action != nil { + let button: HighlightTrackingButton + if let current = self.button { + button = current + } else { + button = HighlightTrackingButton() + self.button = button + self.addSubview(button) + button.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + button.highligthedChanged = { [weak self] isHighlighted in + guard let self, let itemNode = self.itemNode else { + return + } + itemNode.setHighlighted(isHighlighted, at: itemNode.bounds.center, animated: !isHighlighted) + } + } + + transition.setFrame(view: button, frame: CGRect(origin: CGPoint(), size: resultSize)) + } else if let button = self.button { + self.button = nil + button.removeFromSuperview() + } + transition.setFrame(view: itemNode.view, frame: CGRect(origin: CGPoint(), size: resultSize)) return resultSize } else { @@ -107,6 +150,29 @@ public final class ListItemComponentAdaptor: Component { } ) if let itemNode { + itemNode.isUserInteractionEnabled = component.action == nil + if component.action != nil { + let button: HighlightTrackingButton + if let current = self.button { + button = current + } else { + button = HighlightTrackingButton() + self.button = button + self.addSubview(button) + button.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + button.highligthedChanged = { [weak self] isHighlighted in + guard let self, let itemNode = self.itemNode else { + return + } + itemNode.setHighlighted(isHighlighted, at: itemNode.bounds.center, animated: !isHighlighted) + } + } + transition.setFrame(view: button, frame: CGRect(origin: CGPoint(), size: itemNode.bounds.size)) + } else if let button = self.button { + self.button = nil + button.removeFromSuperview() + } + self.itemNode = itemNode self.addSubnode(itemNode) diff --git a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/AffiliateProgramSetupScreen.swift b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/AffiliateProgramSetupScreen.swift index 59d0a46e43..39323b7ae7 100644 --- a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/AffiliateProgramSetupScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/AffiliateProgramSetupScreen.swift @@ -182,7 +182,7 @@ final class AffiliateProgramSetupScreenComponent: Component { self.environment?.controller()?.present(tableAlert( theme: presentationData.theme, title: "Warning", - text: "This change is irreversible. You won't be able to reduce commission or duration. You can only increase these parameters or end the program, which will disable all previously shared referral links.", + text: "Once you start the affiliate program, you won't be able to decrease its commission or duration. You can only increase these parameters or end the program, whuch will disable all previously distributed referral links.", table: TableComponent(theme: environment.theme, items: [ TableComponent.Item(id: 0, title: "Commission", component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString(string: commissionTitle, font: Font.regular(17.0), textColor: environment.theme.actionSheet.primaryTextColor)) @@ -362,7 +362,7 @@ If you end your affiliate program: sourcePeer: bot.peer, commissionPermille: bot.commissionPermille, programDuration: bot.durationMonths, - mode: .active(JoinAffiliateProgramScreen.Active( + mode: .active(JoinAffiliateProgramScreenMode.Active( targetPeer: targetPeer, link: bot.url, userCount: Int(bot.participants), @@ -452,7 +452,7 @@ If you end your affiliate program: } let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }) - let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView)), items: .single(ContextController.Items(id: AnyHashable(0), content: .list(items))), gesture: nil) + let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView, actionsOnTop: false)), items: .single(ContextController.Items(id: AnyHashable(0), content: .list(items))), gesture: nil) controller.presentInGlobalOverlay(contextController) } @@ -1403,7 +1403,7 @@ If you end your affiliate program: sourcePeer: botPeer, commissionPermille: item.commissionPermille, programDuration: item.durationMonths, - mode: .join(JoinAffiliateProgramScreen.Join( + mode: .join(JoinAffiliateProgramScreenMode.Join( initialTargetPeer: targetPeer, canSelectTargetPeer: false, completion: { [weak self] _ in @@ -1646,16 +1646,18 @@ private final class ListContextExtractedContentSource: ContextExtractedContentSo } } -private final class HeaderContextReferenceContentSource: ContextReferenceContentSource { +final class HeaderContextReferenceContentSource: ContextReferenceContentSource { private let controller: ViewController private let sourceView: UIView + private let actionsOnTop: Bool - init(controller: ViewController, sourceView: UIView) { + init(controller: ViewController, sourceView: UIView, actionsOnTop: Bool) { self.controller = controller self.sourceView = sourceView + self.actionsOnTop = actionsOnTop } func transitionInfo() -> ContextControllerReferenceViewInfo? { - return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds) + return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds, actionsPosition: self.actionsOnTop ? .top : .bottom) } } diff --git a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/JoinAffiliateProgramScreen.swift b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/JoinAffiliateProgramScreen.swift index 56fafded8e..dedb249075 100644 --- a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/JoinAffiliateProgramScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/JoinAffiliateProgramScreen.swift @@ -326,6 +326,49 @@ private final class JoinAffiliateProgramScreenComponent: Component { } } + private func displayTargetSelectionMenu(sourceView: UIView) { + guard let component = self.component, let environment = self.environment, let controller = environment.controller() else { + return + } + guard case let .join(join) = component.mode else { + return + } + + var items: [ContextMenuItem] = [] + + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }) + + let peers: [EnginePeer] = [ + join.initialTargetPeer + ] + + let avatarSize = CGSize(width: 30.0, height: 30.0) + + for peer in peers { + let peerLabel: String + if peer.id == component.context.account.peerId { + peerLabel = "personal account" + } else if case .channel = peer { + peerLabel = "channel" + } else { + peerLabel = "bot" + } + items.append(.action(ContextMenuActionItem(text: peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder), textLayout: .secondLineWithValue(peerLabel), icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: peerAvatarCompleteImage(account: component.context.account, peer: peer, size: avatarSize)), action: { [weak self] c, _ in + c?.dismiss(completion: {}) + + guard let self else { + return + } + + self.currentTargetPeer = peer + self.state?.updated(transition: .immediate) + }))) + } + + let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView, actionsOnTop: true)), items: .single(ContextController.Items(id: AnyHashable(0), content: .list(items))), gesture: nil) + controller.presentInGlobalOverlay(contextController) + } + func update(component: JoinAffiliateProgramScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { @@ -690,7 +733,12 @@ private final class JoinAffiliateProgramScreenComponent: Component { theme: environment.theme, strings: environment.strings, peer: currentTargetPeer, - isSelectable: isTargetPeerSelectable + action: isTargetPeerSelectable ? { [weak self] sourceView in + guard let self else { + return + } + self.displayTargetSelectionMenu(sourceView: sourceView) + } : nil )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) @@ -947,36 +995,7 @@ private final class JoinAffiliateProgramScreenComponent: Component { } public class JoinAffiliateProgramScreen: ViewControllerComponentContainer { - public final class Join { - public let initialTargetPeer: EnginePeer - public let canSelectTargetPeer: Bool - public let completion: (EnginePeer) -> Void - - public init(initialTargetPeer: EnginePeer, canSelectTargetPeer: Bool, completion: @escaping (EnginePeer) -> Void) { - self.initialTargetPeer = initialTargetPeer - self.canSelectTargetPeer = canSelectTargetPeer - self.completion = completion - } - } - - public final class Active { - public let targetPeer: EnginePeer - public let link: String - public let userCount: Int - public let copyLink: () -> Void - - public init(targetPeer: EnginePeer, link: String, userCount: Int, copyLink: @escaping () -> Void) { - self.targetPeer = targetPeer - self.link = link - self.userCount = userCount - self.copyLink = copyLink - } - } - - public enum Mode { - case join(Join) - case active(Active) - } + public typealias Mode = JoinAffiliateProgramScreenMode private let context: AccountContext @@ -1042,20 +1061,20 @@ private final class PeerBadgeComponent: Component { let theme: PresentationTheme let strings: PresentationStrings let peer: EnginePeer - let isSelectable: Bool + let action: ((UIView) -> Void)? init( context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, peer: EnginePeer, - isSelectable: Bool + action: ((UIView) -> Void)? ) { self.context = context self.theme = theme self.strings = strings self.peer = peer - self.isSelectable = isSelectable + self.action = action } static func ==(lhs: PeerBadgeComponent, rhs: PeerBadgeComponent) -> Bool { @@ -1071,38 +1090,53 @@ private final class PeerBadgeComponent: Component { if lhs.peer != rhs.peer { return false } - if lhs.isSelectable != rhs.isSelectable { + if (lhs.action == nil) != (rhs.action == nil) { return false } return true } - final class View: UIView { + final class View: HighlightableButton { private let background = ComponentView() private let title = ComponentView() private var avatarNode: AvatarNode? private var selectorIcon: ComponentView? + private var component: PeerBadgeComponent? + override init(frame: CGRect) { super.init(frame: frame) + + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + @objc private func pressed() { + guard let component = self.component else { + return + } + component.action?(self) + } + func update(component: PeerBadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + + self.isEnabled = component.action != nil + let height: CGFloat = 32.0 let avatarPadding: CGFloat = 1.0 let avatarDiameter = height - avatarPadding * 2.0 let avatarTextSpacing: CGFloat = 4.0 - let rightTextInset: CGFloat = component.isSelectable ? 26.0 : 12.0 + let rightTextInset: CGFloat = component.action != nil ? 26.0 : 12.0 let titleSize = self.title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: component.peer.displayTitle(strings: component.strings, displayOrder: .firstLast), font: Font.medium(15.0), textColor: component.isSelectable ? component.theme.list.itemInputField.primaryColor : component.theme.list.itemInputField.primaryColor)) + text: .plain(NSAttributedString(string: component.peer.displayTitle(strings: component.strings, displayOrder: .firstLast), font: Font.medium(15.0), textColor: component.action != nil ? component.theme.list.itemInputField.primaryColor : component.theme.list.itemInputField.primaryColor)) )), environment: {}, containerSize: CGSize(width: availableSize.width - avatarPadding - avatarDiameter - avatarTextSpacing - rightTextInset, height: height) @@ -1110,6 +1144,7 @@ private final class PeerBadgeComponent: Component { let titleFrame = CGRect(origin: CGPoint(x: avatarPadding + avatarDiameter + avatarTextSpacing, y: floorToScreenPixels((height - titleSize.height) * 0.5)), size: titleSize) if let titleView = self.title.view { if titleView.superview == nil { + titleView.isUserInteractionEnabled = false self.addSubview(titleView) } titleView.frame = titleFrame @@ -1120,6 +1155,7 @@ private final class PeerBadgeComponent: Component { avatarNode = current } else { avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 15.0)) + avatarNode.isUserInteractionEnabled = false avatarNode.displaysAsynchronously = false self.avatarNode = avatarNode self.addSubview(avatarNode.view) @@ -1132,7 +1168,7 @@ private final class PeerBadgeComponent: Component { let size = CGSize(width: avatarPadding + avatarDiameter + avatarTextSpacing + titleSize.width + rightTextInset, height: height) - if component.isSelectable { + if component.action != nil { let selectorIcon: ComponentView if let current = self.selectorIcon { selectorIcon = current @@ -1150,6 +1186,7 @@ private final class PeerBadgeComponent: Component { let selectorIconFrame = CGRect(origin: CGPoint(x: size.width - 8.0 - selectorIconSize.width, y: floorToScreenPixels((size.height - selectorIconSize.height) * 0.5)), size: selectorIconSize) if let selectorIconView = selectorIcon.view { if selectorIconView.superview == nil { + selectorIconView.isUserInteractionEnabled = false self.addSubview(selectorIconView) } transition.setFrame(view: selectorIconView, frame: selectorIconFrame) @@ -1162,7 +1199,7 @@ private final class PeerBadgeComponent: Component { let _ = self.background.update( transition: transition, component: AnyComponent(FilledRoundedRectangleComponent( - color: component.isSelectable ? component.theme.list.itemAccentColor.withMultipliedAlpha(0.1) : component.theme.list.itemInputField.backgroundColor, + color: component.action != nil ? component.theme.list.itemAccentColor.withMultipliedAlpha(0.1) : component.theme.list.itemInputField.backgroundColor, cornerRadius: .minEdge, smoothCorners: false )), @@ -1171,6 +1208,7 @@ private final class PeerBadgeComponent: Component { ) if let backgroundView = self.background.view { if backgroundView.superview == nil { + backgroundView.isUserInteractionEnabled = false self.insertSubview(backgroundView, at: 0) } transition.setFrame(view: backgroundView, frame: CGRect(origin: CGPoint(), size: size)) diff --git a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/TableComponent.swift b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/TableComponent.swift index b0ae853f87..52e01d8bb7 100644 --- a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/TableComponent.swift +++ b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/TableComponent.swift @@ -224,22 +224,110 @@ final class TableComponent: CombinedComponent { } } +private final class TableAlertContentComponet: CombinedComponent { + let theme: PresentationTheme + let title: String + let text: String + let table: TableComponent + + init(theme: PresentationTheme, title: String, text: String, table: TableComponent) { + self.theme = theme + self.title = title + self.text = text + self.table = table + } + + static func ==(lhs: TableAlertContentComponet, rhs: TableAlertContentComponet) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.text != rhs.text { + return false + } + if lhs.table != rhs.table { + return false + } + return true + } + + public static var body: Body { + let title = Child(MultilineTextComponent.self) + let text = Child(MultilineTextComponent.self) + let table = Child(TableComponent.self) + + return { context in + let title = title.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString(string: context.component.title, font: Font.semibold(16.0), textColor: context.component.theme.actionSheet.primaryTextColor)), + horizontalAlignment: .center + ), + availableSize: CGSize(width: context.availableSize.width, height: 10000.0), + transition: .immediate + ) + let text = text.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString(string: context.component.text, font: Font.regular(13.0), textColor: context.component.theme.actionSheet.primaryTextColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.2 + ), + availableSize: CGSize(width: context.availableSize.width, height: 10000.0), + transition: .immediate + ) + let table = table.update( + component: context.component.table, + availableSize: CGSize(width: context.availableSize.width, height: 10000.0), + transition: .immediate + ) + + var size = CGSize(width: 0.0, height: 0.0) + + size.width = max(size.width, title.size.width) + size.width = max(size.width, text.size.width) + size.width = max(size.width, table.size.width) + + size.height += title.size.height + size.height += 5.0 + size.height += text.size.height + size.height += 14.0 + size.height += table.size.height + size.height -= 3.0 + + var contentHeight: CGFloat = 0.0 + let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - title.size.width) * 0.5), y: contentHeight), size: title.size) + contentHeight += title.size.height + 5.0 + let textFrame = CGRect(origin: CGPoint(x: floor((size.width - text.size.width) * 0.5), y: contentHeight), size: text.size) + contentHeight += text.size.height + 14.0 + let tableFrame = CGRect(origin: CGPoint(x: floor((size.width - table.size.width) * 0.5), y: contentHeight), size: table.size) + contentHeight += table.size.height + + context.add(title + .position(titleFrame.center) + ) + context.add(text + .position(textFrame.center) + ) + context.add(table + .position(tableFrame.center) + ) + + return size + } + } +} + func tableAlert(theme: PresentationTheme, title: String, text: String, table: TableComponent, actions: [ComponentAlertAction]) -> ViewController { - let content: AnyComponent = AnyComponent(VStack([ - AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: title, font: Font.semibold(17.0), textColor: theme.actionSheet.primaryTextColor)), - horizontalAlignment: .center - ))), - AnyComponentWithIdentity(id: 1, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: text, font: Font.regular(17.0), textColor: theme.actionSheet.primaryTextColor)), - horizontalAlignment: .center, - maximumNumberOfLines: 0 - ))), - AnyComponentWithIdentity(id: 2, component: AnyComponent(table)), - ], spacing: 10.0)) return componentAlertController( theme: AlertControllerTheme(presentationTheme: theme, fontSize: .regular), - content: content, + content: AnyComponent(TableAlertContentComponet( + theme: theme, + title: title, + text: text, + table: table + )), actions: actions, actionLayout: .horizontal ) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenDisclosureItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenDisclosureItem.swift index c8e725e618..30538c69ef 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenDisclosureItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenDisclosureItem.swift @@ -20,6 +20,7 @@ final class PeerInfoScreenDisclosureItem: PeerInfoScreenItem { case semitransparentBadge(String, UIColor) case titleBadge(String, UIColor) case image(UIImage, CGSize) + case labelBadge(String) var text: String { switch self { @@ -27,14 +28,14 @@ final class PeerInfoScreenDisclosureItem: PeerInfoScreenItem { return "" case let .attributedText(text): return text.string - case let .text(text), let .coloredText(text, _), let .badge(text, _), let .semitransparentBadge(text, _), let .titleBadge(text, _): + case let .text(text), let .coloredText(text, _), let .badge(text, _), let .semitransparentBadge(text, _), let .titleBadge(text, _), let .labelBadge(text): return text } } var badgeColor: UIColor? { switch self { - case .none, .text, .coloredText, .image, .attributedText: + case .none, .text, .coloredText, .image, .attributedText, .labelBadge: return nil case let .badge(_, color), let .semitransparentBadge(_, color), let .titleBadge(_, color): return color @@ -170,6 +171,9 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode { } else if case .titleBadge = item.label { labelColorValue = presentationData.theme.list.itemCheckColors.foregroundColor labelFont = Font.medium(11.0) + } else if case .labelBadge = item.label { + labelColorValue = presentationData.theme.list.itemCheckColors.foregroundColor + labelFont = Font.medium(12.0) } else if case let .coloredText(_, color) = item.label { switch color { case .generic: @@ -274,6 +278,14 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode { if self.labelBadgeNode.supernode == nil { self.insertSubnode(self.labelBadgeNode, belowSubnode: self.labelNode) } + } else if case let .labelBadge(text) = item.label, !text.isEmpty { + let badgeColor = presentationData.theme.list.itemCheckColors.fillColor + if previousItem?.label.badgeColor != badgeColor { + self.labelBadgeNode.image = generateFilledRoundedRectImage(size: CGSize(width: 16.0, height: 16.0), cornerRadius: 5.0, color: badgeColor)?.stretchableImage(withLeftCapWidth: 6, topCapHeight: 6) + } + if self.labelBadgeNode.supernode == nil { + self.insertSubnode(self.labelBadgeNode, belowSubnode: self.labelNode) + } } else if item.additionalBadgeLabel != nil { if previousItem?.additionalBadgeLabel == nil { self.labelBadgeNode.image = generateFilledRoundedRectImage(size: CGSize(width: 16.0, height: 16.0), cornerRadius: 5.0, color: presentationData.theme.list.itemCheckColors.fillColor)?.stretchableImage(withLeftCapWidth: 6, topCapHeight: 6) @@ -314,6 +326,8 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode { labelFrame = CGRect(origin: CGPoint(x: width - rightInset - badgeWidth + (badgeWidth - labelSize.width) / 2.0, y: floor((height - labelSize.height) / 2.0)), size: labelSize) } else if case .titleBadge = item.label { labelFrame = CGRect(origin: CGPoint(x: textFrame.maxX + 10.0, y: floor((height - labelSize.height) / 2.0) + 1.0), size: labelSize) + } else if case .labelBadge = item.label { + labelFrame = CGRect(origin: CGPoint(x: width - rightInset - badgeWidth + (badgeWidth - labelSize.width) / 2.0, y: floor((height - labelSize.height) / 2.0)), size: labelSize) } else { labelFrame = CGRect(origin: CGPoint(x: width - rightInset - labelSize.width, y: 12.0), size: labelSize) } @@ -344,9 +358,11 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode { let labelBadgeNodeFrame: CGRect if case let .image(_, imageSize) = item.label { - labelBadgeNodeFrame = CGRect(origin: CGPoint(x: width - rightInset - imageSize.width, y: floorToScreenPixels(textFrame.midY - imageSize.height / 2.0)), size:imageSize) + labelBadgeNodeFrame = CGRect(origin: CGPoint(x: width - rightInset - imageSize.width, y: floorToScreenPixels(textFrame.midY - imageSize.height / 2.0)), size: imageSize) } else if case .titleBadge = item.label { labelBadgeNodeFrame = labelFrame.insetBy(dx: -4.0, dy: -2.0 + UIScreenPixel) + } else if case .labelBadge = item.label { + labelBadgeNodeFrame = labelFrame.insetBy(dx: -4.0, dy: -2.0 + UIScreenPixel) } else if let additionalLabelNode = self.additionalLabelNode { labelBadgeNodeFrame = additionalLabelNode.frame.insetBy(dx: -4.0, dy: -2.0 + UIScreenPixel) } else { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 341016500d..0ef2e5a0c8 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -1215,6 +1215,7 @@ private enum InfoSection: Int, CaseIterable { case permissions case peerInfoTrailing case peerMembers + case botAffiliateProgram } private func infoItems(data: PeerInfoScreenData?, context: AccountContext, presentationData: PresentationData, interaction: PeerInfoInteraction, nearbyPeerDistance: Int32?, reactionSourceMessageId: MessageId?, callMessages: [Message], chatLocation: ChatLocation, isOpenedFromChat: Bool, isMyProfile: Bool) -> [(AnyHashable, [PeerInfoScreenItem])] { @@ -1425,6 +1426,23 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese currentPeerInfoSection = .peerInfoTrailing } + + if let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) { + } else { + if let starRefProgram = cachedData.starRefProgram, starRefProgram.endDate == nil { + if items[.botAffiliateProgram] == nil { + items[.botAffiliateProgram] = [] + } + //TODO:localize + let programTitleValue: String + programTitleValue = "\(starRefProgram.commissionPermille / 10)%" + //TODO:localize + items[.botAffiliateProgram]!.append(PeerInfoScreenDisclosureItem(id: 0, label: .labelBadge(programTitleValue), additionalBadgeLabel: nil, text: "Affiliate Program", icon: PresentationResourcesSettings.affiliateProgram, action: { + interaction.editingOpenAffiliateProgram() + })) + items[.botAffiliateProgram]!.append(PeerInfoScreenCommentItem(id: 1, text: "Share a link to \(EnginePeer.user(user).compactDisplayTitle) with your friends and and earn \(starRefProgram.commissionPermille / 10)% of their spending there.")) + } + } } if let businessHours = cachedData.businessHours { @@ -1921,13 +1939,13 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL interaction.editingOpenPublicLinkSetup() })) //TODO:localize - let programTitleValue: String + let programTitleValue: PeerInfoScreenDisclosureItem.Label if let cachedData = data.cachedData as? CachedUserData, let starRefProgram = cachedData.starRefProgram, starRefProgram.endDate == nil { - programTitleValue = "\(starRefProgram.commissionPermille / 10)%" + programTitleValue = .labelBadge("\(starRefProgram.commissionPermille / 10)%") } else { - programTitleValue = "Off" + programTitleValue = .text("Off") } - items[.peerDataSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemAffiliateProgram, label: .text(programTitleValue), additionalBadgeLabel: presentationData.strings.Settings_New, text: "Affiliate Program", icon: PresentationResourcesSettings.affiliateProgram, action: { + items[.peerDataSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemAffiliateProgram, label: programTitleValue, additionalBadgeLabel: presentationData.strings.Settings_New, text: "Affiliate Program", icon: PresentationResourcesSettings.affiliateProgram, action: { interaction.editingOpenAffiliateProgram() })) @@ -8575,17 +8593,95 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } private func editingOpenAffiliateProgram() { - if let peer = self.data?.peer as? TelegramUser, peer.botInfo != nil { - let _ = (self.context.sharedContext.makeAffiliateProgramSetupScreenInitialData(context: self.context, peerId: peer.id, mode: .editProgram) - |> deliverOnMainQueue).startStandalone(next: { [weak self] initialData in - guard let self else { - return - } - let controller = self.context.sharedContext.makeAffiliateProgramSetupScreen(context: self.context, initialData: initialData) - self.controller?.push(controller) - }) - } else if let channel = self.data?.peer as? TelegramChannel { - let _ = (self.context.sharedContext.makeAffiliateProgramSetupScreenInitialData(context: self.context, peerId: channel.id, mode: .connectedPrograms) + if let peer = self.data?.peer as? TelegramUser, let botInfo = peer.botInfo { + if botInfo.flags.contains(.canEdit) { + let _ = (self.context.sharedContext.makeAffiliateProgramSetupScreenInitialData(context: self.context, peerId: peer.id, mode: .editProgram) + |> deliverOnMainQueue).startStandalone(next: { [weak self] initialData in + guard let self else { + return + } + let controller = self.context.sharedContext.makeAffiliateProgramSetupScreen(context: self.context, initialData: initialData) + self.controller?.push(controller) + }) + } else if let starRefProgram = (self.data?.cachedData as? CachedUserData)?.starRefProgram, starRefProgram.endDate == nil { + self.activeActionDisposable.set((self.context.engine.peers.getStarRefBotConnection(id: peer.id, targetId: self.context.account.peerId) + |> deliverOnMainQueue).startStrict(next: { [weak self] result in + guard let self else { + return + } + let _ = (self.context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId) + ) + |> deliverOnMainQueue).startStandalone(next: { [weak self] accountPeer in + guard let self, let accountPeer else { + return + } + let mode: JoinAffiliateProgramScreenMode + if let result { + mode = .active(JoinAffiliateProgramScreenMode.Active( + targetPeer: accountPeer, + link: result.url, + userCount: Int(result.participants), + copyLink: { [weak self] in + guard let self else { + return + } + //TODO:localize + UIPasteboard.general.string = result.url + let presentationData = self.context.sharedContext.currentPresentationData.with({ $0 }) + self.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: "Link copied to clipboard", text: "Share this link and earn **\(result.commissionPermille / 10)%** of what people who use it spend in **\(EnginePeer.user(peer).compactDisplayTitle)**!"), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + } + )) + } else { + mode = .join(JoinAffiliateProgramScreenMode.Join( + initialTargetPeer: accountPeer, + canSelectTargetPeer: true, + completion: { [weak self] targetPeer in + guard let self else { + return + } + let _ = (self.context.engine.peers.connectStarRefBot(id: targetPeer.id, botId: self.peerId) + |> deliverOnMainQueue).startStandalone(next: { [weak self] result in + guard let self else { + return + } + let bot = result + + self.controller?.push(self.context.sharedContext.makeAffiliateProgramJoinScreen( + context: self.context, + sourcePeer: bot.peer, + commissionPermille: bot.commissionPermille, + programDuration: bot.durationMonths, + mode: .active(JoinAffiliateProgramScreenMode.Active( + targetPeer: targetPeer, + link: bot.url, + userCount: Int(bot.participants), + copyLink: { [weak self] in + guard let self else { + return + } + UIPasteboard.general.string = bot.url + let presentationData = self.context.sharedContext.currentPresentationData.with({ $0 }) + self.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: "Link copied to clipboard", text: "Share this link and earn **\(bot.commissionPermille / 10)%** of what people who use it spend in **\(bot.peer.compactDisplayTitle)**!"), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + } + )) + )) + }) + } + )) + } + self.controller?.push(self.context.sharedContext.makeAffiliateProgramJoinScreen( + context: self.context, + sourcePeer: .user(peer), + commissionPermille: starRefProgram.commissionPermille, + programDuration: starRefProgram.durationMonths, + mode: mode + )) + }) + })) + } + } else if let peer = self.data?.peer { + let _ = (self.context.sharedContext.makeAffiliateProgramSetupScreenInitialData(context: self.context, peerId: peer.id, mode: .connectedPrograms) |> deliverOnMainQueue).startStandalone(next: { [weak self] initialData in guard let self else { return diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift index 57585dc01f..8a28a9a47f 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift @@ -21,6 +21,8 @@ import UndoUI import ListActionItemComponent import StarsAvatarComponent import TelegramStringFormatting +import ListItemComponentAdaptor +import ItemListUI private let initialSubscriptionsDisplayedLimit: Int32 = 3 @@ -102,6 +104,7 @@ final class StarsTransactionsScreenComponent: Component { private let descriptionView = ComponentView() private let balanceView = ComponentView() + private let earnStarsSection = ComponentView() private let subscriptionsView = ComponentView() @@ -657,6 +660,47 @@ final class StarsTransactionsScreenComponent: Component { starTransition.setFrame(view: balanceView, frame: balanceFrame) } contentHeight += balanceSize.height + contentHeight += 34.0 + + let earnStarsSectionSize = self.earnStarsSection.update( + transition: .immediate, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: nil, + footer: nil, + items: [ + //TODO:localize + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListItemComponentAdaptor( + itemGenerator: ItemListDisclosureItem(presentationData: ItemListPresentationData(presentationData), icon: PresentationResourcesSettings.earnStars, title: "Earn Stars", titleBadge: presentationData.strings.Settings_New, label: "Distribute links to mini apps and earn a share of their revenue in Stars.", labelStyle: .multilineDetailText, sectionId: 0, style: .blocks, action: { + }), + params: ListViewItemLayoutParams(width: availableSize.width, leftInset: 0.0, rightInset: 0.0, availableHeight: 10000.0, isStandalone: true), + action: { [weak self] in + guard let self, let component = self.component else { + return + } + let _ = (component.context.sharedContext.makeAffiliateProgramSetupScreenInitialData(context: component.context, peerId: component.context.account.peerId, mode: .connectedPrograms) + |> deliverOnMainQueue).startStandalone(next: { [weak self] initialData in + guard let self, let component = self.component else { + return + } + let setupScreen = component.context.sharedContext.makeAffiliateProgramSetupScreen(context: component.context, initialData: initialData) + self.controller?()?.push(setupScreen) + }) + } + ))) + ] + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInsets, height: availableSize.height) + ) + let earnStarsSectionFrame = CGRect(origin: CGPoint(x: sideInsets * 0.5, y: contentHeight), size: earnStarsSectionSize) + if let earnStarsSectionView = self.earnStarsSection.view { + if earnStarsSectionView.superview == nil { + self.scrollView.addSubview(earnStarsSectionView) + } + starTransition.setFrame(view: earnStarsSectionView, frame: earnStarsSectionFrame) + } + contentHeight += earnStarsSectionSize.height contentHeight += 44.0 let fontBaseDisplaySize = 17.0 diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 436bb1e511..cbdc0d54f8 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -2839,6 +2839,10 @@ public final class SharedAccountContextImpl: SharedAccountContext { public func makeAffiliateProgramSetupScreen(context: AccountContext, initialData: AffiliateProgramSetupScreenInitialData) -> ViewController { return AffiliateProgramSetupScreen(context: context, initialContent: initialData) } + + public func makeAffiliateProgramJoinScreen(context: AccountContext, sourcePeer: EnginePeer, commissionPermille: Int32, programDuration: Int32?, mode: JoinAffiliateProgramScreenMode) -> ViewController { + return JoinAffiliateProgramScreen(context: context, sourcePeer: sourcePeer, commissionPermille: commissionPermille, programDuration: programDuration, mode: mode) + } } private func peerInfoControllerImpl(context: AccountContext, updatedPresentationData: (PresentationData, Signal)?, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, requestsContext: PeerInvitationImportersContext? = nil) -> ViewController? {