Merge branches 'master' and 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin 2024-11-29 21:58:27 +04:00
commit 3778357baa
20 changed files with 818 additions and 216 deletions

View file

@ -896,14 +896,12 @@ public enum JoinAffiliateProgramScreenMode {
public final class Active {
public let targetPeer: EnginePeer
public let link: String
public let userCount: Int
public let copyLink: () -> Void
public let bot: TelegramConnectedStarRefBotList.Item
public let copyLink: (TelegramConnectedStarRefBotList.Item) -> Void
public init(targetPeer: EnginePeer, link: String, userCount: Int, copyLink: @escaping () -> Void) {
public init(targetPeer: EnginePeer, bot: TelegramConnectedStarRefBotList.Item, copyLink: @escaping (TelegramConnectedStarRefBotList.Item) -> Void) {
self.targetPeer = targetPeer
self.link = link
self.userCount = userCount
self.bot = bot
self.copyLink = copyLink
}
}
@ -1106,7 +1104,7 @@ 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 makeAffiliateProgramJoinScreen(context: AccountContext, sourcePeer: EnginePeer, commissionPermille: Int32, programDuration: Int32?, revenuePerUser: Double, mode: JoinAffiliateProgramScreenMode) -> ViewController
func makeDebugSettingsController(context: AccountContext?) -> ViewController?

View file

@ -299,7 +299,7 @@ public func peerAvatarImage(postbox: Postbox, network: Network, peerReference: P
if let cutoutRect {
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fillEllipse(in: cutoutRect)
context.fillEllipse(in: cutoutRect.offsetBy(dx: 0.0, dy: size.height - cutoutRect.maxY - cutoutRect.height))
}
})
let unroundedImage: UIImage?

View file

@ -0,0 +1,54 @@
import Foundation
import UIKit
public final class TransformContents<ChildEnvironment: Equatable>: CombinedComponent {
public typealias EnvironmentType = ChildEnvironment
private let content: AnyComponent<ChildEnvironment>
private let fixedSize: CGSize?
private let translation: CGPoint
public init(content: AnyComponent<ChildEnvironment>, fixedSize: CGSize? = nil, translation: CGPoint) {
self.content = content
self.fixedSize = fixedSize
self.translation = translation
}
public static func ==(lhs: TransformContents<ChildEnvironment>, rhs: TransformContents<ChildEnvironment>) -> Bool {
if lhs.content != rhs.content {
return false
}
if lhs.fixedSize != rhs.fixedSize {
return false
}
if lhs.translation != rhs.translation {
return false
}
return true
}
public static var body: Body {
let child = Child(environment: ChildEnvironment.self)
return { context in
let child = child.update(
component: context.component.content,
environment: { context.environment[ChildEnvironment.self] },
availableSize: context.availableSize,
transition: context.transition
)
let size = context.component.fixedSize ?? child.size
var childFrame = child.size.centered(in: CGRect(origin: CGPoint(), size: size))
childFrame.origin.x += context.component.translation.x
childFrame.origin.y += context.component.translation.y
context.add(child
.position(childFrame.center)
)
return size
}
}
}

View file

@ -2529,7 +2529,16 @@ open class TextNode: ASDisplayNode, TextNodeProtocol {
textColor = color
}
}
if let textColor {
if image.renderingMode == .alwaysOriginal {
let imageRect = CGRect(origin: CGPoint(x: attachment.frame.midX - image.size.width * 0.5, y: attachment.frame.midY - image.size.height * 0.5 + 1.0), size: image.size).offsetBy(dx: lineFrame.minX, dy: lineFrame.minY)
context.translateBy(x: imageRect.midX, y: imageRect.midY)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -imageRect.midX, y: -imageRect.midY)
context.draw(image.cgImage!, in: imageRect)
context.translateBy(x: imageRect.midX, y: imageRect.midY)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -imageRect.midX, y: -imageRect.midY)
} else if let textColor {
if let tintedImage = generateTintedImage(image: image, color: textColor) {
let imageRect = CGRect(origin: CGPoint(x: attachment.frame.midX - tintedImage.size.width * 0.5, y: attachment.frame.midY - tintedImage.size.height * 0.5 + 1.0), size: tintedImage.size).offsetBy(dx: lineFrame.minX, dy: lineFrame.minY)
context.translateBy(x: imageRect.midX, y: imageRect.midY)

View file

@ -1575,7 +1575,8 @@ private func monetizationEntries(
premiumConfiguration: PremiumConfiguration,
monetizationConfiguration: MonetizationConfiguration,
canViewRevenue: Bool,
canViewStarsRevenue: Bool
canViewStarsRevenue: Bool,
canJoinRefPrograms: Bool
) -> [StatsEntry] {
var entries: [StatsEntry] = []
@ -1700,8 +1701,10 @@ private func monetizationEntries(
if displayStarsTransactions {
if !addedTransactionsTabs {
//TODO:localize
entries.append(.earnStarsInfo)
if canJoinRefPrograms {
//TODO:localize
entries.append(.earnStarsInfo)
}
entries.append(.adsTransactionsTitle(presentationData.theme, presentationData.strings.Monetization_StarsTransactions.uppercased()))
}
@ -1767,7 +1770,8 @@ private func channelStatsControllerEntries(
premiumConfiguration: PremiumConfiguration,
monetizationConfiguration: MonetizationConfiguration,
canViewRevenue: Bool,
canViewStarsRevenue: Bool
canViewStarsRevenue: Bool,
canJoinRefPrograms: Bool
) -> [StatsEntry] {
switch state.section {
case .stats:
@ -1809,7 +1813,8 @@ private func channelStatsControllerEntries(
premiumConfiguration: premiumConfiguration,
monetizationConfiguration: monetizationConfiguration,
canViewRevenue: canViewRevenue,
canViewStarsRevenue: canViewStarsRevenue
canViewStarsRevenue: canViewStarsRevenue,
canJoinRefPrograms: canJoinRefPrograms
)
}
}
@ -2157,6 +2162,15 @@ public func channelStatsController(
let (canViewStats, adsRestricted, _, canViewStarsRevenue) = peerData
var canViewRevenue = peerData.2
var canJoinRefPrograms = false
if let data = context.currentAppConfiguration.with({ $0 }).data, let value = data["starref_connect_allowed"] {
if let value = value as? Double {
canJoinRefPrograms = value != 0.0
} else if let value = value as? Bool {
canJoinRefPrograms = value
}
}
let _ = canViewStatsValue.swap(canViewStats)
var isGroup = false
@ -2262,7 +2276,7 @@ public func channelStatsController(
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: title, leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: channelStatsControllerEntries(presentationData: presentationData, state: state, peer: peer, data: data, messages: messages, stories: stories, interactions: interactions, boostData: boostData, boostersState: boostersState, giftsState: giftsState, giveawayAvailable: premiumConfiguration.giveawayGiftsPurchaseAvailable, isGroup: isGroup, boostsOnly: boostsOnly, revenueState: revenueState?.stats, revenueTransactions: revenueTransactions, starsState: starsState?.stats, starsTransactions: starsTransactions, adsRestricted: adsRestricted, premiumConfiguration: premiumConfiguration, monetizationConfiguration: monetizationConfiguration, canViewRevenue: canViewRevenue, canViewStarsRevenue: canViewStarsRevenue), style: .blocks, emptyStateItem: emptyStateItem, headerItem: headerItem, crossfadeState: previous == nil, animateChanges: false)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: channelStatsControllerEntries(presentationData: presentationData, state: state, peer: peer, data: data, messages: messages, stories: stories, interactions: interactions, boostData: boostData, boostersState: boostersState, giftsState: giftsState, giveawayAvailable: premiumConfiguration.giveawayGiftsPurchaseAvailable, isGroup: isGroup, boostsOnly: boostsOnly, revenueState: revenueState?.stats, revenueTransactions: revenueTransactions, starsState: starsState?.stats, starsTransactions: starsTransactions, adsRestricted: adsRestricted, premiumConfiguration: premiumConfiguration, monetizationConfiguration: monetizationConfiguration, canViewRevenue: canViewRevenue, canViewStarsRevenue: canViewStarsRevenue, canJoinRefPrograms: canJoinRefPrograms), style: .blocks, emptyStateItem: emptyStateItem, headerItem: headerItem, crossfadeState: previous == nil, animateChanges: false)
return (controllerState, (listState, arguments))
}

View file

@ -589,6 +589,18 @@ func _internal_removeChatManagingBot(account: Account, chatId: EnginePeer.Id) ->
}
}
public func formatPermille(_ value: Int32) -> String {
return formatPermille(Int(value))
}
public func formatPermille(_ value: Int) -> String {
if value % 10 == 0 {
return "\(value / 10)"
} else {
return String(format: "%.1f", Double(value) / 10.0)
}
}
func _internal_updateStarRefProgram(account: Account, id: EnginePeer.Id, program: (commissionPermille: Int32, durationMonths: Int32?)?) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> Api.InputUser? in
return transaction.getPeer(id).flatMap(apiInputUser)
@ -632,7 +644,6 @@ func _internal_updateStarRefProgram(account: Account, id: EnginePeer.Id, program
}
public final class TelegramConnectedStarRefBotList : Equatable {
public final class Item: Equatable {
public let peer: EnginePeer
public let url: String

View file

@ -281,11 +281,15 @@ public struct StarsAmount: Equatable, Comparable, Hashable, Codable, CustomStrin
}
public var stringValue: String {
return "\(totalValue)"
}
public var totalValue: Double {
if self.nanos == 0 {
return "\(self.value)"
return Double(self.value)
} else {
let totalValue = (Double(self.value) * 1e9 + Double(self.nanos)) / 1e9
return "\(totalValue)"
return totalValue
}
}
@ -302,6 +306,10 @@ public struct StarsAmount: Equatable, Comparable, Hashable, Codable, CustomStrin
}
public static func +(lhs: StarsAmount, rhs: StarsAmount) -> StarsAmount {
if rhs.value < 0 || rhs.nanos < 0 {
return lhs - StarsAmount(value: abs(rhs.value), nanos: abs(rhs.nanos))
}
let totalNanos = Int64(lhs.nanos) + Int64(rhs.nanos)
let overflow = totalNanos / 1_000_000_000
let remainingNanos = totalNanos % 1_000_000_000
@ -529,7 +537,7 @@ private final class StarsContextImpl {
}
var transactions = state.transactions
if addTransaction {
transactions.insert(.init(flags: [.isLocal], id: "\(arc4random())", count: balance, date: Int32(Date().timeIntervalSince1970), peer: .appStore, title: nil, description: nil, photo: nil, transactionDate: nil, transactionUrl: nil, paidMessageId: nil, giveawayMessageId: nil, media: [], subscriptionPeriod: nil, starGift: nil, floodskipNumber: nil), at: 0)
transactions.insert(.init(flags: [.isLocal], id: "\(arc4random())", count: balance, date: Int32(Date().timeIntervalSince1970), peer: .appStore, title: nil, description: nil, photo: nil, transactionDate: nil, transactionUrl: nil, paidMessageId: nil, giveawayMessageId: nil, media: [], subscriptionPeriod: nil, starGift: nil, floodskipNumber: nil, starrefCommissionPermille: nil, starrefPeerId: nil, starrefAmount: nil), at: 0)
}
self.updateState(StarsContext.State(flags: [.isPendingBalance], balance: max(StarsAmount(value: 0, nanos: 0), state.balance + balance), subscriptions: state.subscriptions, canLoadMoreSubscriptions: state.canLoadMoreSubscriptions, transactions: transactions, canLoadMoreTransactions: state.canLoadMoreTransactions, isLoading: state.isLoading))
@ -552,10 +560,6 @@ private extension StarsContext.State.Transaction {
init?(apiTransaction: Api.StarsTransaction, peerId: EnginePeer.Id?, transaction: Transaction) {
switch apiTransaction {
case let .starsTransaction(apiFlags, id, stars, date, transactionPeer, title, description, photo, transactionDate, transactionUrl, _, messageId, extendedMedia, subscriptionPeriod, giveawayPostId, starGift, floodskipNumber, starrefCommissionPermille, starrefPeer, starrefAmount):
let _ = starrefCommissionPermille
let _ = starrefPeer
let _ = starrefAmount
let parsedPeer: StarsContext.State.Transaction.Peer
var paidMessageId: MessageId?
var giveawayMessageId: MessageId?
@ -611,7 +615,7 @@ private extension StarsContext.State.Transaction {
let media = extendedMedia.flatMap({ $0.compactMap { textMediaAndExpirationTimerFromApiMedia($0, PeerId(0)).media } }) ?? []
let _ = subscriptionPeriod
self.init(flags: flags, id: id, count: StarsAmount(apiAmount: stars), date: date, peer: parsedPeer, title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), transactionDate: transactionDate, transactionUrl: transactionUrl, paidMessageId: paidMessageId, giveawayMessageId: giveawayMessageId, media: media, subscriptionPeriod: subscriptionPeriod, starGift: starGift.flatMap { StarGift(apiStarGift: $0) }, floodskipNumber: floodskipNumber)
self.init(flags: flags, id: id, count: StarsAmount(apiAmount: stars), date: date, peer: parsedPeer, title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), transactionDate: transactionDate, transactionUrl: transactionUrl, paidMessageId: paidMessageId, giveawayMessageId: giveawayMessageId, media: media, subscriptionPeriod: subscriptionPeriod, starGift: starGift.flatMap { StarGift(apiStarGift: $0) }, floodskipNumber: floodskipNumber, starrefCommissionPermille: starrefCommissionPermille, starrefPeerId: starrefPeer.flatMap(\.peerId), starrefAmount: starrefAmount.flatMap(StarsAmount.init(apiAmount:)))
}
}
}
@ -686,6 +690,9 @@ public final class StarsContext {
public let subscriptionPeriod: Int32?
public let starGift: StarGift?
public let floodskipNumber: Int32?
public let starrefCommissionPermille: Int32?
public let starrefPeerId: PeerId?
public let starrefAmount: StarsAmount?
public init(
flags: Flags,
@ -703,7 +710,10 @@ public final class StarsContext {
media: [Media],
subscriptionPeriod: Int32?,
starGift: StarGift?,
floodskipNumber: Int32?
floodskipNumber: Int32?,
starrefCommissionPermille: Int32?,
starrefPeerId: PeerId?,
starrefAmount: StarsAmount?
) {
self.flags = flags
self.id = id
@ -721,6 +731,9 @@ public final class StarsContext {
self.subscriptionPeriod = subscriptionPeriod
self.starGift = starGift
self.floodskipNumber = floodskipNumber
self.starrefCommissionPermille = starrefCommissionPermille
self.starrefPeerId = starrefPeerId
self.starrefAmount = starrefAmount
}
public static func == (lhs: Transaction, rhs: Transaction) -> Bool {
@ -772,6 +785,15 @@ public final class StarsContext {
if lhs.floodskipNumber != rhs.floodskipNumber {
return false
}
if lhs.starrefCommissionPermille != rhs.starrefCommissionPermille {
return false
}
if lhs.starrefPeerId != rhs.starrefPeerId {
return false
}
if lhs.starrefAmount != rhs.starrefAmount {
return false
}
return true
}
}

View file

@ -180,7 +180,7 @@ private final class ChatMessageActionButtonNode: ASDisplayNode {
iconImage = incoming ? graphics.chatBubbleActionButtonIncomingMessageIconImage : graphics.chatBubbleActionButtonOutgoingMessageIconImage
case let .url(value):
var isApp = false
if isTelegramMeLink(value), let internalUrl = parseFullInternalUrl(sharedContext: context.sharedContext, url: value) {
if isTelegramMeLink(value), let internalUrl = parseFullInternalUrl(sharedContext: context.sharedContext, context: context, url: value) {
if case .peer(_, .appStart) = internalUrl {
isApp = true
} else if case .peer(_, .attachBotStart) = internalUrl {

View file

@ -798,7 +798,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
var useInlineHLS = true
if let data = context.currentAppConfiguration.with({ $0 }).data {
if let value = data["ios_inline_hls"] as? Double {
if let value = data["ios_inline_hls_v2"] as? Double {
useInlineHLS = value != 0.0
}
}

View file

@ -65,6 +65,12 @@ final class AffiliateProgramSetupScreenComponent: Component {
return true
}
private class ScrollView: UIScrollView {
override func touchesShouldCancel(in view: UIView) -> Bool {
return true
}
}
final class View: UIView, UIScrollViewDelegate {
private let scrollView: UIScrollView
@ -115,11 +121,11 @@ final class AffiliateProgramSetupScreenComponent: Component {
private var suggestedStarBotList: TelegramSuggestedStarRefBotList?
private var suggestedStarBotListDisposable: Disposable?
private var suggestedSortMode: TelegramSuggestedStarRefBotList.SortMode = .date
private var suggestedSortMode: TelegramSuggestedStarRefBotList.SortMode = .profitability
private var isSuggestedSortModeUpdating: Bool = false
override init(frame: CGRect) {
self.scrollView = UIScrollView()
self.scrollView = ScrollView()
self.scrollView.showsVerticalScrollIndicator = true
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.scrollsToTop = false
@ -170,10 +176,10 @@ final class AffiliateProgramSetupScreenComponent: Component {
let programPermille: Int32 = Int32(self.commissionPermille)
let programDuration: Int32? = self.durationValue == Int(Int32.max) ? nil : Int32(self.durationValue)
let commissionTitle: String = "\(programPermille / 10)%"
let commissionTitle: String = "\(formatPermille(programPermille))%"
let durationTitle: String
if let durationMonths = programDuration {
durationTitle = timeIntervalString(strings: environment.strings, value: durationMonths * (24 * 60 * 60))
durationTitle = timeIntervalString(strings: environment.strings, value: durationMonths * (30 * 24 * 60 * 60))
} else {
durationTitle = "Lifetime"
}
@ -362,17 +368,17 @@ If you end your affiliate program:
sourcePeer: bot.peer,
commissionPermille: bot.commissionPermille,
programDuration: bot.durationMonths,
revenuePerUser: bot.participants == 0 ? 0.0 : Double(bot.revenue) / Double(bot.participants),
mode: .active(JoinAffiliateProgramScreenMode.Active(
targetPeer: targetPeer,
link: bot.url,
userCount: Int(bot.participants),
copyLink: { [weak self] in
bot: bot,
copyLink: { [weak self] bot in
guard let self, let component = self.component else {
return
}
UIPasteboard.general.string = bot.url
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 })
self.environment?.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.environment?.controller()?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: "Link copied to clipboard", text: "Share this link and earn **\(formatPermille(bot.commissionPermille))%** of what people who use it spend in **\(bot.peer.compactDisplayTitle)**!"), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current)
}
))
))
@ -411,9 +417,9 @@ If you end your affiliate program:
var items: [ContextMenuItem] = []
let availableModes: [(TelegramSuggestedStarRefBotList.SortMode, String)] = [
(.date, "Date"),
(.profitability, "Profitability"),
(.revenue, "Revenue"),
(.profitability, "Profitability")
(.date, "Date")
]
for (mode, title) in availableModes {
let isSelected = mode == self.suggestedSortMode
@ -457,6 +463,20 @@ If you end your affiliate program:
controller.presentInGlobalOverlay(contextController)
}
private func openExistingAffiliatePrograms() {
guard let component = self.component else {
return
}
let _ = (component.context.sharedContext.makeAffiliateProgramSetupScreenInitialData(context: component.context, peerId: component.initialContent.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.environment?.controller()?.push(setupScreen)
})
}
func update(component: AffiliateProgramSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
@ -537,13 +557,15 @@ If you end your affiliate program:
self.commissionPermille = 10
self.commissionSliderValue = 0.0
self.commissionMinPermille = 10
self.durationValue = 10
self.durationValue = 1
self.durationMinValue = 0
}
} else {
self.commissionPermille = 10
self.commissionSliderValue = 0.0
self.commissionMinPermille = 10
self.durationValue = 10
self.durationValue = 1
self.durationMinValue = 0
}
case .connectedPrograms:
self.connectedStarBotListDisposable = (component.context.engine.peers.requestConnectedStarRefBots(
@ -855,7 +877,7 @@ If you end your affiliate program:
minValue: commissionMinSliderValue,
lowerBoundTitle: "1%",
upperBoundTitle: "90%",
title: "\(self.commissionPermille / 10)%",
title: "\(formatPermille(self.commissionPermille))%",
valueUpdated: { [weak self] value in
guard let self else {
return
@ -992,8 +1014,7 @@ If you end your affiliate program:
guard let self else {
return
}
let _ = self
self.openExistingAffiliatePrograms()
}
)))
],
@ -1164,11 +1185,11 @@ If you end your affiliate program:
for item in connectedStarBotList.items {
let durationTitle: String
if let durationMonths = item.durationMonths {
durationTitle = timeIntervalString(strings: environment.strings, value: durationMonths * (24 * 60 * 60))
durationTitle = timeIntervalString(strings: environment.strings, value: durationMonths * (30 * 24 * 60 * 60))
} else {
durationTitle = "Lifetime"
}
let commissionTitle = "\(item.commissionPermille / 10)%"
let commissionTitle = "\(formatPermille(item.commissionPermille))%"
let itemContextAction: (EnginePeer, ContextExtractedContentContainingView, ContextGesture?) -> Void = { [weak self] peer, sourceView, gesture in
guard let self, let component = self.component, let environment = self.environment else {
@ -1230,7 +1251,7 @@ If you end your affiliate program:
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 })
UIPasteboard.general.string = item.url
environment.controller()?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: "Link copied to clipboard", text: "Share this link and earn **\(item.commissionPermille / 10)%** of what people who use it spend in **\(item.peer.compactDisplayTitle)**!"), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current)
environment.controller()?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: "Link copied to clipboard", text: "Share this link and earn **\(formatPermille(item.commissionPermille))%** of what people who use it spend in **\(item.peer.compactDisplayTitle)**!"), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current)
})))
itemList.append(.action(ContextMenuActionItem(text: "Leave", textColor: .destructive, icon: { theme in
@ -1344,11 +1365,22 @@ If you end your affiliate program:
}
do {
var suggestedSectionItems: [AnyComponentWithIdentity<Empty>] = []
if suggestedStarBotListItems.isEmpty {
suggestedSectionItems.append(AnyComponentWithIdentity(id: "empty", component: AnyComponent(ZStack([
AnyComponentWithIdentity(id: 0, component: AnyComponent(Rectangle(color: .clear, width: nil, height: 100.0))),
AnyComponentWithIdentity(id: 1, component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: "No available programs yet.\nPlease check the page later.", font: Font.regular(15.0), textColor: environment.theme.list.itemSecondaryTextColor)),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.2
)))
]))))
}
for item in suggestedStarBotListItems {
let commissionTitle = "\(item.program.commissionPermille / 10)%"
let commissionTitle = "\(formatPermille(item.program.commissionPermille))%"
let durationTitle: String
if let durationMonths = item.program.durationMonths {
durationTitle = timeIntervalString(strings: environment.strings, value: durationMonths * (24 * 60 * 60))
durationTitle = timeIntervalString(strings: environment.strings, value: durationMonths * (30 * 24 * 60 * 60))
} else {
durationTitle = "Lifetime"
}
@ -1404,6 +1436,7 @@ If you end your affiliate program:
sourcePeer: botPeer,
commissionPermille: item.program.commissionPermille,
programDuration: item.program.durationMonths,
revenuePerUser: item.program.dailyRevenuePerUser?.totalValue ?? 0.0,
mode: .join(JoinAffiliateProgramScreenMode.Join(
initialTargetPeer: targetPeer,
canSelectTargetPeer: false,
@ -1440,31 +1473,34 @@ If you end your affiliate program:
))))
}
var suggestedHeaderItems: [AnyComponentWithIdentity<Empty>] = []
suggestedHeaderItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "PROGRAMS",
font: Font.regular(13.0),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
))))
if suggestedStarBotListItems.count > 1 {
suggestedHeaderItems.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(BotSectionSortButtonComponent(
theme: environment.theme,
strings: environment.strings,
sortMode: self.suggestedSortMode,
action: { [weak self] sourceView in
guard let self else {
return
}
self.openSortModeMenu(sourceView: sourceView)
}
))))
}
let suggestedProgramsSectionSize = self.suggestedProgramsSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
header: AnyComponent(HStack([
AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "PROGRAMS",
font: Font.regular(13.0),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
))),
AnyComponentWithIdentity(id: 1, component: AnyComponent(BotSectionSortButtonComponent(
theme: environment.theme,
strings: environment.strings,
sortMode: self.suggestedSortMode,
action: { [weak self] sourceView in
guard let self else {
return
}
self.openSortModeMenu(sourceView: sourceView)
}
)))
], spacing: 4.0, alignment: .alternatingLeftRight)),
header: AnyComponent(HStack(suggestedHeaderItems, spacing: 4.0, alignment: .alternatingLeftRight)),
footer: nil,
items: suggestedSectionItems,
displaySeparators: true
@ -1479,19 +1515,12 @@ If you end your affiliate program:
self.scrollView.addSubview(suggestedProgramsSectionView)
}
transition.setFrame(view: suggestedProgramsSectionView, frame: suggestedProgramsSectionFrame)
if !suggestedStarBotListItems.isEmpty {
suggestedProgramsSectionView.isHidden = false
} else {
suggestedProgramsSectionView.isHidden = true
}
suggestedProgramsSectionView.contentViewImpl.alpha = self.isSuggestedSortModeUpdating ? 0.6 : 1.0
suggestedProgramsSectionView.contentViewImpl.isUserInteractionEnabled = !self.isSuggestedSortModeUpdating
}
if !suggestedStarBotListItems.isEmpty {
contentHeight += suggestedProgramsSectionSize.height
contentHeight += sectionSpacing
}
contentHeight += suggestedProgramsSectionSize.height
contentHeight += sectionSpacing
}
}
}

View file

@ -28,6 +28,7 @@ private final class JoinAffiliateProgramScreenComponent: Component {
let sourcePeer: EnginePeer
let commissionPermille: Int32
let programDuration: Int32?
let revenuePerUser: Double
let mode: JoinAffiliateProgramScreen.Mode
init(
@ -35,12 +36,14 @@ private final class JoinAffiliateProgramScreenComponent: Component {
sourcePeer: EnginePeer,
commissionPermille: Int32,
programDuration: Int32?,
revenuePerUser: Double,
mode: JoinAffiliateProgramScreen.Mode
) {
self.context = context
self.sourcePeer = sourcePeer
self.commissionPermille = commissionPermille
self.programDuration = programDuration
self.revenuePerUser = revenuePerUser
self.mode = mode
}
@ -86,16 +89,18 @@ private final class JoinAffiliateProgramScreenComponent: Component {
private var toast: ComponentView<Empty>?
private let sourceAvatar = ComponentView<Empty>()
private let sourceAvatarBadge = ComponentView<Empty>()
private let targetAvatar = ComponentView<Empty>()
private let targetAvatarBadge = ComponentView<Empty>()
private let sourceTargetArrow = UIImageView()
private let linkIconBackground = ComponentView<Empty>()
private let linkIcon = ComponentView<Empty>()
private let linkIconBadge = ComponentView<Empty>()
private var linkIconBadge: ComponentView<Empty>?
private let title = ComponentView<Empty>()
private let subtitle = ComponentView<Empty>()
private var dailyRevenueText: ComponentView<Empty>?
private let titleTransformContainer: UIView
private let bottomPanelContainer: UIView
private let actionButton = ComponentView<Empty>()
@ -119,11 +124,16 @@ private final class JoinAffiliateProgramScreenComponent: Component {
private var topOffsetDistance: CGFloat?
private var currentTargetPeer: EnginePeer?
private var currentMode: JoinAffiliateProgramScreen.Mode?
private var possibleTargetPeers: [EnginePeer] = []
private var possibleTargetPeersDisposable: Disposable?
private var changeTargetPeerDisposable: Disposable?
private var isChangingTargetPeer: Bool = false
private var cachedCloseImage: UIImage?
private var inlineTextStarImage: UIImage?
override init(frame: CGRect) {
self.bottomOverscrollLimit = 200.0
@ -194,6 +204,7 @@ private final class JoinAffiliateProgramScreenComponent: Component {
deinit {
self.possibleTargetPeersDisposable?.dispose()
self.changeTargetPeerDisposable?.dispose()
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
@ -337,7 +348,7 @@ private final class JoinAffiliateProgramScreenComponent: Component {
guard let component = self.component, let environment = self.environment, let controller = environment.controller() else {
return
}
guard case let .join(join) = component.mode else {
guard let currentTargetPeer = self.currentTargetPeer else {
return
}
@ -346,7 +357,7 @@ private final class JoinAffiliateProgramScreenComponent: Component {
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 })
let peers: [EnginePeer] = self.possibleTargetPeers.isEmpty ? [
join.initialTargetPeer
currentTargetPeer
] : self.possibleTargetPeers
let avatarSize = CGSize(width: 30.0, height: 30.0)
@ -360,14 +371,80 @@ private final class JoinAffiliateProgramScreenComponent: Component {
} 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
let isSelected = peer.id == self.currentTargetPeer?.id
let accentColor = environment.theme.list.itemAccentColor
let avatarSignal = peerAvatarCompleteImage(account: component.context.account, peer: peer, size: avatarSize)
|> map { image in
let context = DrawingContext(size: avatarSize, scale: 0.0, clear: true)
context?.withContext { c in
UIGraphicsPushContext(c)
defer {
UIGraphicsPopContext()
}
if isSelected {
}
c.saveGState()
let scaleFactor = (avatarSize.width - 3.0 * 2.0) / avatarSize.width
if isSelected {
c.translateBy(x: avatarSize.width * 0.5, y: avatarSize.height * 0.5)
c.scaleBy(x: scaleFactor, y: scaleFactor)
c.translateBy(x: -avatarSize.width * 0.5, y: -avatarSize.height * 0.5)
}
if let image {
image.draw(in: CGRect(origin: CGPoint(), size: avatarSize))
}
c.restoreGState()
if isSelected {
c.setStrokeColor(accentColor.cgColor)
let lineWidth: CGFloat = 1.0 + UIScreenPixel
c.setLineWidth(lineWidth)
c.strokeEllipse(in: CGRect(origin: CGPoint(), size: avatarSize).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5))
}
}
return context?.generateImage()
}
items.append(.action(ContextMenuActionItem(text: peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder), textLayout: .secondLineWithValue(peerLabel), icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: avatarSignal), action: { [weak self] c, _ in
c?.dismiss(completion: {})
guard let self else {
guard let self, let currentMode = self.currentMode, let component = self.component else {
return
}
if self.currentTargetPeer?.id == peer.id {
return
}
self.currentTargetPeer = peer
switch currentMode {
case .join:
self.currentTargetPeer = peer
case let .active(active):
self.isChangingTargetPeer = true
self.changeTargetPeerDisposable?.dispose()
self.changeTargetPeerDisposable = (component.context.engine.peers.connectStarRefBot(id: peer.id, botId: component.sourcePeer.id)
|> deliverOnMainQueue).startStrict(next: { [weak self] result in
guard let self else {
return
}
self.isChangingTargetPeer = false
self.currentMode = .active(JoinAffiliateProgramScreen.Mode.Active(
targetPeer: peer,
bot: result,
copyLink: active.copyLink
))
self.state?.updated(transition: .immediate)
}, error: { [weak self] _ in
guard let self else {
return
}
self.isChangingTargetPeer = false
self.state?.updated(transition: .immediate)
})
}
self.state?.updated(transition: .immediate)
})))
}
@ -389,7 +466,11 @@ private final class JoinAffiliateProgramScreenComponent: Component {
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
let currentMode = self.currentMode ?? component.mode
if self.component == nil {
self.currentMode = component.mode
var loadPossibleTargetPeers = false
switch component.mode {
case let .join(join):
@ -461,7 +542,7 @@ private final class JoinAffiliateProgramScreenComponent: Component {
let clippingY: CGFloat
if let currentTargetPeer = self.currentTargetPeer, case .join = component.mode {
if let currentTargetPeer = self.currentTargetPeer, case .join = currentMode {
contentHeight += 34.0
let sourceAvatarSize = self.sourceAvatar.update(
@ -501,25 +582,71 @@ private final class JoinAffiliateProgramScreenComponent: Component {
transition.setFrame(view: targetAvatarView, frame: targetAvatarFrame)
}
let badgeIconInset: CGFloat = 2.0
if component.revenuePerUser != 0.0 {
var revenueString = String(format: "%.1f", component.revenuePerUser)
if revenueString.hasSuffix(".0") {
revenueString = String(revenueString[revenueString.startIndex ..< revenueString.index(revenueString.endIndex, offsetBy: -2)])
}
let sourceAvatarBadgeSize = self.sourceAvatarBadge.update(
transition: transition,
component: AnyComponent(BorderedBadgeComponent(
backgroundColor: environment.theme.list.itemDisclosureActions.constructive.fillColor,
cutoutColor: environment.theme.list.plainBackgroundColor,
content: AnyComponent(HStack([
AnyComponentWithIdentity(id: 0, component: AnyComponent(TransformContents(
content: AnyComponent(BundleIconComponent(
name: "Premium/PremiumStar",
tintColor: environment.theme.list.itemDisclosureActions.constructive.foregroundColor,
scaleFactor: 0.58
)),
fixedSize: CGSize(width: 13.0, height: 10.0),
translation: CGPoint(x: 0.0, y: 1.0 + UIScreenPixel)
))),
AnyComponentWithIdentity(id: 1, component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: revenueString, font: Font.regular(13.0), textColor: environment.theme.list.itemDisclosureActions.constructive.foregroundColor))
)))
], spacing: 2.0)),
insets: UIEdgeInsets(top: 3.0, left: 6.0, bottom: 3.0, right: 6.0),
cutoutWidth: 1.0 + UIScreenPixel)
),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
let sourceAvatarBadgeFrame = CGRect(origin: CGPoint(x: sourceAvatarFrame.minX + floor((sourceAvatarFrame.width - sourceAvatarBadgeSize.width) * 0.5), y: sourceAvatarFrame.maxY - 7.0 - floor(sourceAvatarBadgeSize.height * 0.5)), size: sourceAvatarBadgeSize)
if let sourceAvatarBadgeView = self.sourceAvatarBadge.view {
if sourceAvatarBadgeView.superview == nil {
self.scrollContentView.addSubview(sourceAvatarBadgeView)
}
transition.setFrame(view: sourceAvatarBadgeView, frame: sourceAvatarBadgeFrame)
}
}
let targetAvatarBadgeSize = self.targetAvatarBadge.update(
transition: transition,
component: AnyComponent(BorderedBadgeComponent(
backgroundColor: UIColor(rgb: 0x8A7AFF),
backgroundColor: environment.theme.list.itemCheckColors.fillColor,
cutoutColor: environment.theme.list.plainBackgroundColor,
content: AnyComponent(BundleIconComponent(
name: "Premium/PremiumStar",
tintColor: .white,
scaleFactor: 0.95
)),
insets: UIEdgeInsets(top: badgeIconInset, left: badgeIconInset, bottom: badgeIconInset, right: badgeIconInset),
aspect: 1.0,
cutoutWidth: 1.0 + UIScreenPixel
)),
content: AnyComponent(HStack([
AnyComponentWithIdentity(id: 0, component: AnyComponent(TransformContents(
content: AnyComponent(BundleIconComponent(
name: "Media Editor/Link",
tintColor: environment.theme.list.itemCheckColors.foregroundColor,
scaleFactor: 0.75
)),
translation: CGPoint(x: 0.0, y: 0.0)
))
),
AnyComponentWithIdentity(id: 1, component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: "\(formatPermille(component.commissionPermille))%", font: Font.regular(13.0), textColor: environment.theme.list.itemCheckColors.foregroundColor))
)))
], spacing: 2.0)),
insets: UIEdgeInsets(top: 3.0, left: 6.0, bottom: 3.0, right: 6.0),
cutoutWidth: 1.0 + UIScreenPixel)
),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
let targetAvatarBadgeFrame = CGRect(origin: CGPoint(x: targetAvatarFrame.maxX + 3.0 - targetAvatarBadgeSize.width, y: targetAvatarFrame.maxY + 3.0 - targetAvatarBadgeSize.height), size: targetAvatarBadgeSize)
let targetAvatarBadgeFrame = CGRect(origin: CGPoint(x: targetAvatarFrame.minX + floor((targetAvatarFrame.width - targetAvatarBadgeSize.width) * 0.5), y: targetAvatarFrame.maxY - 7.0 - floor(targetAvatarBadgeSize.height * 0.5)), size: targetAvatarBadgeSize)
if let targetAvatarBadgeView = self.targetAvatarBadge.view {
if targetAvatarBadgeView.superview == nil {
self.scrollContentView.addSubview(targetAvatarBadgeView)
@ -553,7 +680,7 @@ private final class JoinAffiliateProgramScreenComponent: Component {
}
transition.setFrame(view: self.sourceTargetArrow, frame: sourceTargetArrowFrame)
}
} else if case let .active(active) = component.mode {
} else if case let .active(active) = currentMode {
contentHeight += 31.0
let linkIconBackgroundSize = self.linkIconBackground.update(
@ -592,8 +719,18 @@ private final class JoinAffiliateProgramScreenComponent: Component {
transition.setFrame(view: linkIconView, frame: linkIconFrame)
}
if active.userCount != 0 {
let linkIconBadgeSize = self.linkIconBadge.update(
if active.bot.participants != 0 {
let linkIconBadge: ComponentView<Empty>
var linkIconBadgeTransition = transition
if let current = self.linkIconBadge {
linkIconBadge = current
} else {
linkIconBadgeTransition = linkIconBadgeTransition.withAnimation(.none)
linkIconBadge = ComponentView()
self.linkIconBadge = linkIconBadge
}
let linkIconBadgeSize = linkIconBadge.update(
transition: .immediate,
component: AnyComponent(BorderedBadgeComponent(
backgroundColor: UIColor(rgb: 0x34C759),
@ -605,7 +742,7 @@ private final class JoinAffiliateProgramScreenComponent: Component {
scaleFactor: 1.0
))),
AnyComponentWithIdentity(id: 1, component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: "\(active.userCount)", font: Font.bold(14.0), textColor: .white))
text: .plain(NSAttributedString(string: "\(active.bot.participants)", font: Font.bold(14.0), textColor: .white))
)))
], spacing: 4.0)),
insets: UIEdgeInsets(top: 4.0, left: 9.0, bottom: 4.0, right: 8.0),
@ -615,18 +752,21 @@ private final class JoinAffiliateProgramScreenComponent: Component {
containerSize: CGSize(width: 100.0, height: 100.0)
)
let linkIconBadgeFrame = CGRect(origin: CGPoint(x: linkIconBackgroundFrame.minX + floor((linkIconBackgroundFrame.width - linkIconBadgeSize.width) * 0.5), y: linkIconBackgroundFrame.maxY - floor(linkIconBadgeSize.height * 0.5)), size: linkIconBadgeSize)
if let linkIconBadgeView = self.linkIconBadge.view {
if let linkIconBadgeView = linkIconBadge.view {
if linkIconBadgeView.superview == nil {
self.scrollContentView.addSubview(linkIconBadgeView)
}
transition.setFrame(view: linkIconBadgeView, frame: linkIconBadgeFrame)
linkIconBadgeTransition.setFrame(view: linkIconBadgeView, frame: linkIconBadgeFrame)
}
} else if let linkIconBadge = self.linkIconBadge {
self.linkIconBadge = nil
linkIconBadge.view?.removeFromSuperview()
}
contentHeight += linkIconBackgroundSize.height + 21.0
}
let commissionTitle = "\(component.commissionPermille / 10)%"
let commissionTitle = "\(formatPermille(component.commissionPermille))%"
let durationTitle: String
if let durationMonths = component.programDuration {
durationTitle = timeIntervalString(strings: environment.strings, value: durationMonths * (24 * 60 * 60))
@ -635,12 +775,22 @@ private final class JoinAffiliateProgramScreenComponent: Component {
}
let titleString: String
let subtitleString: String
var subtitleString: String
var dailyRevenueString: String?
let termsString: String
switch component.mode {
switch currentMode {
case .join:
titleString = "Affiliate Program"
subtitleString = "**\(component.sourcePeer.compactDisplayTitle)** will share **\(commissionTitle)** of the revenue from each user you refer to it for **\(durationTitle)**."
if component.revenuePerUser != 0.0 {
var revenueString = String(format: "%.1f", component.revenuePerUser)
if revenueString.hasSuffix(".0") {
revenueString = String(revenueString[revenueString.startIndex ..< revenueString.index(revenueString.endIndex, offsetBy: -2)])
}
dailyRevenueString = "Daily revenue per user: #**\(revenueString)**"
}
termsString = "By joining this program, you afree to the [terms and conditions](https://telegram.org/terms) of Affiliate Programs."
case let .active(active):
titleString = "Referral Link"
@ -651,12 +801,12 @@ private final class JoinAffiliateProgramScreenComponent: Component {
timeString = "for **\(durationTitle)** after they follow your link."
}
subtitleString = "Share this link with your users to earn a **\(commissionTitle)** commission on their spending in **\(component.sourcePeer.compactDisplayTitle)** \(timeString)."
if active.userCount == 0 {
if active.bot.participants == 0 {
termsString = "No one opened \(component.sourcePeer.compactDisplayTitle) through this link yet."
} else if active.userCount == 1 {
} else if active.bot.participants == 1 {
termsString = "1 user opened \(component.sourcePeer.compactDisplayTitle) through this link."
} else {
termsString = "\(active.userCount) users opened \(component.sourcePeer.compactDisplayTitle) through this link."
termsString = "\(active.bot.participants) users opened \(component.sourcePeer.compactDisplayTitle) through this link."
}
}
let titleSize = self.title.update(
@ -711,11 +861,119 @@ private final class JoinAffiliateProgramScreenComponent: Component {
transition.setPosition(view: subtitleView, position: subtitleFrame.center)
subtitleView.bounds = CGRect(origin: CGPoint(), size: subtitleFrame.size)
}
contentHeight += subtitleSize.height + 23.0
contentHeight += subtitleSize.height
if let dailyRevenueString {
let dailyRevenueText: ComponentView<Empty>
if let current = self.dailyRevenueText {
dailyRevenueText = current
} else {
dailyRevenueText = ComponentView()
self.dailyRevenueText = dailyRevenueText
}
var inlineTextStarImage: UIImage?
if let current = self.inlineTextStarImage {
inlineTextStarImage = current
} else {
if let image = UIImage(bundleImageName: "Premium/Stars/StarSmall") {
let starInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
inlineTextStarImage = generateImage(CGSize(width: starInsets.left + image.size.width + starInsets.right, height: image.size.height), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
defer {
UIGraphicsPopContext()
}
image.draw(at: CGPoint(x: starInsets.left, y: starInsets.top))
})?.withRenderingMode(.alwaysOriginal)
self.inlineTextStarImage = inlineTextStarImage
}
}
let attributedDailyRevenueString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(dailyRevenueString, attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor),
bold: MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.itemPrimaryTextColor),
link: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemAccentColor),
linkAttribute: { url in
return ("URL", url)
}
), textAlignment: .center))
if let range = attributedDailyRevenueString.string.range(of: "#"), let starImage = inlineTextStarImage {
final class RunDelegateData {
let ascent: CGFloat
let descent: CGFloat
let width: CGFloat
init(ascent: CGFloat, descent: CGFloat, width: CGFloat) {
self.ascent = ascent
self.descent = descent
self.width = width
}
}
let runDelegateData = RunDelegateData(
ascent: Font.regular(15.0).ascender,
descent: Font.regular(15.0).descender,
width: starImage.size.width + 2.0
)
var callbacks = CTRunDelegateCallbacks(
version: kCTRunDelegateCurrentVersion,
dealloc: { dataRef in
Unmanaged<RunDelegateData>.fromOpaque(dataRef).release()
},
getAscent: { dataRef in
let data = Unmanaged<RunDelegateData>.fromOpaque(dataRef)
return data.takeUnretainedValue().ascent
},
getDescent: { dataRef in
let data = Unmanaged<RunDelegateData>.fromOpaque(dataRef)
return data.takeUnretainedValue().descent
},
getWidth: { dataRef in
let data = Unmanaged<RunDelegateData>.fromOpaque(dataRef)
return data.takeUnretainedValue().width
}
)
if let runDelegate = CTRunDelegateCreate(&callbacks, Unmanaged.passRetained(runDelegateData).toOpaque()) {
attributedDailyRevenueString.addAttribute(NSAttributedString.Key(kCTRunDelegateAttributeName as String), value: runDelegate, range: NSRange(range, in: attributedDailyRevenueString.string))
}
attributedDailyRevenueString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: attributedDailyRevenueString.string))
attributedDailyRevenueString.addAttribute(.foregroundColor, value: UIColor(rgb: 0xffffff), range: NSRange(range, in: attributedDailyRevenueString.string))
attributedDailyRevenueString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: attributedDailyRevenueString.string))
}
let dailyRevenueTextSize = dailyRevenueText.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(attributedDailyRevenueString),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.2
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
contentHeight += 16.0
let dailyRevenueTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - dailyRevenueTextSize.width) * 0.5), y: contentHeight), size: dailyRevenueTextSize)
if let dailyRevenueTextView = dailyRevenueText.view {
if dailyRevenueTextView.superview == nil {
self.scrollContentView.addSubview(dailyRevenueTextView)
}
transition.setPosition(view: dailyRevenueTextView, position: dailyRevenueTextFrame.center)
dailyRevenueTextView.bounds = CGRect(origin: CGPoint(), size: dailyRevenueTextFrame.size)
}
contentHeight += dailyRevenueTextSize.height
} else if let dailyRevenueText = self.dailyRevenueText {
self.dailyRevenueText = nil
dailyRevenueText.view?.removeFromSuperview()
}
contentHeight += 23.0
var displayTargetPeer = false
var isTargetPeerSelectable = false
switch component.mode {
switch currentMode {
case let .join(join):
displayTargetPeer = join.canSelectTargetPeer
isTargetPeerSelectable = join.canSelectTargetPeer
@ -777,8 +1035,8 @@ private final class JoinAffiliateProgramScreenComponent: Component {
}
contentHeight += 12.0
if case let .active(active) = component.mode {
var cleanLink = active.link
if case let .active(active) = currentMode {
var cleanLink = active.bot.url
let removePrefixes: [String] = ["http://", "https://"]
for prefix in removePrefixes {
if cleanLink.hasPrefix(prefix) {
@ -801,11 +1059,11 @@ private final class JoinAffiliateProgramScreenComponent: Component {
minSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0),
contentInsets: UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 10.0),
action: { [weak self] in
guard let self, let component = self.component, case let .active(active) = component.mode else {
guard let self, case let .active(active) = self.currentMode else {
return
}
self.environment?.controller()?.dismiss()
active.copyLink()
active.copyLink(active.bot)
},
animateAlpha: true,
animateScale: false,
@ -820,13 +1078,14 @@ private final class JoinAffiliateProgramScreenComponent: Component {
self.scrollContentView.addSubview(linkTextView)
}
transition.setFrame(view: linkTextView, frame: linkTextFrame)
transition.setAlpha(view: linkTextView, alpha: self.isChangingTargetPeer ? 0.6 : 1.0)
}
contentHeight += linkTextSize.height
contentHeight += 24.0
}
let actionButtonTitle: String
switch component.mode {
switch currentMode {
case .join:
actionButtonTitle = "Join Program"
case .active:
@ -853,18 +1112,18 @@ private final class JoinAffiliateProgramScreenComponent: Component {
isEnabled: true,
displaysProgress: false,
action: { [weak self] in
guard let self, let component = self.component else {
guard let self, let currentMode = self.currentMode else {
return
}
self.environment?.controller()?.dismiss()
switch component.mode {
switch currentMode {
case let .join(join):
if let currentTargetPeer = self.currentTargetPeer {
join.completion(currentTargetPeer)
}
case let .active(active):
active.copyLink()
active.copyLink(active.bot)
}
}
)),
@ -928,7 +1187,7 @@ private final class JoinAffiliateProgramScreenComponent: Component {
self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, bottomInset: environment.safeInsets.bottom, topInset: topInset)
if case .active = component.mode {
if case .active = currentMode {
let toast: ComponentView<Empty>
if let current = self.toast {
toast = current
@ -1027,6 +1286,7 @@ public class JoinAffiliateProgramScreen: ViewControllerComponentContainer {
sourcePeer: EnginePeer,
commissionPermille: Int32,
programDuration: Int32?,
revenuePerUser: Double,
mode: Mode
) {
self.context = context
@ -1036,6 +1296,7 @@ public class JoinAffiliateProgramScreen: ViewControllerComponentContainer {
sourcePeer: sourcePeer,
commissionPermille: commissionPermille,
programDuration: programDuration,
revenuePerUser: revenuePerUser,
mode: mode
), navigationBarAppearance: .none)

View file

@ -1430,17 +1430,28 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
if let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) {
} else {
if let starRefProgram = cachedData.starRefProgram, starRefProgram.endDate == nil {
if items[.botAffiliateProgram] == nil {
items[.botAffiliateProgram] = []
var canJoinRefProgram = false
if let data = context.currentAppConfiguration.with({ $0 }).data, let value = data["starref_connect_allowed"] {
if let value = value as? Double {
canJoinRefProgram = value != 0.0
} else if let value = value as? Bool {
canJoinRefProgram = value
}
}
if canJoinRefProgram {
if items[.botAffiliateProgram] == nil {
items[.botAffiliateProgram] = []
}
//TODO:localize
let programTitleValue: String
programTitleValue = "\(formatPermille(starRefProgram.commissionPermille))%"
//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 \(formatPermille(starRefProgram.commissionPermille))% of their spending there."))
}
//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."))
}
}
}
@ -1938,16 +1949,28 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL
items[.peerDataSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemUsername, label: .text("@\(user.addressName ?? "")"), text: presentationData.strings.PeerInfo_Bot_Username, icon: PresentationResourcesSettings.bot, action: {
interaction.editingOpenPublicLinkSetup()
}))
//TODO:localize
let programTitleValue: PeerInfoScreenDisclosureItem.Label
if let cachedData = data.cachedData as? CachedUserData, let starRefProgram = cachedData.starRefProgram, starRefProgram.endDate == nil {
programTitleValue = .labelBadge("\(starRefProgram.commissionPermille / 10)%")
} else {
programTitleValue = .text("Off")
var canSetupRefProgram = false
if let data = context.currentAppConfiguration.with({ $0 }).data, let value = data["starref_program_allowed"] {
if let value = value as? Double {
canSetupRefProgram = value != 0.0
} else if let value = value as? Bool {
canSetupRefProgram = value
}
}
if canSetupRefProgram {
//TODO:localize
let programTitleValue: PeerInfoScreenDisclosureItem.Label
if let cachedData = data.cachedData as? CachedUserData, let starRefProgram = cachedData.starRefProgram, starRefProgram.endDate == nil {
programTitleValue = .labelBadge("\(formatPermille(starRefProgram.commissionPermille))%")
} else {
programTitleValue = .text("Off")
}
items[.peerDataSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemAffiliateProgram, label: programTitleValue, additionalBadgeLabel: presentationData.strings.Settings_New, text: "Affiliate Program", icon: PresentationResourcesSettings.affiliateProgram, action: {
interaction.editingOpenAffiliateProgram()
}))
}
items[.peerDataSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemAffiliateProgram, label: programTitleValue, additionalBadgeLabel: presentationData.strings.Settings_New, text: "Affiliate Program", icon: PresentationResourcesSettings.affiliateProgram, action: {
interaction.editingOpenAffiliateProgram()
}))
items[.peerSettings]!.append(PeerInfoScreenActionItem(id: ItemIntro, text: presentationData.strings.PeerInfo_Bot_EditIntro, icon: UIImage(bundleImageName: "Peer Info/BotIntro"), action: {
interaction.openPeerMention("botfather", .withBotStartPayload(ChatControllerInitialBotStart(payload: "\(user.addressName ?? "")-intro", behavior: .interactive)))
@ -2199,9 +2222,20 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL
}
if channel.hasPermission(.changeInfo) {
items[.peerAdditionalSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemAffiliatePrograms, label: .text(""), additionalBadgeLabel: presentationData.strings.Settings_New, text: "Affiliate Programs", icon: PresentationResourcesSettings.affiliateProgram, action: {
interaction.editingOpenAffiliateProgram()
}))
var canJoinRefProgram = false
if let data = context.currentAppConfiguration.with({ $0 }).data, let value = data["starref_connect_allowed"] {
if let value = value as? Double {
canJoinRefProgram = value != 0.0
} else if let value = value as? Bool {
canJoinRefProgram = value
}
}
if canJoinRefProgram {
items[.peerAdditionalSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemAffiliatePrograms, label: .text(""), additionalBadgeLabel: presentationData.strings.Settings_New, text: "Affiliate Programs", icon: PresentationResourcesSettings.affiliateProgram, action: {
interaction.editingOpenAffiliateProgram()
}))
}
}
if isCreator { //if let cachedData = data.cachedData as? CachedChannelData, cachedData.flags.contains(.canDeleteHistory) {
@ -8596,7 +8630,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
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
|> deliverOnMainQueue).startStandalone(next: { [weak self] initialData in
guard let self else {
return
}
@ -8620,16 +8654,15 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
if let result {
mode = .active(JoinAffiliateProgramScreenMode.Active(
targetPeer: accountPeer,
link: result.url,
userCount: Int(result.participants),
copyLink: { [weak self] in
bot: result,
copyLink: { [weak self] result 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)
self.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: "Link copied to clipboard", text: "Share this link and earn **\(formatPermille(result.commissionPermille))%** of what people who use it spend in **\(result.peer.compactDisplayTitle)**!"), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current)
}
))
} else {
@ -8652,17 +8685,17 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
sourcePeer: bot.peer,
commissionPermille: bot.commissionPermille,
programDuration: bot.durationMonths,
revenuePerUser: bot.participants == 0 ? 0.0 : Double(bot.revenue) / Double(bot.participants),
mode: .active(JoinAffiliateProgramScreenMode.Active(
targetPeer: targetPeer,
link: bot.url,
userCount: Int(bot.participants),
copyLink: { [weak self] in
bot: bot,
copyLink: { [weak self] result in
guard let self else {
return
}
UIPasteboard.general.string = bot.url
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 **\(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?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: "Link copied to clipboard", text: "Share this link and earn **\(formatPermille(result.commissionPermille))%** of what people who use it spend in **\(result.peer.compactDisplayTitle)**!"), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current)
}
))
))
@ -8675,6 +8708,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
sourcePeer: .user(peer),
commissionPermille: starRefProgram.commissionPermille,
programDuration: starRefProgram.durationMonths,
revenuePerUser: starRefProgram.dailyRevenuePerUser?.totalValue ?? 0.0,
mode: mode
))
})

View file

@ -33,7 +33,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
let context: AccountContext
let subject: StarsTransactionScreen.Subject
let cancel: (Bool) -> Void
let openPeer: (EnginePeer) -> Void
let openPeer: (EnginePeer, Bool) -> Void
let openMessage: (EngineMessage.Id) -> Void
let openMedia: ([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void
let openAppExamples: () -> Void
@ -44,7 +44,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
context: AccountContext,
subject: StarsTransactionScreen.Subject,
cancel: @escaping (Bool) -> Void,
openPeer: @escaping (EnginePeer) -> Void,
openPeer: @escaping (EnginePeer, Bool) -> Void,
openMessage: @escaping (EngineMessage.Id) -> Void,
openMedia: @escaping ([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void,
openAppExamples: @escaping () -> Void,
@ -95,6 +95,9 @@ private final class StarsTransactionSheetContent: CombinedComponent {
if case let .peer(peer) = transaction.peer {
peerIds.append(peer.id)
}
if let starrefPeerId = transaction.starrefPeerId {
peerIds.append(starrefPeerId)
}
case let .receipt(receipt):
peerIds.append(receipt.botPaymentId)
case let .gift(message):
@ -232,6 +235,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
var giveawayMessageId: MessageId?
var isBoost = false
var giftAnimation: TelegramMediaFile?
var isRefProgram = false
var delayedCloseOnOpenPeer = true
switch subject {
@ -399,6 +403,23 @@ private final class StarsTransactionSheetContent: CombinedComponent {
}
transactionPeer = transaction.peer
isGift = true
} else if let starrefCommissionPermille = transaction.starrefCommissionPermille {
//TODO:localize
isRefProgram = true
if transaction.starrefPeerId == nil {
titleText = "\(formatPermille(starrefCommissionPermille))% Commission"
} else {
titleText = transaction.title ?? "Product"
}
descriptionText = ""
count = transaction.count
countOnTop = false
transactionId = transaction.id
date = transaction.date
transactionPeer = transaction.peer
if case let .peer(peer) = transaction.peer {
toPeer = peer
}
} else if transaction.flags.contains(.isReaction) {
titleText = strings.Stars_Transaction_Reaction_Title
descriptionText = ""
@ -722,7 +743,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
)
)
))
} else if let toPeer {
} else if let toPeer, !isRefProgram {
let title: String
if isSubscription {
if isBotSubscription {
@ -751,7 +772,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
),
action: {
if delayedCloseOnOpenPeer {
component.openPeer(toPeer)
component.openPeer(toPeer, false)
Queue.mainQueue().after(1.0, {
component.cancel(false)
})
@ -845,6 +866,133 @@ private final class StarsTransactionSheetContent: CombinedComponent {
)
))
}
if case let .transaction(transaction, _) = subject {
//TODO:localize
if transaction.starrefCommissionPermille != nil {
if transaction.starrefPeerId == nil {
tableItems.append(.init(
id: "reason",
title: "Reason",
component: AnyComponent(
Button(
content: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: "Affiliate Program", font: tableFont, textColor: tableLinkColor))
)),
action: {
if let toPeer {
component.openPeer(toPeer, true)
Queue.mainQueue().after(1.0, {
component.cancel(false)
})
}
}
)
),
insets: UIEdgeInsets(top: 0.0, left: 12.0, bottom: 0.0, right: 5.0)
))
}
if let toPeer, transaction.starrefPeerId == nil {
tableItems.append(.init(
id: "miniapp",
title: "Mini App",
component: AnyComponent(
Button(
content: AnyComponent(
PeerCellComponent(
context: component.context,
theme: theme,
peer: toPeer
)
),
action: {
if delayedCloseOnOpenPeer {
component.openPeer(toPeer, false)
Queue.mainQueue().after(1.0, {
component.cancel(false)
})
} else {
if let controller = controller() as? StarsTransactionScreen, let navigationController = controller.navigationController, let chatController = navigationController.viewControllers.first(where: { $0 is ChatController }) as? ChatController {
chatController.playShakeAnimation()
}
component.cancel(true)
}
}
)
)
))
}
}
if let starRefPeerId = transaction.starrefPeerId, let starRefPeer = state.peerMap[starRefPeerId] {
//TODO:localize
tableItems.append(.init(
id: "to",
title: "Affiliate",
component: AnyComponent(
Button(
content: AnyComponent(
PeerCellComponent(
context: component.context,
theme: theme,
peer: starRefPeer
)
),
action: {
if delayedCloseOnOpenPeer {
component.openPeer(starRefPeer, false)
Queue.mainQueue().after(1.0, {
component.cancel(false)
})
} else {
if let controller = controller() as? StarsTransactionScreen, let navigationController = controller.navigationController, let chatController = navigationController.viewControllers.first(where: { $0 is ChatController }) as? ChatController {
chatController.playShakeAnimation()
}
component.cancel(true)
}
}
)
)
))
if let toPeer {
tableItems.append(.init(
id: "referred",
title: "Referred User",
component: AnyComponent(
Button(
content: AnyComponent(
PeerCellComponent(
context: component.context,
theme: theme,
peer: toPeer
)
),
action: {
if delayedCloseOnOpenPeer {
component.openPeer(toPeer, true)
Queue.mainQueue().after(1.0, {
component.cancel(false)
})
} else {
if let controller = controller() as? StarsTransactionScreen, let navigationController = controller.navigationController, let chatController = navigationController.viewControllers.first(where: { $0 is ChatController }) as? ChatController {
chatController.playShakeAnimation()
}
component.cancel(true)
}
}
)
)
))
}
}
if let starrefCommissionPermille = transaction.starrefCommissionPermille, transaction.starrefPeerId != nil {
tableItems.append(.init(
id: "commission",
title: "Commission",
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: "\(formatPermille(starrefCommissionPermille))%", font: tableFont, textColor: tableTextColor))
)),
insets: UIEdgeInsets(top: 0.0, left: 12.0, bottom: 0.0, right: 5.0)
))
}
}
if let transactionId {
tableItems.append(.init(
@ -1200,7 +1348,7 @@ private final class StarsTransactionSheetComponent: CombinedComponent {
let context: AccountContext
let subject: StarsTransactionScreen.Subject
let openPeer: (EnginePeer) -> Void
let openPeer: (EnginePeer, Bool) -> Void
let openMessage: (EngineMessage.Id) -> Void
let openMedia: ([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void
let openAppExamples: () -> Void
@ -1210,7 +1358,7 @@ private final class StarsTransactionSheetComponent: CombinedComponent {
init(
context: AccountContext,
subject: StarsTransactionScreen.Subject,
openPeer: @escaping (EnginePeer) -> Void,
openPeer: @escaping (EnginePeer, Bool) -> Void,
openMessage: @escaping (EngineMessage.Id) -> Void,
openMedia: @escaping ([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void,
openAppExamples: @escaping () -> Void,
@ -1363,7 +1511,7 @@ public class StarsTransactionScreen: ViewControllerComponentContainer {
) {
self.context = context
var openPeerImpl: ((EnginePeer) -> Void)?
var openPeerImpl: ((EnginePeer, Bool) -> Void)?
var openMessageImpl: ((EngineMessage.Id) -> Void)?
var openMediaImpl: (([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void)?
var openAppExamplesImpl: (() -> Void)?
@ -1375,8 +1523,8 @@ public class StarsTransactionScreen: ViewControllerComponentContainer {
component: StarsTransactionSheetComponent(
context: context,
subject: subject,
openPeer: { peerId in
openPeerImpl?(peerId)
openPeer: { peerId, isProfile in
openPeerImpl?(peerId, isProfile)
},
openMessage: { messageId in
openMessageImpl?(messageId)
@ -1402,7 +1550,7 @@ public class StarsTransactionScreen: ViewControllerComponentContainer {
self.navigationPresentation = .flatModal
self.automaticallyControlPresentationContextLayout = false
openPeerImpl = { [weak self] peer in
openPeerImpl = { [weak self] peer, isProfile in
guard let self, let navigationController = self.navigationController as? NavigationController else {
return
}
@ -1415,7 +1563,13 @@ public class StarsTransactionScreen: ViewControllerComponentContainer {
guard let peer else {
return
}
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, chatController: nil, context: context, chatLocation: .peer(peer), subject: nil, botStart: nil, updateTextInputState: nil, keepStack: .always, useExisting: true, purposefulAction: nil, scrollToEndIfExists: false, activateMessageSearch: nil, animated: true))
if isProfile {
if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
navigationController.pushViewController(controller)
}
} else {
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, chatController: nil, context: context, chatLocation: .peer(peer), subject: nil, botStart: nil, updateTextInputState: nil, keepStack: .always, useExisting: true, purposefulAction: nil, scrollToEndIfExists: false, activateMessageSearch: nil, animated: true))
}
})
}

View file

@ -662,46 +662,57 @@ final class StarsTransactionsScreenComponent: Component {
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
var canJoinRefProgram = false
if let data = component.context.currentAppConfiguration.with({ $0 }).data, let value = data["starref_connect_allowed"] {
if let value = value as? Double {
canJoinRefProgram = value != 0.0
} else if let value = value as? Bool {
canJoinRefProgram = value
}
}
if canJoinRefProgram {
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 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)
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)
}
starTransition.setFrame(view: earnStarsSectionView, frame: earnStarsSectionFrame)
contentHeight += earnStarsSectionSize.height
contentHeight += 44.0
}
contentHeight += earnStarsSectionSize.height
contentHeight += 44.0
let fontBaseDisplaySize = 17.0
var subscriptionsItems: [AnyComponentWithIdentity<Empty>] = []

View file

@ -118,7 +118,7 @@ func openWebAppImpl(
var fullSize = false
var isFullscreen = false
if isTelegramMeLink(url), let internalUrl = parseFullInternalUrl(sharedContext: context.sharedContext, url: url), case .peer(_, .appStart) = internalUrl {
if isTelegramMeLink(url), let internalUrl = parseFullInternalUrl(sharedContext: context.sharedContext, context: context, url: url), case .peer(_, .appStart) = internalUrl {
if url.contains("mode=fullscreen") {
isFullscreen = true
fullSize = true

View file

@ -1147,7 +1147,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
self.allAdMessages = (messages.first, [], 0)
} else {
var adPeerName: String?
if let adAttribute = messages.first?.adAttribute, let parsedUrl = parseAdUrl(sharedContext: self.context.sharedContext, url: adAttribute.url), case let .peer(reference, _) = parsedUrl, case let .name(peerName) = reference {
if let adAttribute = messages.first?.adAttribute, let parsedUrl = parseAdUrl(sharedContext: self.context.sharedContext, context: self.context, url: adAttribute.url), case let .peer(reference, _) = parsedUrl, case let .name(peerName) = reference {
adPeerName = peerName
}

View file

@ -114,7 +114,7 @@ public func parseConfirmationCodeUrl(sharedContext: SharedAccountContext, url: U
}
}
if url.scheme == "tg" {
if let host = url.host, let query = url.query, let parsedUrl = parseInternalUrl(sharedContext: sharedContext, query: host + "?" + query) {
if let host = url.host, let query = url.query, let parsedUrl = parseInternalUrl(sharedContext: sharedContext, context: nil, query: host + "?" + query) {
switch parsedUrl {
case let .confirmationCode(code):
return code

View file

@ -2840,8 +2840,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
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)
public func makeAffiliateProgramJoinScreen(context: AccountContext, sourcePeer: EnginePeer, commissionPermille: Int32, programDuration: Int32?, revenuePerUser: Double, mode: JoinAffiliateProgramScreenMode) -> ViewController {
return JoinAffiliateProgramScreen(context: context, sourcePeer: sourcePeer, commissionPermille: commissionPermille, programDuration: programDuration, revenuePerUser: revenuePerUser, mode: mode)
}
}

View file

@ -113,7 +113,7 @@ private enum ParsedUrl {
case internalUrl(ParsedInternalUrl)
}
public func parseInternalUrl(sharedContext: SharedAccountContext, query: String) -> ParsedInternalUrl? {
public func parseInternalUrl(sharedContext: SharedAccountContext, context: AccountContext?, query: String) -> ParsedInternalUrl? {
var query = query
if query.hasPrefix("s/") {
query = String(query[query.index(query.startIndex, offsetBy: 2)...])
@ -270,7 +270,12 @@ public func parseInternalUrl(sharedContext: SharedAccountContext, query: String)
}
return .peer(.name(peerName), .attachBotStart(value, startAttach))
} else if queryItem.name == "start" {
let linkRefPrefix = "_tgref_"
var linkRefPrefix = "_tgr_"
if let context {
if let data = context.currentAppConfiguration.with({ $0 }).data, let value = data["starref_start_param_prefixes"] as? String {
linkRefPrefix = value
}
}
if value.hasPrefix(linkRefPrefix) {
let referrer = String(value[value.index(value.startIndex, offsetBy: linkRefPrefix.count)...])
return .peer(.name(peerName), .referrer(referrer))
@ -1139,14 +1144,14 @@ public func parseProxyUrl(sharedContext: SharedAccountContext, url: String) -> (
for scheme in schemes {
let basePrefix = scheme + basePath + "/"
if url.lowercased().hasPrefix(basePrefix) {
if let internalUrl = parseInternalUrl(sharedContext: sharedContext, query: String(url[basePrefix.endIndex...])), case let .proxy(host, port, username, password, secret) = internalUrl {
if let internalUrl = parseInternalUrl(sharedContext: sharedContext, context: nil, query: String(url[basePrefix.endIndex...])), case let .proxy(host, port, username, password, secret) = internalUrl {
return (host, port, username, password, secret)
}
}
}
}
if let parsedUrl = URL(string: url), parsedUrl.scheme == "tg", let host = parsedUrl.host, let query = parsedUrl.query {
if let internalUrl = parseInternalUrl(sharedContext: sharedContext, query: host + "?" + query), case let .proxy(host, port, username, password, secret) = internalUrl {
if let internalUrl = parseInternalUrl(sharedContext: sharedContext, context: nil, query: host + "?" + query), case let .proxy(host, port, username, password, secret) = internalUrl {
return (host, port, username, password, secret)
}
}
@ -1160,14 +1165,14 @@ public func parseStickerPackUrl(sharedContext: SharedAccountContext, url: String
for scheme in schemes {
let basePrefix = scheme + basePath + "/"
if url.lowercased().hasPrefix(basePrefix) {
if let internalUrl = parseInternalUrl(sharedContext: sharedContext, query: String(url[basePrefix.endIndex...])), case let .stickerPack(name, _) = internalUrl {
if let internalUrl = parseInternalUrl(sharedContext: sharedContext, context: nil, query: String(url[basePrefix.endIndex...])), case let .stickerPack(name, _) = internalUrl {
return name
}
}
}
}
if let parsedUrl = URL(string: url), parsedUrl.scheme == "tg", let host = parsedUrl.host, let query = parsedUrl.query {
if let internalUrl = parseInternalUrl(sharedContext: sharedContext, query: host + "?" + query), case let .stickerPack(name, _) = internalUrl {
if let internalUrl = parseInternalUrl(sharedContext: sharedContext, context: nil, query: host + "?" + query), case let .stickerPack(name, _) = internalUrl {
return name
}
}
@ -1181,14 +1186,14 @@ public func parseWallpaperUrl(sharedContext: SharedAccountContext, url: String)
for scheme in schemes {
let basePrefix = scheme + basePath + "/"
if url.lowercased().hasPrefix(basePrefix) {
if let internalUrl = parseInternalUrl(sharedContext: sharedContext, query: String(url[basePrefix.endIndex...])), case let .wallpaper(wallpaper) = internalUrl {
if let internalUrl = parseInternalUrl(sharedContext: sharedContext, context: nil, query: String(url[basePrefix.endIndex...])), case let .wallpaper(wallpaper) = internalUrl {
return wallpaper
}
}
}
}
if let parsedUrl = URL(string: url), parsedUrl.scheme == "tg", let host = parsedUrl.host, let query = parsedUrl.query {
if let internalUrl = parseInternalUrl(sharedContext: sharedContext, query: host + "?" + query), case let .wallpaper(wallpaper) = internalUrl {
if let internalUrl = parseInternalUrl(sharedContext: sharedContext, context: nil, query: host + "?" + query), case let .wallpaper(wallpaper) = internalUrl {
return wallpaper
}
}
@ -1196,20 +1201,20 @@ public func parseWallpaperUrl(sharedContext: SharedAccountContext, url: String)
return nil
}
public func parseAdUrl(sharedContext: SharedAccountContext, url: String) -> ParsedInternalUrl? {
public func parseAdUrl(sharedContext: SharedAccountContext, context: AccountContext, url: String) -> ParsedInternalUrl? {
let schemes = ["http://", "https://", ""]
for basePath in baseTelegramMePaths {
for scheme in schemes {
let basePrefix = scheme + basePath + "/"
if url.lowercased().hasPrefix(basePrefix) {
if let internalUrl = parseInternalUrl(sharedContext: sharedContext, query: String(url[basePrefix.endIndex...])), case .peer = internalUrl {
if let internalUrl = parseInternalUrl(sharedContext: sharedContext, context: context, query: String(url[basePrefix.endIndex...])), case .peer = internalUrl {
return internalUrl
}
}
}
}
if let parsedUrl = URL(string: url), parsedUrl.scheme == "tg", let host = parsedUrl.host, let query = parsedUrl.query {
if let internalUrl = parseInternalUrl(sharedContext: sharedContext, query: host + "?" + query), case .peer = internalUrl {
if let internalUrl = parseInternalUrl(sharedContext: sharedContext, context: context, query: host + "?" + query), case .peer = internalUrl {
return internalUrl
}
}
@ -1217,13 +1222,13 @@ public func parseAdUrl(sharedContext: SharedAccountContext, url: String) -> Pars
return nil
}
public func parseFullInternalUrl(sharedContext: SharedAccountContext, url: String) -> ParsedInternalUrl? {
public func parseFullInternalUrl(sharedContext: SharedAccountContext, context: AccountContext, url: String) -> ParsedInternalUrl? {
let schemes = ["http://", "https://", ""]
for basePath in baseTelegramMePaths {
for scheme in schemes {
let basePrefix = scheme + basePath + "/"
if url.lowercased().hasPrefix(basePrefix) {
if let internalUrl = parseInternalUrl(sharedContext: sharedContext, query: String(url[basePrefix.endIndex...])) {
if let internalUrl = parseInternalUrl(sharedContext: sharedContext, context: context, query: String(url[basePrefix.endIndex...])) {
return internalUrl
}
}
@ -1309,7 +1314,7 @@ public func resolveUrlImpl(context: AccountContext, peerId: PeerId?, url: String
}
}
if url.lowercased().hasPrefix(basePrefix) {
if let internalUrl = parseInternalUrl(sharedContext: context.sharedContext, query: String(url[basePrefix.endIndex...])) {
if let internalUrl = parseInternalUrl(sharedContext: context.sharedContext, context: context, query: String(url[basePrefix.endIndex...])) {
return resolveInternalUrl(context: context, url: internalUrl)
|> map { result -> ResolveUrlResult in
switch result {

View file

@ -456,7 +456,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
}
})
} else {
if let url = controller.url, isTelegramMeLink(url), let internalUrl = parseFullInternalUrl(sharedContext: self.context.sharedContext, url: url), case .peer(_, .appStart) = internalUrl {
if let url = controller.url, isTelegramMeLink(url), let internalUrl = parseFullInternalUrl(sharedContext: self.context.sharedContext, context: self.context, url: url), case .peer(_, .appStart) = internalUrl {
let _ = (self.context.sharedContext.resolveUrl(context: self.context, peerId: controller.peerId, url: url, skipUrlAuth: false)
|> deliverOnMainQueue).startStandalone(next: { [weak self] result in
guard let self, let controller = self.controller else {