Stars ref

This commit is contained in:
Isaac 2024-11-29 00:07:25 +04:00
parent 6c06c0e805
commit 1322c4364e
14 changed files with 634 additions and 88 deletions

View File

@ -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<AffiliateProgramSetupScreenInitialData, NoError>
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()

View File

@ -33,6 +33,7 @@ swift_library(
"//submodules/TelegramUI/Components/TabSelectorComponent",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/TelegramUI/Components/TextNodeWithEntities",
"//submodules/TelegramUI/Components/ListItemComponentAdaptor",
],
visibility = [
"//visibility:public",

View File

@ -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)

View File

@ -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<StatsGraph?, NoError>, 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<StatsGraph?, NoError>, 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?()
})

View File

@ -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<Never, ConnectStarRefBotError> {
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<TelegramConnectedStarRefBotList.Item?, NoError> {
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<TelegramConnectedStarRefBotList.Item?, NoError> 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<Api.payments.ConnectedStarRefBots?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<TelegramConnectedStarRefBotList.Item?, NoError> 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
}
}
}
}
}
}

View File

@ -1652,6 +1652,10 @@ public extension TelegramEngine {
public func removeConnectedStarRefBot(id: EnginePeer.Id, link: String) -> Signal<Never, ConnectStarRefBotError> {
return _internal_removeConnectedStarRefBot(account: self.account, id: id, link: link)
}
public func getStarRefBotConnection(id: EnginePeer.Id, targetId: EnginePeer.Id) -> Signal<TelegramConnectedStarRefBotList.Item?, NoError> {
return _internal_getStarRefBotConnection(account: self.account, id: id, targetId: targetId)
}
}
}

View File

@ -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<ItemGeneratorType: ItemGenerator>(
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<Empty>, 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)

View File

@ -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)
}
}

View File

@ -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<ViewControllerComponentContainer.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<Empty>()
private let title = ComponentView<Empty>()
private var avatarNode: AvatarNode?
private var selectorIcon: ComponentView<Empty>?
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<Empty>, 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<Empty>
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))

View File

@ -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<Empty> = 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
)

View File

@ -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 {

View File

@ -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

View File

@ -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<Empty>()
private let balanceView = ComponentView<Empty>()
private let earnStarsSection = ComponentView<Empty>()
private let subscriptionsView = ComponentView<Empty>()
@ -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

View File

@ -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<PresentationData, NoError>)?, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, requestsContext: PeerInvitationImportersContext? = nil) -> ViewController? {