diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 1c8fad66a4..205c4e31ec 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -812,6 +812,9 @@ public protocol CollectibleItemInfoScreenInitialData: AnyObject { public protocol BusinessLinksSetupScreenInitialData: AnyObject { } +public protocol AffiliateProgramSetupScreenInitialData: AnyObject { +} + public enum CollectibleItemInfoScreenSubject { case phoneNumber(String) case username(String) @@ -1052,6 +1055,9 @@ public protocol SharedAccountContext: AnyObject { func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal)?, botPeer: EnginePeer, chatPeer: EnginePeer?, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool, payload: String?) + func makeAffiliateProgramSetupScreenInitialData(context: AccountContext, peerId: EnginePeer.Id) -> Signal + func makeAffiliateProgramSetupScreen(context: AccountContext, initialData: AffiliateProgramSetupScreenInitialData) -> ViewController + func makeDebugSettingsController(context: AccountContext?) -> ViewController? func navigateToCurrentCall() diff --git a/submodules/ComponentFlow/Source/Components/HStack.swift b/submodules/ComponentFlow/Source/Components/HStack.swift index 1bb452d1ba..e27b823240 100644 --- a/submodules/ComponentFlow/Source/Components/HStack.swift +++ b/submodules/ComponentFlow/Source/Components/HStack.swift @@ -1,15 +1,22 @@ import Foundation import UIKit +public enum HStackAlignment { + case left + case alternatingLeftRight +} + public final class HStack: CombinedComponent { public typealias EnvironmentType = ChildEnvironment private let items: [AnyComponentWithIdentity] private let spacing: CGFloat + private let alignment: HStackAlignment - public init(_ items: [AnyComponentWithIdentity], spacing: CGFloat) { + public init(_ items: [AnyComponentWithIdentity], spacing: CGFloat, alignment: HStackAlignment = .left) { self.items = items self.spacing = spacing + self.alignment = alignment } public static func ==(lhs: HStack, rhs: HStack) -> Bool { @@ -19,6 +26,9 @@ public final class HStack: CombinedComponent { if lhs.spacing != rhs.spacing { return false } + if lhs.alignment != rhs.alignment { + return false + } return true } @@ -42,21 +52,51 @@ public final class HStack: CombinedComponent { } var size = CGSize(width: 0.0, height: 0.0) - for child in updatedChildren { - size.width += child.size.width - size.height = max(size.height, child.size.height) - } - size.width += context.component.spacing * CGFloat(updatedChildren.count - 1) - - var nextX = 0.0 - for child in updatedChildren { - context.add(child - .position(child.size.centered(in: CGRect(origin: CGPoint(x: nextX, y: floor((size.height - child.size.height) * 0.5)), size: child.size)).center) - .appear(.default(scale: true, alpha: true)) - .disappear(.default(scale: true, alpha: true)) - ) - nextX += child.size.width - nextX += context.component.spacing + switch context.component.alignment { + case .left: + for child in updatedChildren { + size.width += child.size.width + size.height = max(size.height, child.size.height) + } + size.width += context.component.spacing * CGFloat(updatedChildren.count - 1) + + var nextX = 0.0 + for child in updatedChildren { + context.add(child + .position(child.size.centered(in: CGRect(origin: CGPoint(x: nextX, y: floor((size.height - child.size.height) * 0.5)), size: child.size)).center) + .appear(.default(scale: true, alpha: true)) + .disappear(.default(scale: true, alpha: true)) + ) + nextX += child.size.width + nextX += context.component.spacing + } + case .alternatingLeftRight: + size.width = context.availableSize.width + for child in updatedChildren { + size.height = max(size.height, child.size.height) + } + + var nextLeftX = 0.0 + var nextRightX = size.width + for i in 0 ..< updatedChildren.count { + let child = updatedChildren[i] + let childFrame: CGRect + if i % 2 == 0 { + childFrame = CGRect(origin: CGPoint(x: nextLeftX, y: floor((size.height - child.size.height) * 0.5)), size: child.size) + nextLeftX += child.size.width + nextLeftX += context.component.spacing + } else { + childFrame = CGRect(origin: CGPoint(x: nextRightX - child.size.width, y: floor((size.height - child.size.height) * 0.5)), size: child.size) + nextRightX -= child.size.width + nextRightX -= context.component.spacing + } + + context.add(child + .position(child.size.centered(in: childFrame).center) + .appear(.default(scale: true, alpha: true)) + .disappear(.default(scale: true, alpha: true)) + ) + } } return size diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index ff1d488ded..0e39803e1e 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -462,6 +462,7 @@ swift_library( "//submodules/TelegramUI/Components/Stars/StarsIntroScreen", "//submodules/TelegramUI/Components/Gifts/GiftOptionsScreen", "//submodules/TelegramUI/Components/ContentReportScreen", + "//submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen", ] + select({ "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "//build-system:ios_sim_arm64": [], diff --git a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift index a7b50740e5..2b9f5ef3e3 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift @@ -1363,42 +1363,44 @@ private final class ChatSendStarsScreenComponent: Component { let sliderSize = self.slider.update( transition: transition, component: AnyComponent(SliderComponent( - valueCount: self.amount.maxSliderValue + 1, - value: self.amount.sliderValue, - markPositions: false, + content: .discrete(SliderComponent.Discrete( + valueCount: self.amount.maxSliderValue + 1, + value: self.amount.sliderValue, + markPositions: false, + valueUpdated: { [weak self] value in + guard let self, let component = self.component else { + return + } + self.amount = self.amount.withSliderValue(value) + self.didChangeAmount = true + + self.state?.updated(transition: ComponentTransition(animation: .none).withUserData(IsAdjustingAmountHint())) + + let sliderValue = Float(value) / Float(component.maxAmount) + let currentTimestamp = CACurrentMediaTime() + + if let previousTimestamp { + let deltaTime = currentTimestamp - previousTimestamp + let delta = sliderValue - self.previousSliderValue + let deltaValue = abs(sliderValue - self.previousSliderValue) + + let speed = deltaValue / Float(deltaTime) + let newSpeed = max(0, min(65.0, speed * 70.0)) + + if newSpeed < 0.01 && deltaValue < 0.001 { + } else { + self.badgeStars.update(speed: newSpeed, delta: delta) + } + } + + self.previousSliderValue = sliderValue + self.previousTimestamp = currentTimestamp + } + )), trackBackgroundColor: .clear, trackForegroundColor: .clear, knobSize: 26.0, knobColor: .white, - valueUpdated: { [weak self] value in - guard let self, let component = self.component else { - return - } - self.amount = self.amount.withSliderValue(value) - self.didChangeAmount = true - - self.state?.updated(transition: ComponentTransition(animation: .none).withUserData(IsAdjustingAmountHint())) - - let sliderValue = Float(value) / Float(component.maxAmount) - let currentTimestamp = CACurrentMediaTime() - - if let previousTimestamp { - let deltaTime = currentTimestamp - previousTimestamp - let delta = sliderValue - self.previousSliderValue - let deltaValue = abs(sliderValue - self.previousSliderValue) - - let speed = deltaValue / Float(deltaTime) - let newSpeed = max(0, min(65.0, speed * 70.0)) - - if newSpeed < 0.01 && deltaValue < 0.001 { - } else { - self.badgeStars.update(speed: newSpeed, delta: delta) - } - } - - self.previousSliderValue = sliderValue - self.previousTimestamp = currentTimestamp - }, isTrackingUpdated: { [weak self] isTracking in guard let self else { return diff --git a/submodules/TelegramUI/Components/ListItemSliderSelectorComponent/Sources/ListItemSliderSelectorComponent.swift b/submodules/TelegramUI/Components/ListItemSliderSelectorComponent/Sources/ListItemSliderSelectorComponent.swift index c92687fbe5..0a12200c02 100644 --- a/submodules/TelegramUI/Components/ListItemSliderSelectorComponent/Sources/ListItemSliderSelectorComponent.swift +++ b/submodules/TelegramUI/Components/ListItemSliderSelectorComponent/Sources/ListItemSliderSelectorComponent.swift @@ -9,43 +9,91 @@ import ListSectionComponent import SliderComponent public final class ListItemSliderSelectorComponent: Component { + public final class Discrete: Equatable { + public let values: [String] + public let markPositions: Bool + public let selectedIndex: Int + public let title: String? + public let selectedIndexUpdated: (Int) -> Void + + public init(values: [String], markPositions: Bool, selectedIndex: Int, title: String?, selectedIndexUpdated: @escaping (Int) -> Void) { + self.values = values + self.markPositions = markPositions + self.selectedIndex = selectedIndex + self.title = title + self.selectedIndexUpdated = selectedIndexUpdated + } + + public static func ==(lhs: Discrete, rhs: Discrete) -> Bool { + if lhs.values != rhs.values { + return false + } + if lhs.markPositions != rhs.markPositions { + return false + } + if lhs.selectedIndex != rhs.selectedIndex { + return false + } + if lhs.title != rhs.title { + return false + } + return true + } + } + + public final class Continuous: Equatable { + public let value: CGFloat + public let lowerBoundTitle: String + public let upperBoundTitle: String + public let title: String + public let valueUpdated: (CGFloat) -> Void + + public init(value: CGFloat, lowerBoundTitle: String, upperBoundTitle: String, title: String, valueUpdated: @escaping (CGFloat) -> Void) { + self.value = value + self.lowerBoundTitle = lowerBoundTitle + self.upperBoundTitle = upperBoundTitle + self.title = title + self.valueUpdated = valueUpdated + } + + public static func ==(lhs: Continuous, rhs: Continuous) -> Bool { + if lhs.value != rhs.value { + return false + } + if lhs.lowerBoundTitle != rhs.lowerBoundTitle { + return false + } + if lhs.upperBoundTitle != rhs.upperBoundTitle { + return false + } + if lhs.title != rhs.title { + return false + } + return true + } + } + + public enum Content: Equatable { + case discrete(Discrete) + case continuous(Continuous) + } + public let theme: PresentationTheme - public let values: [String] - public let markPositions: Bool - public let selectedIndex: Int - public let title: String? - public let selectedIndexUpdated: (Int) -> Void + public let content: Content public init( theme: PresentationTheme, - values: [String], - markPositions: Bool, - selectedIndex: Int, - title: String?, - selectedIndexUpdated: @escaping (Int) -> Void + content: Content ) { self.theme = theme - self.values = values - self.markPositions = markPositions - self.selectedIndex = selectedIndex - self.title = title - self.selectedIndexUpdated = selectedIndexUpdated + self.content = content } public static func ==(lhs: ListItemSliderSelectorComponent, rhs: ListItemSliderSelectorComponent) -> Bool { if lhs.theme !== rhs.theme { return false } - if lhs.values != rhs.values { - return false - } - if lhs.markPositions != rhs.markPositions { - return false - } - if lhs.selectedIndex != rhs.selectedIndex { - return false - } - if lhs.title != rhs.title { + if lhs.content != rhs.content { return false } return true @@ -81,48 +129,90 @@ public final class ListItemSliderSelectorComponent: Component { let titleAreaWidth: CGFloat = availableSize.width - titleSideInset * 2.0 var validIds: [Int] = [] - for i in 0 ..< component.values.count { - if component.title != nil { - if i != 0 && i != component.values.count - 1 { - continue + var mainTitleValue: String? + + switch component.content { + case let .discrete(discrete): + mainTitleValue = discrete.title + + for i in 0 ..< discrete.values.count { + if discrete.title != nil { + if i != 0 && i != discrete.values.count - 1 { + continue + } + } + + validIds.append(i) + + var titleTransition = transition + let title: ComponentView + if let current = self.titles[i] { + title = current + } else { + titleTransition = titleTransition.withAnimation(.none) + title = ComponentView() + self.titles[i] = title + } + let titleSize = title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: discrete.values[i], font: Font.regular(13.0), textColor: component.theme.list.itemSecondaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + var titleFrame = CGRect(origin: CGPoint(x: titleSideInset - floor(titleSize.width * 0.5), y: 14.0), size: titleSize) + if discrete.values.count > 1 { + titleFrame.origin.x += floor(CGFloat(i) / CGFloat(discrete.values.count - 1) * titleAreaWidth) + } + if titleFrame.minX < titleClippingSideInset { + titleFrame.origin.x = titleSideInset + } + if titleFrame.maxX > availableSize.width - titleClippingSideInset { + titleFrame.origin.x = availableSize.width - titleClippingSideInset - titleSize.width + } + if let titleView = title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) + titleTransition.setPosition(view: titleView, position: titleFrame.center) } } + case let .continuous(continuous): + mainTitleValue = continuous.title - validIds.append(i) - - var titleTransition = transition - let title: ComponentView - if let current = self.titles[i] { - title = current - } else { - titleTransition = titleTransition.withAnimation(.none) - title = ComponentView() - self.titles[i] = title - } - let titleSize = title.update( - transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: component.values[i], font: Font.regular(13.0), textColor: component.theme.list.itemSecondaryTextColor)) - )), - environment: {}, - containerSize: CGSize(width: 100.0, height: 100.0) - ) - var titleFrame = CGRect(origin: CGPoint(x: titleSideInset - floor(titleSize.width * 0.5), y: 14.0), size: titleSize) - if component.values.count > 1 { - titleFrame.origin.x += floor(CGFloat(i) / CGFloat(component.values.count - 1) * titleAreaWidth) - } - if titleFrame.minX < titleClippingSideInset { - titleFrame.origin.x = titleSideInset - } - if titleFrame.maxX > availableSize.width - titleClippingSideInset { - titleFrame.origin.x = availableSize.width - titleClippingSideInset - titleSize.width - } - if let titleView = title.view { - if titleView.superview == nil { - self.addSubview(titleView) + for i in 0 ..< 2 { + validIds.append(i) + + var titleTransition = transition + let title: ComponentView + if let current = self.titles[i] { + title = current + } else { + titleTransition = titleTransition.withAnimation(.none) + title = ComponentView() + self.titles[i] = title + } + let titleSize = title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: i == 0 ? continuous.lowerBoundTitle : continuous.upperBoundTitle, font: Font.regular(13.0), textColor: component.theme.list.itemSecondaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + var titleFrame = CGRect(origin: CGPoint(x: titleSideInset, y: 14.0), size: titleSize) + if i == 1 { + titleFrame.origin.x = availableSize.width - titleClippingSideInset - titleSize.width + } + if let titleView = title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) + titleTransition.setPosition(view: titleView, position: titleFrame.center) } - titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) - titleTransition.setPosition(view: titleView, position: titleFrame.center) } } var removeIds: [Int] = [] @@ -136,7 +226,7 @@ public final class ListItemSliderSelectorComponent: Component { self.titles.removeValue(forKey: id) } - if let title = component.title { + if let title = mainTitleValue { let mainTitle: ComponentView var mainTitleTransition = transition if let current = self.mainTitle { @@ -169,24 +259,50 @@ public final class ListItemSliderSelectorComponent: Component { } } - let sliderSize = self.slider.update( - transition: transition, - component: AnyComponent(SliderComponent( - valueCount: component.values.count, - value: component.selectedIndex, - markPositions: component.markPositions, - trackBackgroundColor: component.theme.list.controlSecondaryColor, - trackForegroundColor: component.theme.list.itemAccentColor, - valueUpdated: { [weak self] value in - guard let self, let component = self.component else { - return - } - component.selectedIndexUpdated(value) - } - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) - ) + let sliderSize: CGSize + switch component.content { + case let .discrete(discrete): + sliderSize = self.slider.update( + transition: transition, + component: AnyComponent(SliderComponent( + content: .discrete(SliderComponent.Discrete( + valueCount: discrete.values.count, + value: discrete.selectedIndex, + markPositions: discrete.markPositions, + valueUpdated: { [weak self] value in + guard let self, let component = self.component, case let .discrete(discrete) = component.content else { + return + } + discrete.selectedIndexUpdated(value) + }) + ), + trackBackgroundColor: component.theme.list.controlSecondaryColor, + trackForegroundColor: component.theme.list.itemAccentColor + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) + ) + case let .continuous(continuous): + sliderSize = self.slider.update( + transition: transition, + component: AnyComponent(SliderComponent( + content: .continuous(SliderComponent.Continuous( + value: continuous.value, + valueUpdated: { [weak self] value in + guard let self, let component = self.component, case let .continuous(continuous) = component.content else { + return + } + continuous.valueUpdated(value) + }) + ), + trackBackgroundColor: component.theme.list.controlSecondaryColor, + trackForegroundColor: component.theme.list.itemAccentColor + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) + ) + } + let sliderFrame = CGRect(origin: CGPoint(x: sideInset, y: 36.0), size: sliderSize) if let sliderView = self.slider.view { if sliderView.superview == nil { diff --git a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift index 5fa8c37f6f..d4caaa3919 100644 --- a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift +++ b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift @@ -1281,20 +1281,22 @@ final class PeerAllowedReactionsScreenComponent: Component { items: [ AnyComponentWithIdentity(id: 0, component: AnyComponent(ListItemSliderSelectorComponent( theme: environment.theme, - values: reactionCountValueList.map { item in - return item - }, - markPositions: false, - selectedIndex: max(0, min(reactionCountValueList.count - 1, self.allowedReactionCount - 1)), - title: sliderTitle, - selectedIndexUpdated: { [weak self] index in - guard let self else { - return + content: .discrete(ListItemSliderSelectorComponent.Discrete( + values: reactionCountValueList.map { item in + return item + }, + markPositions: false, + selectedIndex: max(0, min(reactionCountValueList.count - 1, self.allowedReactionCount - 1)), + title: sliderTitle, + selectedIndexUpdated: { [weak self] index in + guard let self else { + return + } + let index = max(1, min(reactionCountValueList.count, index + 1)) + self.allowedReactionCount = index + self.state?.updated(transition: .immediate) } - let index = max(1, min(reactionCountValueList.count, index + 1)) - self.allowedReactionCount = index - self.state?.updated(transition: .immediate) - } + )) ))) ], displaySeparators: false diff --git a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/BUILD b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/BUILD new file mode 100644 index 0000000000..5a21f5d498 --- /dev/null +++ b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/BUILD @@ -0,0 +1,36 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "AffiliateProgramSetupScreen", + module_name = "AffiliateProgramSetupScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/AsyncDisplayKit", + "//submodules/TelegramCore", + "//submodules/Postbox", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/TelegramStringFormatting", + "//submodules/ComponentFlow", + "//submodules/AppBundle", + "//submodules/UndoUI", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/ViewControllerComponent", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/TelegramUI/Components/ListSectionComponent", + "//submodules/TelegramUI/Components/ListItemSliderSelectorComponent", + "//submodules/TelegramUI/Components/ListActionItemComponent", + "//submodules/Components/BlurredBackgroundComponent", + "//submodules/Markdown", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/AffiliateProgramSetupScreen.swift b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/AffiliateProgramSetupScreen.swift new file mode 100644 index 0000000000..dc013bc4fe --- /dev/null +++ b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/AffiliateProgramSetupScreen.swift @@ -0,0 +1,715 @@ +import Foundation +import UIKit +import Display +import TelegramPresentationData +import ComponentFlow +import ComponentDisplayAdapters +import AppBundle +import ViewControllerComponent +import AccountContext +import TelegramCore +import Postbox +import SwiftSignalKit +import MultilineTextComponent +import ButtonComponent +import UndoUI +import BundleIconComponent +import ListSectionComponent +import ListItemSliderSelectorComponent +import ListActionItemComponent +import Markdown +import BlurredBackgroundComponent + +final class AffiliateProgramSetupScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let initialContent: AffiliateProgramSetupScreen.Content + + init( + context: AccountContext, + initialContent: AffiliateProgramSetupScreen.Content + ) { + self.context = context + self.initialContent = initialContent + } + + static func ==(lhs: AffiliateProgramSetupScreenComponent, rhs: AffiliateProgramSetupScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + + return true + } + + final class View: UIView, UIScrollViewDelegate { + private let scrollView: UIScrollView + + private let title = ComponentView() + private let titleTransformContainer: UIView + private let subtitle = ComponentView() + + private let introBackground = ComponentView() + private var introIconItems: [Int: ComponentView] = [:] + private var introTitleItems: [Int: ComponentView] = [:] + private var introTextItems: [Int: ComponentView] = [:] + + private let commissionSection = ComponentView() + private let durationSection = ComponentView() + private let existingProgramsSection = ComponentView() + private let endProgramSection = ComponentView() + + private let bottomPanelSeparator = SimpleLayer() + private let bottomPanelBackground = ComponentView() + private let bottomPanelButton = ComponentView() + private let bottomPanelText = ComponentView() + + private var isUpdating: Bool = false + + private var component: AffiliateProgramSetupScreenComponent? + private(set) weak var state: EmptyComponentState? + private var environment: EnvironmentType? + + override init(frame: CGRect) { + self.scrollView = UIScrollView() + self.scrollView.showsVerticalScrollIndicator = true + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.scrollsToTop = false + self.scrollView.delaysContentTouches = false + self.scrollView.canCancelContentTouches = true + self.scrollView.contentInsetAdjustmentBehavior = .never + self.scrollView.alwaysBounceVertical = true + + self.titleTransformContainer = UIView() + self.scrollView.addSubview(self.titleTransformContainer) + + super.init(frame: frame) + + self.scrollView.delegate = self + self.addSubview(self.scrollView) + + self.layer.addSublayer(self.bottomPanelSeparator) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + func scrollToTop() { + self.scrollView.setContentOffset(CGPoint(), animated: true) + } + + func attemptNavigation(complete: @escaping () -> Void) -> Bool { + return true + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + self.updateScrolling(transition: .immediate) + } + + private func updateScrolling(transition: ComponentTransition) { + let navigationAlphaDistance: CGFloat = 16.0 + let navigationAlpha: CGFloat = max(0.0, min(1.0, self.scrollView.contentOffset.y / navigationAlphaDistance)) + if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar { + transition.setAlpha(layer: navigationBar.backgroundNode.layer, alpha: navigationAlpha) + transition.setAlpha(layer: navigationBar.stripeNode.layer, alpha: navigationAlpha) + } + } + + func update(component: AffiliateProgramSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + let environment = environment[EnvironmentType.self].value + let themeUpdated = self.environment?.theme !== environment.theme + self.environment = environment + + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }) + + if themeUpdated { + self.backgroundColor = environment.theme.list.blocksBackgroundColor + self.bottomPanelSeparator.backgroundColor = environment.theme.rootController.navigationBar.separatorColor.cgColor + } + + self.component = component + self.state = state + + let topInset: CGFloat = environment.navigationHeight + 87.0 + let bottomInset: CGFloat = 8.0 + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + let textSideInset: CGFloat = 16.0 + let sectionSpacing: CGFloat = 24.0 + + var contentHeight: CGFloat = 0.0 + contentHeight += topInset + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: "Affiliate Program", font: Font.bold(30.0), textColor: environment.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - textSideInset * 2.0, height: 1000.0) + ) + let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: contentHeight), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + self.titleTransformContainer.addSubview(titleView) + } + titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) + transition.setPosition(view: self.titleTransformContainer, position: titleFrame.center) + } + contentHeight += titleSize.height + contentHeight += 10.0 + + let subtitleSize = self.subtitle.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: "Reward those who help grow your userbase.", font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor)), + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - textSideInset * 2.0, height: 1000.0) + ) + let subtitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - subtitleSize.width) * 0.5), y: contentHeight), size: subtitleSize) + if let subtitleView = self.subtitle.view { + if subtitleView.superview == nil { + self.scrollView.addSubview(subtitleView) + subtitleView.bounds = CGRect(origin: CGPoint(), size: subtitleFrame.size) + transition.setPosition(view: subtitleView, position: subtitleFrame.center) + } + } + contentHeight += subtitleSize.height + contentHeight += 24.0 + + let introItems: [(icon: String, title: String, text: String)] = [ + ( + "Chat/Context Menu/Smile", + "Share revenue with affiliates", + "Set the commission for revenue generated by users referred to you." + ), + ( + "Chat/Context Menu/Smile", + "Launch your affiliate program", + "Telegram will feature your program for millions of potential affiliates." + ), + ( + "Chat/Context Menu/Smile", + "Let affiliates promote you", + "Affiliates will share your referral link with their audience." + ) + ] + var introItemsHeight: CGFloat = 17.0 + let introItemIconX: CGFloat = sideInset + 19.0 + let introItemTextX: CGFloat = sideInset + 56.0 + let introItemTextRightInset: CGFloat = sideInset + 10.0 + let introItemSpacing: CGFloat = 22.0 + for i in 0 ..< introItems.count { + if i != 0 { + introItemsHeight += introItemSpacing + } + + let item = introItems[i] + + let itemIcon: ComponentView + let itemTitle: ComponentView + let itemText: ComponentView + + if let current = self.introIconItems[i] { + itemIcon = current + } else { + itemIcon = ComponentView() + self.introIconItems[i] = itemIcon + } + + if let current = self.introTitleItems[i] { + itemTitle = current + } else { + itemTitle = ComponentView() + self.introTitleItems[i] = itemTitle + } + + if let current = self.introTextItems[i] { + itemText = current + } else { + itemText = ComponentView() + self.introTextItems[i] = itemText + } + + let iconSize = itemIcon.update( + transition: .immediate, + component: AnyComponent(BundleIconComponent( + name: item.icon, + tintColor: environment.theme.list.itemAccentColor + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + let titleSize = itemTitle.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: item.title, font: Font.semibold(15.0), textColor: environment.theme.list.itemPrimaryTextColor)), + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - introItemTextRightInset - introItemTextX, height: 1000.0) + ) + let textSize = itemText.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: item.text, font: Font.regular(15.0), textColor: environment.theme.list.itemSecondaryTextColor)), + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - introItemTextRightInset - introItemTextX, height: 1000.0) + ) + + let itemIconFrame = CGRect(origin: CGPoint(x: introItemIconX, y: contentHeight + introItemsHeight + 8.0), size: iconSize) + let itemTitleFrame = CGRect(origin: CGPoint(x: introItemTextX, y: contentHeight + introItemsHeight), size: titleSize) + let itemTextFrame = CGRect(origin: CGPoint(x: introItemTextX, y: itemTitleFrame.maxY + 5.0), size: textSize) + + if let itemIconView = itemIcon.view { + if itemIconView.superview == nil { + self.scrollView.addSubview(itemIconView) + } + transition.setFrame(view: itemIconView, frame: itemIconFrame) + } + if let itemTitleView = itemTitle.view { + if itemTitleView.superview == nil { + itemTitleView.layer.anchorPoint = CGPoint() + self.scrollView.addSubview(itemTitleView) + } + transition.setPosition(view: itemTitleView, position: itemTitleFrame.origin) + itemTitleView.bounds = CGRect(origin: CGPoint(), size: itemTitleFrame.size) + } + if let itemTextView = itemText.view { + if itemTextView.superview == nil { + itemTextView.layer.anchorPoint = CGPoint() + self.scrollView.addSubview(itemTextView) + } + transition.setPosition(view: itemTextView, position: itemTextFrame.origin) + itemTextView.bounds = CGRect(origin: CGPoint(), size: itemTextFrame.size) + } + introItemsHeight = itemTextFrame.maxY - contentHeight + } + introItemsHeight += 19.0 + + let introBackgroundFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: CGSize(width: availableSize.width - sideInset * 2.0, height: introItemsHeight)) + let _ = self.introBackground.update( + transition: transition, + component: AnyComponent(FilledRoundedRectangleComponent( + color: environment.theme.list.itemBlocksBackgroundColor, + cornerRadius: .value(5.0), + smoothCorners: true + )), + environment: {}, + containerSize: introBackgroundFrame.size + ) + if let introBackgroundView = self.introBackground.view { + if introBackgroundView.superview == nil, let firstIconItemView = self.introIconItems[0]?.view { + self.scrollView.insertSubview(introBackgroundView, belowSubview: firstIconItemView) + } + transition.setFrame(view: introBackgroundView, frame: introBackgroundFrame) + } + contentHeight += introItemsHeight + contentHeight += sectionSpacing + 6.0 + + let commissionSectionSize = self.commissionSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "COMMISSION", + font: Font.regular(13.0), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + footer: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "Define the percentage of star revenue your affiliates earn for referring users to your bot.", + font: Font.regular(13.0), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListItemSliderSelectorComponent( + theme: environment.theme, + content: .continuous(ListItemSliderSelectorComponent.Continuous( + value: 0.0, + lowerBoundTitle: "1%", + upperBoundTitle: "90%", + title: "1%", + valueUpdated: { [weak self] value in + guard let self else { + return + } + let _ = self + } + )) + ))) + ], + displaySeparators: false + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let commissionSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: commissionSectionSize) + if let commissionSectionView = self.commissionSection.view { + if commissionSectionView.superview == nil { + self.scrollView.addSubview(commissionSectionView) + } + transition.setFrame(view: commissionSectionView, frame: commissionSectionFrame) + } + contentHeight += commissionSectionSize.height + contentHeight += sectionSpacing + 12.0 + + let durationItems: [(months: Int32, title: String, selectedTitle: String)] = [ + (1, "1m", "1 MONTH"), + (3, "3m", "3 MONTHS"), + (6, "6m", "6 MONTHS"), + (12, "1y", "1 YEAR"), + (2 * 12, "2y", "2 YEARS"), + (3 * 12, "3y", "3 YEARS"), + (Int32.max, "∞", "INDEFINITELY") + ] + let durationSectionSize = self.durationSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: AnyComponent(HStack([ + AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "DURATION", + font: Font.regular(13.0), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + ))), + AnyComponentWithIdentity(id: 1, component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: durationItems[0].selectedTitle, + font: Font.regular(13.0), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + ))) + ], spacing: 4.0, alignment: .alternatingLeftRight)), + footer: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "Set the duration for which affiliates will earn commissions from referred users.", + font: Font.regular(13.0), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListItemSliderSelectorComponent( + theme: environment.theme, + content: .discrete(ListItemSliderSelectorComponent.Discrete( + values: durationItems.map(\.title), + markPositions: true, + selectedIndex: 0, + title: nil, + selectedIndexUpdated: { [weak self] value in + guard let self else { + return + } + let _ = self + } + )) + ))) + ], + displaySeparators: false + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let durationSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: durationSectionSize) + if let durationSectionView = self.durationSection.view { + if durationSectionView.superview == nil { + self.scrollView.addSubview(durationSectionView) + } + transition.setFrame(view: durationSectionView, frame: durationSectionFrame) + } + contentHeight += durationSectionSize.height + contentHeight += sectionSpacing + 12.0 + + let existingProgramsSectionSize = self.existingProgramsSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: nil, + footer: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "Explore what other mini apps offer.", + font: Font.regular(13.0), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "View Existing Programs", + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + accessory: .arrow, + action: { [weak self] _ in + guard let self else { + return + } + + let _ = self + } + ))) + ], + displaySeparators: false + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let existingProgramsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: existingProgramsSectionSize) + if let existingProgramsSectionView = self.existingProgramsSection.view { + if existingProgramsSectionView.superview == nil { + self.scrollView.addSubview(existingProgramsSectionView) + } + transition.setFrame(view: existingProgramsSectionView, frame: existingProgramsSectionFrame) + } + contentHeight += existingProgramsSectionSize.height + contentHeight += sectionSpacing + 12.0 + + let endProgramSectionSize = self.endProgramSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: nil, + footer: nil, + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "End Affiliate Program", + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemDestructiveColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .center, spacing: 2.0)), + accessory: nil, + action: { [weak self] _ in + guard let self else { + return + } + + let _ = self + } + ))) + ], + displaySeparators: false + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let endProgramSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: endProgramSectionSize) + if let endProgramSectionView = self.endProgramSection.view { + if endProgramSectionView.superview == nil { + self.scrollView.addSubview(endProgramSectionView) + } + transition.setFrame(view: endProgramSectionView, frame: endProgramSectionFrame) + } + contentHeight += endProgramSectionSize.height + contentHeight += sectionSpacing + + contentHeight += bottomInset + + let bottomPanelTextSize = self.bottomPanelText.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .markdown( + text: "By creating an affiliate program, you afree to the [terms and conditions](https://telegram.org/terms) of Affiliate Programs.", + attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemSecondaryTextColor), + bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: environment.theme.list.itemSecondaryTextColor), + link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemAccentColor), + linkAttribute: { url in + return ("URL", url) + } + ) + ), + horizontalAlignment: .center, + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + + let bottomPanelButtonInsets = UIEdgeInsets(top: 10.0, left: sideInset, bottom: 10.0, right: sideInset) + + let bottomPanelButtonSize = self.bottomPanelButton.update( + transition: transition, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + color: environment.theme.list.itemCheckColors.fillColor, + foreground: environment.theme.list.itemCheckColors.foregroundColor, + pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8) + ), + content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(Text(text: "Start Affiliate Program", font: Font.semibold(17.0), color: environment.theme.list.itemCheckColors.foregroundColor))), + isEnabled: true, + allowActionWhenDisabled: true, + displaysProgress: false, + action: { [weak self] in + guard let self else { + return + } + let _ = self + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - bottomPanelButtonInsets.left - bottomPanelButtonInsets.right, height: 50.0) + ) + + let bottomPanelHeight: CGFloat = bottomPanelButtonInsets.top + bottomPanelButtonSize.height + bottomPanelButtonInsets.bottom + bottomPanelTextSize.height + 8.0 + environment.safeInsets.bottom + let bottomPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelHeight), size: CGSize(width: availableSize.width, height: bottomPanelHeight)) + + let _ = self.bottomPanelBackground.update( + transition: transition, + component: AnyComponent(BlurredBackgroundComponent( + color: environment.theme.rootController.navigationBar.blurredBackgroundColor + )), + environment: {}, + containerSize: bottomPanelFrame.size + ) + + if let bottomPanelBackgroundView = self.bottomPanelBackground.view { + if bottomPanelBackgroundView.superview == nil { + self.addSubview(bottomPanelBackgroundView) + } + transition.setFrame(view: bottomPanelBackgroundView, frame: bottomPanelFrame) + } + transition.setFrame(layer: self.bottomPanelSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: bottomPanelFrame.minY - UIScreenPixel), size: CGSize(width: availableSize.width, height: UIScreenPixel))) + + let bottomPanelButtonFrame = CGRect(origin: CGPoint(x: bottomPanelFrame.minX + bottomPanelButtonInsets.left, y: bottomPanelFrame.minY + bottomPanelButtonInsets.top), size: bottomPanelButtonSize) + if let bottomPanelButtonView = self.bottomPanelButton.view { + if bottomPanelButtonView.superview == nil { + self.addSubview(bottomPanelButtonView) + } + transition.setFrame(view: bottomPanelButtonView, frame: bottomPanelButtonFrame) + } + + let bottomPanelTextFrame = CGRect(origin: CGPoint(x: bottomPanelFrame.minX + floor((bottomPanelFrame.width - bottomPanelTextSize.width) * 0.5), y: bottomPanelButtonFrame.maxY + bottomPanelButtonInsets.bottom), size: bottomPanelTextSize) + if let bottomPanelTextView = self.bottomPanelText.view { + if bottomPanelTextView.superview == nil { + self.addSubview(bottomPanelTextView) + } + transition.setPosition(view: bottomPanelTextView, position: bottomPanelTextFrame.center) + bottomPanelTextView.bounds = CGRect(origin: CGPoint(), size: bottomPanelTextFrame.size) + } + + contentHeight += bottomPanelFrame.height + + let contentSize = CGSize(width: availableSize.width, height: contentHeight) + if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) { + self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize) + } + if self.scrollView.contentSize != contentSize { + self.scrollView.contentSize = contentSize + } + let scrollInsets = UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: environment.safeInsets.bottom, right: 0.0) + if self.scrollView.scrollIndicatorInsets != scrollInsets { + self.scrollView.scrollIndicatorInsets = scrollInsets + } + + self.updateScrolling(transition: transition) + + return availableSize + } + } + + func makeView() -> View { + return View() + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +public class AffiliateProgramSetupScreen: ViewControllerComponentContainer { + public final class Content: AffiliateProgramSetupScreenInitialData { + let peerId: EnginePeer.Id + + init( + peerId: EnginePeer.Id + ) { + self.peerId = peerId + } + } + + private let context: AccountContext + private var isDismissed: Bool = false + + public init( + context: AccountContext, + initialContent: Content + ) { + self.context = context + + super.init(context: context, component: AffiliateProgramSetupScreenComponent( + context: context, + initialContent: initialContent + ), navigationBarAppearance: .default, theme: .default) + + self.scrollToTop = { [weak self] in + guard let self, let componentView = self.node.hostView.componentView as? AffiliateProgramSetupScreenComponent.View else { + return + } + componentView.scrollToTop() + } + + self.attemptNavigation = { [weak self] complete in + guard let self, let componentView = self.node.hostView.componentView as? AffiliateProgramSetupScreenComponent.View else { + return true + } + + return componentView.attemptNavigation(complete: complete) + } + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + @objc private func cancelPressed() { + self.dismiss() + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + } + + public static func content(context: AccountContext, peerId: EnginePeer.Id) -> Signal { + return .single(Content( + peerId: peerId + )) + } +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 51531704cd..e2c38c4a11 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -607,6 +607,7 @@ private final class PeerInfoInteraction { let openWorkingHoursContextMenu: (ASDisplayNode, ContextGesture?) -> Void let openBusinessLocationContextMenu: (ASDisplayNode, ContextGesture?) -> Void let openBirthdayContextMenu: (ASDisplayNode, ContextGesture?) -> Void + let editingOpenAffiliateProgram: () -> Void let getController: () -> ViewController? init( @@ -675,6 +676,7 @@ private final class PeerInfoInteraction { openWorkingHoursContextMenu: @escaping (ASDisplayNode, ContextGesture?) -> Void, openBusinessLocationContextMenu: @escaping (ASDisplayNode, ContextGesture?) -> Void, openBirthdayContextMenu: @escaping (ASDisplayNode, ContextGesture?) -> Void, + editingOpenAffiliateProgram: @escaping () -> Void, getController: @escaping () -> ViewController? ) { self.openUsername = openUsername @@ -742,6 +744,7 @@ private final class PeerInfoInteraction { self.openWorkingHoursContextMenu = openWorkingHoursContextMenu self.openBusinessLocationContextMenu = openBusinessLocationContextMenu self.openBirthdayContextMenu = openBirthdayContextMenu + self.editingOpenAffiliateProgram = editingOpenAffiliateProgram self.getController = getController } } @@ -1906,6 +1909,7 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL let ItemInfo = 3 let ItemDelete = 4 let ItemUsername = 5 + let ItemAffiliateProgram = 6 let ItemIntro = 7 let ItemCommands = 8 @@ -1916,6 +1920,10 @@ 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 + items[.peerDataSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemAffiliateProgram, label: .text("Off"), additionalBadgeLabel: presentationData.strings.Settings_New, text: "Affiliate Program", icon: PresentationResourcesSettings.bot, 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))) @@ -3012,6 +3020,11 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro return } self.openBirthdayContextMenu(node: node, gesture: gesture) + }, editingOpenAffiliateProgram: { [weak self] in + guard let self else { + return + } + self.editingOpenAffiliateProgram() }, getController: { [weak self] in return self?.controller @@ -8548,6 +8561,19 @@ 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) + |> 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) + }) + } + } + private func editingOpenNameColorSetup() { if self.peerId == self.context.account.peerId { let controller = PeerNameColorScreen(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, subject: .account) diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupScreen.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupScreen.swift index 10f82b47bd..e5527d1b32 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupScreen.swift @@ -1461,20 +1461,22 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component { items: [ AnyComponentWithIdentity(id: 0, component: AnyComponent(ListItemSliderSelectorComponent( theme: environment.theme, - values: valueList.map { item in - return environment.strings.MessageTimer_Days(Int32(item)) - }, - markPositions: true, - selectedIndex: selectedInactivityIndex, - title: nil, - selectedIndexUpdated: { [weak self] index in - guard let self else { - return + content: .discrete(ListItemSliderSelectorComponent.Discrete( + values: valueList.map { item in + return environment.strings.MessageTimer_Days(Int32(item)) + }, + markPositions: true, + selectedIndex: selectedInactivityIndex, + title: nil, + selectedIndexUpdated: { [weak self] index in + guard let self else { + return + } + let index = max(0, min(valueList.count - 1, index)) + self.inactivityDays = valueList[index] + self.state?.updated(transition: .immediate) } - let index = max(0, min(valueList.count - 1, index)) - self.inactivityDays = valueList[index] - self.state?.updated(transition: .immediate) - } + )) ))) ] )), diff --git a/submodules/TelegramUI/Components/SliderComponent/Sources/SliderComponent.swift b/submodules/TelegramUI/Components/SliderComponent/Sources/SliderComponent.swift index e6b4756517..d397095b5f 100644 --- a/submodules/TelegramUI/Components/SliderComponent/Sources/SliderComponent.swift +++ b/submodules/TelegramUI/Components/SliderComponent/Sources/SliderComponent.swift @@ -7,46 +7,80 @@ import LegacyComponents import ComponentFlow public final class SliderComponent: Component { - public let valueCount: Int - public let value: Int - public let markPositions: Bool + public final class Discrete: Equatable { + public let valueCount: Int + public let value: Int + public let markPositions: Bool + public let valueUpdated: (Int) -> Void + + public init(valueCount: Int, value: Int, markPositions: Bool, valueUpdated: @escaping (Int) -> Void) { + self.valueCount = valueCount + self.value = value + self.markPositions = markPositions + self.valueUpdated = valueUpdated + } + + public static func ==(lhs: Discrete, rhs: Discrete) -> Bool { + if lhs.valueCount != rhs.valueCount { + return false + } + if lhs.value != rhs.value { + return false + } + if lhs.markPositions != rhs.markPositions { + return false + } + return true + } + } + + public final class Continuous: Equatable { + public let value: CGFloat + public let valueUpdated: (CGFloat) -> Void + + public init(value: CGFloat, valueUpdated: @escaping (CGFloat) -> Void) { + self.value = value + self.valueUpdated = valueUpdated + } + + public static func ==(lhs: Continuous, rhs: Continuous) -> Bool { + if lhs.value != rhs.value { + return false + } + return true + } + } + + public enum Content: Equatable { + case discrete(Discrete) + case continuous(Continuous) + } + + public let content: Content public let trackBackgroundColor: UIColor public let trackForegroundColor: UIColor public let knobSize: CGFloat? public let knobColor: UIColor? - public let valueUpdated: (Int) -> Void public let isTrackingUpdated: ((Bool) -> Void)? public init( - valueCount: Int, - value: Int, - markPositions: Bool, + content: Content, trackBackgroundColor: UIColor, trackForegroundColor: UIColor, knobSize: CGFloat? = nil, knobColor: UIColor? = nil, - valueUpdated: @escaping (Int) -> Void, isTrackingUpdated: ((Bool) -> Void)? = nil ) { - self.valueCount = valueCount - self.value = value - self.markPositions = markPositions + self.content = content self.trackBackgroundColor = trackBackgroundColor self.trackForegroundColor = trackForegroundColor self.knobSize = knobSize self.knobColor = knobColor - self.valueUpdated = valueUpdated self.isTrackingUpdated = isTrackingUpdated } public static func ==(lhs: SliderComponent, rhs: SliderComponent) -> Bool { - if lhs.valueCount != rhs.valueCount { - return false - } - if lhs.value != rhs.value { - return false - } - if lhs.markPositions != rhs.markPositions { + if lhs.content != rhs.content { return false } if lhs.trackBackgroundColor != rhs.trackBackgroundColor { @@ -122,10 +156,16 @@ public final class SliderComponent: Component { sliderView.minimumValue = 0.0 sliderView.startValue = 0.0 sliderView.disablesInteractiveTransitionGestureRecognizer = true - sliderView.maximumValue = CGFloat(component.valueCount - 1) - sliderView.positionsCount = component.valueCount - sliderView.useLinesForPositions = true - sliderView.markPositions = component.markPositions + + switch component.content { + case let .discrete(discrete): + sliderView.maximumValue = CGFloat(discrete.valueCount - 1) + sliderView.positionsCount = discrete.valueCount + sliderView.useLinesForPositions = true + sliderView.markPositions = discrete.markPositions + case .continuous: + sliderView.maximumValue = 1.0 + } sliderView.backgroundColor = nil sliderView.isOpaque = false @@ -162,7 +202,12 @@ public final class SliderComponent: Component { self.sliderView = sliderView self.addSubview(sliderView) } - sliderView.value = CGFloat(component.value) + switch component.content { + case let .discrete(discrete): + sliderView.value = CGFloat(discrete.value) + case let .continuous(continuous): + sliderView.value = continuous.value + } sliderView.interactionBegan = { internalIsTrackingUpdated?(true) } @@ -180,7 +225,12 @@ public final class SliderComponent: Component { guard let component = self.component, let sliderView = self.sliderView else { return } - component.valueUpdated(Int(sliderView.value)) + switch component.content { + case let .discrete(discrete): + discrete.valueUpdated(Int(sliderView.value)) + case let .continuous(continuous): + continuous.valueUpdated(sliderView.value) + } } } diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 447c7a04b8..e80775fe0c 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -74,6 +74,7 @@ import GiftOptionsScreen import GiftViewScreen import StarsIntroScreen import ContentReportScreen +import AffiliateProgramSetupScreen private final class AccountUserInterfaceInUseContext { let subscribers = Bag<(Bool) -> Void>() @@ -2828,6 +2829,14 @@ public final class SharedAccountContextImpl: SharedAccountContext { public func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal)?, botPeer: EnginePeer, chatPeer: EnginePeer?, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool, payload: String?) { openWebAppImpl(context: context, parentController: parentController, updatedPresentationData: updatedPresentationData, botPeer: botPeer, chatPeer: chatPeer, threadId: threadId, buttonText: buttonText, url: url, simple: simple, source: source, skipTermsOfService: skipTermsOfService, payload: payload) } + + public func makeAffiliateProgramSetupScreenInitialData(context: AccountContext, peerId: EnginePeer.Id) -> Signal { + return AffiliateProgramSetupScreen.content(context: context, peerId: peerId) + } + + public func makeAffiliateProgramSetupScreen(context: AccountContext, initialData: AffiliateProgramSetupScreenInitialData) -> ViewController { + return AffiliateProgramSetupScreen(context: context, initialContent: initialData as! AffiliateProgramSetupScreen.Content) + } } private func peerInfoControllerImpl(context: AccountContext, updatedPresentationData: (PresentationData, Signal)?, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, requestsContext: PeerInvitationImportersContext? = nil) -> ViewController? {