mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
1938 lines
92 KiB
Swift
1938 lines
92 KiB
Swift
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 BundleIconComponent
|
|
import Markdown
|
|
import PresentationDataUtils
|
|
import TelegramStringFormatting
|
|
import ContextUI
|
|
import AvatarNode
|
|
import PlainButtonComponent
|
|
import ToastComponent
|
|
|
|
private final class JoinAffiliateProgramScreenComponent: Component {
|
|
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
|
|
|
let context: AccountContext
|
|
let sourcePeer: EnginePeer
|
|
let commissionPermille: Int32
|
|
let programDuration: Int32?
|
|
let mode: JoinAffiliateProgramScreen.Mode
|
|
|
|
init(
|
|
context: AccountContext,
|
|
sourcePeer: EnginePeer,
|
|
commissionPermille: Int32,
|
|
programDuration: Int32?,
|
|
mode: JoinAffiliateProgramScreen.Mode
|
|
) {
|
|
self.context = context
|
|
self.sourcePeer = sourcePeer
|
|
self.commissionPermille = commissionPermille
|
|
self.programDuration = programDuration
|
|
self.mode = mode
|
|
}
|
|
|
|
static func ==(lhs: JoinAffiliateProgramScreenComponent, rhs: JoinAffiliateProgramScreenComponent) -> Bool {
|
|
if lhs.context !== rhs.context {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
private final class ScrollView: UIScrollView {
|
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
return super.hitTest(point, with: event)
|
|
}
|
|
}
|
|
|
|
private struct ItemLayout: Equatable {
|
|
var containerSize: CGSize
|
|
var containerInset: CGFloat
|
|
var bottomInset: CGFloat
|
|
var topInset: CGFloat
|
|
|
|
init(containerSize: CGSize, containerInset: CGFloat, bottomInset: CGFloat, topInset: CGFloat) {
|
|
self.containerSize = containerSize
|
|
self.containerInset = containerInset
|
|
self.bottomInset = bottomInset
|
|
self.topInset = topInset
|
|
}
|
|
}
|
|
|
|
final class View: UIView, UIScrollViewDelegate {
|
|
private let dimView: UIView
|
|
private let backgroundLayer: SimpleLayer
|
|
private let navigationBarContainer: SparseContainerView
|
|
private let navigationBackgroundView: BlurredBackgroundView
|
|
private let navigationBarSeparator: SimpleLayer
|
|
private let scrollView: ScrollView
|
|
private let scrollContentClippingView: SparseContainerView
|
|
private let scrollContentView: UIView
|
|
|
|
private let closeButton = ComponentView<Empty>()
|
|
|
|
private var toast: ComponentView<Empty>?
|
|
|
|
private let sourceAvatar = 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 var linkIconBadge: ComponentView<Empty>?
|
|
|
|
private let title = ComponentView<Empty>()
|
|
private let subtitle = ComponentView<Empty>()
|
|
private let titleTransformContainer: UIView
|
|
private let bottomPanelContainer: UIView
|
|
private let actionButton = ComponentView<Empty>()
|
|
private let bottomText = ComponentView<Empty>()
|
|
private let linkText = ComponentView<Empty>()
|
|
|
|
private let targetText = ComponentView<Empty>()
|
|
private let targetPeer = ComponentView<Empty>()
|
|
|
|
private let bottomOverscrollLimit: CGFloat
|
|
|
|
private var isFirstTimeApplyingModalFactor: Bool = true
|
|
private var ignoreScrolling: Bool = false
|
|
|
|
private var component: JoinAffiliateProgramScreenComponent?
|
|
private weak var state: EmptyComponentState?
|
|
private var environment: ViewControllerComponentContainer.Environment?
|
|
private var isUpdating: Bool = false
|
|
|
|
private var itemLayout: ItemLayout?
|
|
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?
|
|
|
|
override init(frame: CGRect) {
|
|
self.bottomOverscrollLimit = 200.0
|
|
|
|
self.dimView = UIView()
|
|
|
|
self.backgroundLayer = SimpleLayer()
|
|
self.backgroundLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
|
self.backgroundLayer.cornerRadius = 10.0
|
|
|
|
self.navigationBarContainer = SparseContainerView()
|
|
|
|
self.navigationBackgroundView = BlurredBackgroundView(color: .clear, enableBlur: true)
|
|
self.navigationBarSeparator = SimpleLayer()
|
|
|
|
self.scrollView = ScrollView()
|
|
|
|
self.scrollContentClippingView = SparseContainerView()
|
|
self.scrollContentClippingView.clipsToBounds = true
|
|
|
|
self.scrollContentView = UIView()
|
|
|
|
self.titleTransformContainer = UIView()
|
|
self.titleTransformContainer.isUserInteractionEnabled = false
|
|
|
|
self.bottomPanelContainer = UIView()
|
|
|
|
super.init(frame: frame)
|
|
|
|
self.addSubview(self.dimView)
|
|
self.layer.addSublayer(self.backgroundLayer)
|
|
|
|
self.scrollView.delaysContentTouches = true
|
|
self.scrollView.canCancelContentTouches = true
|
|
self.scrollView.clipsToBounds = false
|
|
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
|
|
self.scrollView.contentInsetAdjustmentBehavior = .never
|
|
}
|
|
if #available(iOS 13.0, *) {
|
|
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
|
|
}
|
|
self.scrollView.showsVerticalScrollIndicator = false
|
|
self.scrollView.showsHorizontalScrollIndicator = false
|
|
self.scrollView.alwaysBounceHorizontal = false
|
|
self.scrollView.alwaysBounceVertical = true
|
|
self.scrollView.scrollsToTop = false
|
|
self.scrollView.delegate = self
|
|
self.scrollView.clipsToBounds = true
|
|
|
|
self.addSubview(self.scrollContentClippingView)
|
|
self.scrollContentClippingView.addSubview(self.scrollView)
|
|
|
|
self.scrollView.addSubview(self.scrollContentView)
|
|
|
|
self.addSubview(self.navigationBarContainer)
|
|
self.addSubview(self.titleTransformContainer)
|
|
self.addSubview(self.bottomPanelContainer)
|
|
|
|
self.navigationBarContainer.addSubview(self.navigationBackgroundView)
|
|
self.navigationBarContainer.layer.addSublayer(self.navigationBarSeparator)
|
|
|
|
self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
deinit {
|
|
self.possibleTargetPeersDisposable?.dispose()
|
|
self.changeTargetPeerDisposable?.dispose()
|
|
}
|
|
|
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
if !self.ignoreScrolling {
|
|
self.updateScrolling(transition: .immediate)
|
|
}
|
|
}
|
|
|
|
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
|
|
}
|
|
|
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
if !self.bounds.contains(point) {
|
|
return nil
|
|
}
|
|
if !self.backgroundLayer.frame.contains(point) {
|
|
return self.dimView
|
|
}
|
|
|
|
if let result = self.navigationBarContainer.hitTest(self.convert(point, to: self.navigationBarContainer), with: event) {
|
|
return result
|
|
}
|
|
|
|
let result = super.hitTest(point, with: event)
|
|
return result
|
|
}
|
|
|
|
@objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
|
|
if case .ended = recognizer.state {
|
|
guard let environment = self.environment, let controller = environment.controller() else {
|
|
return
|
|
}
|
|
controller.dismiss()
|
|
}
|
|
}
|
|
|
|
private func updateScrolling(transition: ComponentTransition) {
|
|
guard let environment = self.environment, let controller = environment.controller(), let itemLayout = self.itemLayout else {
|
|
return
|
|
}
|
|
var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset
|
|
|
|
let titleCenterY: CGFloat = -itemLayout.topInset + itemLayout.containerInset + 54.0 * 0.5
|
|
|
|
let titleTransformDistance: CGFloat = 20.0
|
|
let titleY: CGFloat = max(titleCenterY, self.titleTransformContainer.center.y + topOffset + itemLayout.containerInset)
|
|
|
|
transition.setSublayerTransform(view: self.titleTransformContainer, transform: CATransform3DMakeTranslation(0.0, titleY - self.titleTransformContainer.center.y, 0.0))
|
|
|
|
let titleYDistance: CGFloat = titleY - titleCenterY
|
|
let titleTransformFraction: CGFloat = 1.0 - max(0.0, min(1.0, titleYDistance / titleTransformDistance))
|
|
let titleMinScale: CGFloat = 17.0 / 24.0
|
|
let titleScale: CGFloat = 1.0 * (1.0 - titleTransformFraction) + titleMinScale * titleTransformFraction
|
|
if let titleView = self.title.view {
|
|
transition.setScale(view: titleView, scale: titleScale)
|
|
}
|
|
|
|
let navigationAlpha: CGFloat = titleTransformFraction
|
|
transition.setAlpha(view: self.navigationBackgroundView, alpha: navigationAlpha)
|
|
transition.setAlpha(layer: self.navigationBarSeparator, alpha: navigationAlpha)
|
|
|
|
topOffset = max(0.0, topOffset)
|
|
transition.setTransform(layer: self.backgroundLayer, transform: CATransform3DMakeTranslation(0.0, topOffset + itemLayout.containerInset, 0.0))
|
|
|
|
if let toastView = self.toast?.view {
|
|
let toastY = topOffset + itemLayout.containerInset - toastView.bounds.height - 16.0
|
|
transition.setTransform(layer: toastView.layer, transform: CATransform3DMakeTranslation(0.0, toastY, 0.0))
|
|
|
|
let toastAlpha: CGFloat
|
|
if toastY < itemLayout.containerInset {
|
|
toastAlpha = 0.0
|
|
} else {
|
|
toastAlpha = 1.0
|
|
}
|
|
if toastAlpha != toastView.alpha {
|
|
ComponentTransition.easeInOut(duration: 0.2).setAlpha(view: toastView, alpha: toastAlpha)
|
|
}
|
|
}
|
|
|
|
transition.setPosition(view: self.navigationBarContainer, position: CGPoint(x: 0.0, y: topOffset + itemLayout.containerInset))
|
|
|
|
let topOffsetDistance: CGFloat = min(200.0, floor(itemLayout.containerSize.height * 0.25))
|
|
self.topOffsetDistance = topOffsetDistance
|
|
var topOffsetFraction = topOffset / topOffsetDistance
|
|
topOffsetFraction = max(0.0, min(1.0, topOffsetFraction))
|
|
|
|
let transitionFactor: CGFloat = 1.0 - topOffsetFraction
|
|
var modalOverlayTransition = transition
|
|
if self.isFirstTimeApplyingModalFactor {
|
|
self.isFirstTimeApplyingModalFactor = false
|
|
modalOverlayTransition = .spring(duration: 0.5)
|
|
}
|
|
if self.isUpdating {
|
|
DispatchQueue.main.async { [weak controller] in
|
|
guard let controller else {
|
|
return
|
|
}
|
|
controller.updateModalStyleOverlayTransitionFactor(transitionFactor, transition: modalOverlayTransition.containedViewLayoutTransition)
|
|
}
|
|
} else {
|
|
controller.updateModalStyleOverlayTransitionFactor(transitionFactor, transition: modalOverlayTransition.containedViewLayoutTransition)
|
|
}
|
|
}
|
|
|
|
func animateIn() {
|
|
self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
|
let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY
|
|
self.scrollContentClippingView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
|
self.backgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
|
self.navigationBarContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
|
self.titleTransformContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
|
self.bottomPanelContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
|
if let toastView = self.toast?.view {
|
|
toastView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
|
toastView.layer.animateAlpha(from: 0.0, to: toastView.alpha, duration: 0.15)
|
|
}
|
|
}
|
|
|
|
func animateOut(completion: @escaping () -> Void) {
|
|
let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY
|
|
|
|
self.dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
|
|
self.scrollContentClippingView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in
|
|
completion()
|
|
})
|
|
self.backgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
|
|
self.navigationBarContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
|
|
self.titleTransformContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
|
|
self.bottomPanelContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
|
|
if let toastView = self.toast?.view {
|
|
toastView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
|
|
toastView.layer.animateAlpha(from: toastView.alpha, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
|
}
|
|
|
|
if let environment = self.environment, let controller = environment.controller() {
|
|
controller.updateModalStyleOverlayTransitionFactor(0.0, transition: .animated(duration: 0.3, curve: .easeInOut))
|
|
}
|
|
}
|
|
|
|
private func displayTargetSelectionMenu(sourceView: UIView) {
|
|
guard let component = self.component, let environment = self.environment, let controller = environment.controller() else {
|
|
return
|
|
}
|
|
guard let currentTargetPeer = self.currentTargetPeer else {
|
|
return
|
|
}
|
|
|
|
var items: [ContextMenuItem] = []
|
|
|
|
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 })
|
|
|
|
let peers: [EnginePeer] = self.possibleTargetPeers.isEmpty ? [
|
|
currentTargetPeer
|
|
] : self.possibleTargetPeers
|
|
|
|
let avatarSize = CGSize(width: 30.0, height: 30.0)
|
|
|
|
for peer in peers {
|
|
let peerLabel: String
|
|
if peer.id == component.context.account.peerId {
|
|
peerLabel = "personal account"
|
|
} else if case .channel = peer {
|
|
peerLabel = "channel"
|
|
} else {
|
|
peerLabel = "bot"
|
|
}
|
|
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, 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)
|
|
})))
|
|
}
|
|
|
|
let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView, actionsOnTop: true)), items: .single(ContextController.Items(id: AnyHashable(0), content: .list(items))), gesture: nil)
|
|
controller.presentInGlobalOverlay(contextController)
|
|
}
|
|
|
|
func update(component: JoinAffiliateProgramScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
|
|
self.isUpdating = true
|
|
defer {
|
|
self.isUpdating = false
|
|
}
|
|
|
|
let environment = environment[ViewControllerComponentContainer.Environment.self].value
|
|
let themeUpdated = self.environment?.theme !== environment.theme
|
|
|
|
let resetScrolling = self.scrollView.bounds.width != availableSize.width
|
|
|
|
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):
|
|
self.currentTargetPeer = join.initialTargetPeer
|
|
loadPossibleTargetPeers = join.canSelectTargetPeer
|
|
case let .active(active):
|
|
self.currentTargetPeer = active.targetPeer
|
|
loadPossibleTargetPeers = true
|
|
}
|
|
|
|
if loadPossibleTargetPeers {
|
|
self.possibleTargetPeersDisposable = (component.context.engine.peers.getPossibleStarRefBotTargets()
|
|
|> deliverOnMainQueue).startStrict(next: { [weak self] result in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.possibleTargetPeers = result
|
|
})
|
|
}
|
|
}
|
|
|
|
self.component = component
|
|
self.state = state
|
|
self.environment = environment
|
|
|
|
if themeUpdated {
|
|
self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
|
|
self.backgroundLayer.backgroundColor = environment.theme.list.plainBackgroundColor.cgColor
|
|
|
|
self.navigationBackgroundView.updateColor(color: environment.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate)
|
|
self.navigationBarSeparator.backgroundColor = environment.theme.rootController.navigationBar.separatorColor.cgColor
|
|
}
|
|
|
|
transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize))
|
|
|
|
var contentHeight: CGFloat = 0.0
|
|
|
|
let closeImage: UIImage
|
|
if let image = self.cachedCloseImage, !themeUpdated {
|
|
closeImage = image
|
|
} else {
|
|
closeImage = generateCloseButtonImage(backgroundColor: environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.05), foregroundColor: environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.4))!
|
|
self.cachedCloseImage = closeImage
|
|
}
|
|
|
|
let closeButtonSize = self.closeButton.update(
|
|
transition: transition,
|
|
component: AnyComponent(Button(
|
|
content: AnyComponent(Image(image: closeImage, size: closeImage.size)),
|
|
action: { [weak self] in
|
|
guard let self, let controller = self.environment?.controller() else {
|
|
return
|
|
}
|
|
controller.dismiss()
|
|
}
|
|
).minSize(CGSize(width: 62.0, height: 56.0))),
|
|
environment: {},
|
|
containerSize: CGSize(width: 100.0, height: 100.0)
|
|
)
|
|
let closeButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - environment.safeInsets.right - closeButtonSize.width, y: 0.0), size: closeButtonSize)
|
|
if let closeButtonView = self.closeButton.view {
|
|
if closeButtonView.superview == nil {
|
|
self.navigationBarContainer.addSubview(closeButtonView)
|
|
}
|
|
transition.setFrame(view: closeButtonView, frame: closeButtonFrame)
|
|
}
|
|
|
|
let containerInset: CGFloat = environment.statusBarHeight + 10.0
|
|
|
|
let clippingY: CGFloat
|
|
|
|
if let currentTargetPeer = self.currentTargetPeer, case .join = currentMode {
|
|
contentHeight += 34.0
|
|
|
|
let sourceAvatarSize = self.sourceAvatar.update(
|
|
transition: transition,
|
|
component: AnyComponent(AvatarComponent(
|
|
context: component.context,
|
|
peer: component.sourcePeer
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: 78.0, height: 78.0)
|
|
)
|
|
let targetAvatarSize = self.targetAvatar.update(
|
|
transition: transition,
|
|
component: AnyComponent(AvatarComponent(
|
|
context: component.context,
|
|
peer: currentTargetPeer
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: 78.0, height: 78.0)
|
|
)
|
|
|
|
let avatarSpacing: CGFloat = 41.0
|
|
|
|
let sourceAvatarFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - sourceAvatarSize.width - avatarSpacing - targetAvatarSize.width) * 0.5), y: contentHeight), size: sourceAvatarSize)
|
|
let targetAvatarFrame = CGRect(origin: CGPoint(x: sourceAvatarFrame.maxX + avatarSpacing, y: contentHeight), size: targetAvatarSize)
|
|
|
|
if let sourceAvatarView = self.sourceAvatar.view {
|
|
if sourceAvatarView.superview == nil {
|
|
self.scrollContentView.addSubview(sourceAvatarView)
|
|
}
|
|
transition.setFrame(view: sourceAvatarView, frame: sourceAvatarFrame)
|
|
}
|
|
if let targetAvatarView = self.targetAvatar.view {
|
|
if targetAvatarView.superview == nil {
|
|
self.scrollContentView.addSubview(targetAvatarView)
|
|
}
|
|
transition.setFrame(view: targetAvatarView, frame: targetAvatarFrame)
|
|
}
|
|
|
|
let badgeIconInset: CGFloat = 2.0
|
|
let targetAvatarBadgeSize = self.targetAvatarBadge.update(
|
|
transition: transition,
|
|
component: AnyComponent(BorderedBadgeComponent(
|
|
backgroundColor: UIColor(rgb: 0x8A7AFF),
|
|
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
|
|
)),
|
|
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)
|
|
if let targetAvatarBadgeView = self.targetAvatarBadge.view {
|
|
if targetAvatarBadgeView.superview == nil {
|
|
self.scrollContentView.addSubview(targetAvatarBadgeView)
|
|
}
|
|
transition.setFrame(view: targetAvatarBadgeView, frame: targetAvatarBadgeFrame)
|
|
}
|
|
|
|
contentHeight += sourceAvatarSize.height + 16.0
|
|
|
|
if self.sourceTargetArrow.image == nil || themeUpdated {
|
|
self.sourceTargetArrow.image = generateImage(CGSize(width: 12.0, height: 22.0), rotatedContext: { size, context in
|
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
|
context.setStrokeColor(environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.2).cgColor)
|
|
|
|
let lineWidth: CGFloat = 3.0
|
|
context.setLineWidth(lineWidth)
|
|
context.setLineJoin(.round)
|
|
context.setLineCap(.round)
|
|
|
|
context.move(to: CGPoint(x: lineWidth * 0.5, y: lineWidth * 0.5))
|
|
context.addLine(to: CGPoint(x: size.width - lineWidth * 0.5, y: size.height * 0.5))
|
|
context.addLine(to: CGPoint(x: lineWidth * 0.5, y: size.height - lineWidth * 0.5))
|
|
context.strokePath()
|
|
})
|
|
}
|
|
if let sourceTargetArrowSize = self.sourceTargetArrow.image?.size {
|
|
let sourceTargetArrowFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - sourceTargetArrowSize.width) * 0.5), y: sourceAvatarFrame.minY + floor((sourceAvatarFrame.height - sourceTargetArrowSize.height) * 0.5)), size: sourceTargetArrowSize)
|
|
|
|
if self.sourceTargetArrow.superview == nil {
|
|
self.scrollContentView.addSubview(self.sourceTargetArrow)
|
|
}
|
|
transition.setFrame(view: self.sourceTargetArrow, frame: sourceTargetArrowFrame)
|
|
}
|
|
} else if case let .active(active) = component.mode {
|
|
contentHeight += 31.0
|
|
|
|
let linkIconBackgroundSize = self.linkIconBackground.update(
|
|
transition: transition,
|
|
component: AnyComponent(FilledRoundedRectangleComponent(
|
|
color: environment.theme.list.itemCheckColors.fillColor,
|
|
cornerRadius: .minEdge,
|
|
smoothCorners: false
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: 90.0, height: 90.0)
|
|
)
|
|
let linkIconBackgroundFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - linkIconBackgroundSize.width) * 0.5), y: contentHeight), size: linkIconBackgroundSize)
|
|
if let linkIconBackgroundView = self.linkIconBackground.view {
|
|
if linkIconBackgroundView.superview == nil {
|
|
self.scrollContentView.addSubview(linkIconBackgroundView)
|
|
}
|
|
transition.setFrame(view: linkIconBackgroundView, frame: linkIconBackgroundFrame)
|
|
}
|
|
|
|
let linkIconSize = self.linkIcon.update(
|
|
transition: transition,
|
|
component: AnyComponent(BundleIconComponent(
|
|
name: "Chat/Links/LargeLink",
|
|
tintColor: environment.theme.list.itemCheckColors.foregroundColor,
|
|
scaleFactor: 0.88
|
|
)),
|
|
environment: {},
|
|
containerSize: linkIconBackgroundSize
|
|
)
|
|
let linkIconFrame = CGRect(origin: CGPoint(x: linkIconBackgroundFrame.minX + floor((linkIconBackgroundFrame.width - linkIconSize.width) * 0.5), y: linkIconBackgroundFrame.minY + floor((linkIconBackgroundFrame.height - linkIconSize.height) * 0.5)), size: linkIconSize)
|
|
if let linkIconView = self.linkIcon.view {
|
|
if linkIconView.superview == nil {
|
|
self.scrollContentView.addSubview(linkIconView)
|
|
}
|
|
transition.setFrame(view: linkIconView, frame: linkIconFrame)
|
|
}
|
|
|
|
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),
|
|
cutoutColor: environment.theme.list.plainBackgroundColor,
|
|
content: AnyComponent(HStack([
|
|
AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent(
|
|
name: "Stories/RepostUser",
|
|
tintColor: environment.theme.list.itemCheckColors.foregroundColor,
|
|
scaleFactor: 1.0
|
|
))),
|
|
AnyComponentWithIdentity(id: 1, component: AnyComponent(MultilineTextComponent(
|
|
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),
|
|
cutoutWidth: 1.0 + UIScreenPixel)
|
|
),
|
|
environment: {},
|
|
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 = linkIconBadge.view {
|
|
if linkIconBadgeView.superview == nil {
|
|
self.scrollContentView.addSubview(linkIconBadgeView)
|
|
}
|
|
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 durationTitle: String
|
|
if let durationMonths = component.programDuration {
|
|
durationTitle = timeIntervalString(strings: environment.strings, value: durationMonths * (24 * 60 * 60))
|
|
} else {
|
|
durationTitle = "lifetime"
|
|
}
|
|
|
|
let titleString: String
|
|
let subtitleString: String
|
|
let termsString: String
|
|
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)**."
|
|
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"
|
|
let timeString: String
|
|
if component.programDuration == nil {
|
|
timeString = "**forever** after they follow your link."
|
|
} else {
|
|
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.bot.participants == 0 {
|
|
termsString = "No one opened \(component.sourcePeer.compactDisplayTitle) through this link yet."
|
|
} else if active.bot.participants == 1 {
|
|
termsString = "1 user opened \(component.sourcePeer.compactDisplayTitle) through this link."
|
|
} else {
|
|
termsString = "\(active.bot.participants) users opened \(component.sourcePeer.compactDisplayTitle) through this link."
|
|
}
|
|
}
|
|
let titleSize = self.title.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(MultilineTextComponent(
|
|
text: .plain(NSAttributedString(string: titleString, font: Font.bold(24.0), textColor: environment.theme.list.itemPrimaryTextColor))
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
|
|
)
|
|
let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: contentHeight), size: titleSize)
|
|
if let titleView = 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 + 14.0
|
|
|
|
let navigationBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: 54.0))
|
|
transition.setFrame(view: self.navigationBackgroundView, frame: navigationBackgroundFrame)
|
|
self.navigationBackgroundView.update(size: navigationBackgroundFrame.size, cornerRadius: 10.0, maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner], transition: transition.containedViewLayoutTransition)
|
|
transition.setFrame(layer: self.navigationBarSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 54.0), size: CGSize(width: availableSize.width, height: UIScreenPixel)))
|
|
|
|
let subtitleSize = self.subtitle.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(MultilineTextComponent(
|
|
text: .markdown(
|
|
text: subtitleString,
|
|
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)
|
|
}
|
|
)
|
|
),
|
|
horizontalAlignment: .center,
|
|
maximumNumberOfLines: 0,
|
|
lineSpacing: 0.2
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.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.scrollContentView.addSubview(subtitleView)
|
|
}
|
|
transition.setPosition(view: subtitleView, position: subtitleFrame.center)
|
|
subtitleView.bounds = CGRect(origin: CGPoint(), size: subtitleFrame.size)
|
|
}
|
|
contentHeight += subtitleSize.height + 23.0
|
|
|
|
var displayTargetPeer = false
|
|
var isTargetPeerSelectable = false
|
|
switch component.mode {
|
|
case let .join(join):
|
|
displayTargetPeer = join.canSelectTargetPeer
|
|
isTargetPeerSelectable = join.canSelectTargetPeer
|
|
case .active:
|
|
displayTargetPeer = true
|
|
isTargetPeerSelectable = true
|
|
}
|
|
|
|
if displayTargetPeer {
|
|
let targetTextSize = self.targetText.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(MultilineTextComponent(
|
|
text: .plain(NSAttributedString(string: "Commission will be sent to:", font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor)),
|
|
horizontalAlignment: .center,
|
|
maximumNumberOfLines: 0,
|
|
lineSpacing: 0.2
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
|
|
)
|
|
let targetTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - targetTextSize.width) * 0.5), y: contentHeight), size: targetTextSize)
|
|
if let targetTextView = self.targetText.view {
|
|
if targetTextView.superview == nil {
|
|
self.scrollContentView.addSubview(targetTextView)
|
|
}
|
|
transition.setPosition(view: targetTextView, position: targetTextFrame.center)
|
|
targetTextView.bounds = CGRect(origin: CGPoint(), size: targetTextFrame.size)
|
|
}
|
|
contentHeight += targetTextSize.height + 12.0
|
|
|
|
if let currentTargetPeer = self.currentTargetPeer {
|
|
let targetPeerSize = self.targetPeer.update(
|
|
transition: transition,
|
|
component: AnyComponent(PeerBadgeComponent(
|
|
context: component.context,
|
|
theme: environment.theme,
|
|
strings: environment.strings,
|
|
peer: currentTargetPeer,
|
|
action: isTargetPeerSelectable ? { [weak self] sourceView in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.displayTargetSelectionMenu(sourceView: sourceView)
|
|
} : nil
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
|
|
)
|
|
let targetPeerFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - targetPeerSize.width) * 0.5), y: contentHeight), size: targetPeerSize)
|
|
if let targetPeerView = self.targetPeer.view {
|
|
if targetPeerView.superview == nil {
|
|
self.scrollContentView.addSubview(targetPeerView)
|
|
}
|
|
transition.setFrame(view: targetPeerView, frame: targetPeerFrame)
|
|
}
|
|
contentHeight += targetPeerSize.height
|
|
contentHeight += 20.0
|
|
}
|
|
}
|
|
contentHeight += 12.0
|
|
|
|
if case let .active(active) = currentMode {
|
|
var cleanLink = active.bot.url
|
|
let removePrefixes: [String] = ["http://", "https://"]
|
|
for prefix in removePrefixes {
|
|
if cleanLink.hasPrefix(prefix) {
|
|
cleanLink = String(cleanLink[cleanLink.index(cleanLink.startIndex, offsetBy: prefix.count)...])
|
|
}
|
|
}
|
|
let linkTextSize = self.linkText.update(
|
|
transition: transition,
|
|
component: AnyComponent(PlainButtonComponent(
|
|
content: AnyComponent(MultilineTextComponent(
|
|
text: .plain(NSAttributedString(string: cleanLink, font: Font.regular(17.0), textColor: environment.theme.list.itemInputField.primaryColor)),
|
|
truncationType: .middle
|
|
)),
|
|
background: AnyComponent(FilledRoundedRectangleComponent(
|
|
color: environment.theme.list.itemInputField.backgroundColor,
|
|
cornerRadius: .value(8.0),
|
|
smoothCorners: true
|
|
)),
|
|
effectAlignment: .center,
|
|
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 {
|
|
return
|
|
}
|
|
self.environment?.controller()?.dismiss()
|
|
active.copyLink(active.bot)
|
|
},
|
|
animateAlpha: true,
|
|
animateScale: false,
|
|
animateContents: false
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0)
|
|
)
|
|
let linkTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - linkTextSize.width) * 0.5), y: contentHeight), size: linkTextSize)
|
|
if let linkTextView = self.linkText.view {
|
|
if linkTextView.superview == nil {
|
|
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 currentMode {
|
|
case .join:
|
|
actionButtonTitle = "Join Program"
|
|
case .active:
|
|
actionButtonTitle = "Copy Link"
|
|
}
|
|
let actionButtonSize = self.actionButton.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.9)
|
|
),
|
|
content: AnyComponentWithIdentity(
|
|
id: AnyHashable(0),
|
|
component: AnyComponent(ButtonTextContentComponent(
|
|
text: actionButtonTitle,
|
|
badge: 0,
|
|
textColor: environment.theme.list.itemCheckColors.foregroundColor,
|
|
badgeBackground: environment.theme.list.itemCheckColors.foregroundColor,
|
|
badgeForeground: environment.theme.list.itemCheckColors.fillColor
|
|
))
|
|
),
|
|
isEnabled: true,
|
|
displaysProgress: false,
|
|
action: { [weak self] in
|
|
guard let self, let component = self.component else {
|
|
return
|
|
}
|
|
self.environment?.controller()?.dismiss()
|
|
|
|
switch component.mode {
|
|
case let .join(join):
|
|
if let currentTargetPeer = self.currentTargetPeer {
|
|
join.completion(currentTargetPeer)
|
|
}
|
|
case let .active(active):
|
|
active.copyLink(active.bot)
|
|
}
|
|
}
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0)
|
|
)
|
|
|
|
let bottomTextSize = self.bottomText.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(MultilineTextComponent(
|
|
text: .markdown(
|
|
text: termsString,
|
|
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 bottomTextSpacing: CGFloat = 10.0
|
|
let bottomPanelHeight = 10.0 + environment.safeInsets.bottom + actionButtonSize.height + bottomTextSpacing + bottomTextSize.height
|
|
|
|
let bottomPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelHeight), size: CGSize(width: availableSize.width, height: bottomPanelHeight))
|
|
transition.setFrame(view: self.bottomPanelContainer, frame: bottomPanelFrame)
|
|
|
|
let actionButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: 0.0), size: actionButtonSize)
|
|
if let actionButtonView = self.actionButton.view {
|
|
if actionButtonView.superview == nil {
|
|
self.bottomPanelContainer.addSubview(actionButtonView)
|
|
}
|
|
transition.setFrame(view: actionButtonView, frame: actionButtonFrame)
|
|
}
|
|
|
|
let bottomTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - bottomTextSize.width) * 0.5), y: actionButtonFrame.maxY + bottomTextSpacing), size: bottomTextSize)
|
|
if let bottomTextView = self.bottomText.view {
|
|
if bottomTextView.superview == nil {
|
|
self.bottomPanelContainer.addSubview(bottomTextView)
|
|
}
|
|
transition.setPosition(view: bottomTextView, position: bottomTextFrame.center)
|
|
bottomTextView.bounds = CGRect(origin: CGPoint(), size: bottomTextFrame.size)
|
|
}
|
|
|
|
contentHeight += bottomPanelHeight
|
|
|
|
clippingY = bottomPanelFrame.minY - 8.0
|
|
|
|
let topInset: CGFloat = max(0.0, availableSize.height - containerInset - contentHeight)
|
|
|
|
let scrollContentHeight = max(topInset + contentHeight + containerInset, availableSize.height - containerInset)
|
|
|
|
//self.scrollContentClippingView.layer.cornerRadius = 10.0
|
|
|
|
self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, bottomInset: environment.safeInsets.bottom, topInset: topInset)
|
|
|
|
if case .active = currentMode {
|
|
let toast: ComponentView<Empty>
|
|
if let current = self.toast {
|
|
toast = current
|
|
} else {
|
|
toast = ComponentView()
|
|
self.toast = toast
|
|
}
|
|
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)
|
|
let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
|
|
let toastSize = toast.update(
|
|
transition: transition,
|
|
component: AnyComponent(ToastContentComponent(
|
|
icon: AnyComponent(AvatarComponent(
|
|
context: component.context,
|
|
peer: component.sourcePeer,
|
|
size: CGSize(width: 30.0, height: 30.0)
|
|
)),
|
|
content: AnyComponent(VStack([
|
|
AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent(
|
|
text: .markdown(text: "Program joined", attributes: MarkdownAttributes(body: bold, bold: bold, link: body, linkAttribute: { _ in nil })),
|
|
maximumNumberOfLines: 0
|
|
))),
|
|
AnyComponentWithIdentity(id: 1, component: AnyComponent(MultilineTextComponent(
|
|
text: .markdown(text: "You can now copy the referral link.", attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in nil })),
|
|
maximumNumberOfLines: 0
|
|
)))
|
|
], alignment: .left, spacing: 6.0)),
|
|
insets: UIEdgeInsets(top: 10.0, left: 12.0, bottom: 10.0, right: 10.0),
|
|
iconSpacing: 12.0
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width - environment.safeInsets.left - environment.safeInsets.right - 12.0 * 2.0, height: 1000.0)
|
|
)
|
|
let toastFrame = CGRect(origin: CGPoint(x: environment.safeInsets.left + 12.0, y: 0.0), size: toastSize)
|
|
if let toastView = toast.view {
|
|
if toastView.superview == nil {
|
|
self.addSubview(toastView)
|
|
}
|
|
transition.setPosition(view: toastView, position: toastFrame.center)
|
|
transition.setBounds(view: toastView, bounds: CGRect(origin: CGPoint(), size: toastFrame.size))
|
|
}
|
|
}
|
|
|
|
transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + containerInset), size: CGSize(width: availableSize.width, height: contentHeight)))
|
|
|
|
transition.setPosition(layer: self.backgroundLayer, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0))
|
|
transition.setBounds(layer: self.backgroundLayer, bounds: CGRect(origin: CGPoint(), size: availableSize))
|
|
|
|
let scrollClippingFrame = CGRect(origin: CGPoint(x: sideInset, y: containerInset), size: CGSize(width: availableSize.width - sideInset * 2.0, height: clippingY - containerInset))
|
|
transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center)
|
|
transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size))
|
|
|
|
self.ignoreScrolling = true
|
|
let previousBounds = self.scrollView.bounds
|
|
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height)))
|
|
let contentSize = CGSize(width: availableSize.width, height: scrollContentHeight)
|
|
if contentSize != self.scrollView.contentSize {
|
|
self.scrollView.contentSize = contentSize
|
|
}
|
|
if resetScrolling {
|
|
self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: availableSize)
|
|
} else {
|
|
if !previousBounds.isEmpty, !transition.animation.isImmediate {
|
|
let bounds = self.scrollView.bounds
|
|
if bounds.maxY != previousBounds.maxY {
|
|
let offsetY = previousBounds.maxY - bounds.maxY
|
|
transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true)
|
|
}
|
|
}
|
|
}
|
|
self.ignoreScrolling = false
|
|
self.updateScrolling(transition: transition)
|
|
|
|
return availableSize
|
|
}
|
|
}
|
|
|
|
func makeView() -> View {
|
|
return View(frame: CGRect())
|
|
}
|
|
|
|
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
|
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
|
}
|
|
}
|
|
|
|
public class JoinAffiliateProgramScreen: ViewControllerComponentContainer {
|
|
public typealias Mode = JoinAffiliateProgramScreenMode
|
|
|
|
private let context: AccountContext
|
|
|
|
private var isDismissed: Bool = false
|
|
|
|
public init(
|
|
context: AccountContext,
|
|
sourcePeer: EnginePeer,
|
|
commissionPermille: Int32,
|
|
programDuration: Int32?,
|
|
mode: Mode
|
|
) {
|
|
self.context = context
|
|
|
|
super.init(context: context, component: JoinAffiliateProgramScreenComponent(
|
|
context: context,
|
|
sourcePeer: sourcePeer,
|
|
commissionPermille: commissionPermille,
|
|
programDuration: programDuration,
|
|
mode: mode
|
|
), navigationBarAppearance: .none)
|
|
|
|
self.statusBar.statusBarStyle = .Ignore
|
|
self.navigationPresentation = .flatModal
|
|
self.blocksBackgroundWhenInOverlay = true
|
|
}
|
|
|
|
required public init(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
deinit {
|
|
}
|
|
|
|
override public func viewDidAppear(_ animated: Bool) {
|
|
super.viewDidAppear(animated)
|
|
|
|
self.view.disablesInteractiveModalDismiss = true
|
|
|
|
if let componentView = self.node.hostView.componentView as? JoinAffiliateProgramScreenComponent.View {
|
|
componentView.animateIn()
|
|
}
|
|
}
|
|
|
|
override public func dismiss(completion: (() -> Void)? = nil) {
|
|
if !self.isDismissed {
|
|
self.isDismissed = true
|
|
|
|
if let componentView = self.node.hostView.componentView as? JoinAffiliateProgramScreenComponent.View {
|
|
componentView.animateOut(completion: { [weak self] in
|
|
completion?()
|
|
self?.dismiss(animated: false)
|
|
})
|
|
} else {
|
|
self.dismiss(animated: false)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private final class PeerBadgeComponent: Component {
|
|
let context: AccountContext
|
|
let theme: PresentationTheme
|
|
let strings: PresentationStrings
|
|
let peer: EnginePeer
|
|
let action: ((UIView) -> Void)?
|
|
|
|
init(
|
|
context: AccountContext,
|
|
theme: PresentationTheme,
|
|
strings: PresentationStrings,
|
|
peer: EnginePeer,
|
|
action: ((UIView) -> Void)?
|
|
) {
|
|
self.context = context
|
|
self.theme = theme
|
|
self.strings = strings
|
|
self.peer = peer
|
|
self.action = action
|
|
}
|
|
|
|
static func ==(lhs: PeerBadgeComponent, rhs: PeerBadgeComponent) -> Bool {
|
|
if lhs.context !== rhs.context {
|
|
return false
|
|
}
|
|
if lhs.theme !== rhs.theme {
|
|
return false
|
|
}
|
|
if lhs.strings !== rhs.strings {
|
|
return false
|
|
}
|
|
if lhs.peer != rhs.peer {
|
|
return false
|
|
}
|
|
if (lhs.action == nil) != (rhs.action == nil) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
final class View: HighlightableButton {
|
|
private let background = ComponentView<Empty>()
|
|
private let title = ComponentView<Empty>()
|
|
private var avatarNode: AvatarNode?
|
|
private var selectorIcon: ComponentView<Empty>?
|
|
|
|
private var component: PeerBadgeComponent?
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
|
|
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
@objc private func pressed() {
|
|
guard let component = self.component else {
|
|
return
|
|
}
|
|
component.action?(self)
|
|
}
|
|
|
|
func update(component: PeerBadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
|
self.component = component
|
|
|
|
self.isEnabled = component.action != nil
|
|
|
|
let height: CGFloat = 32.0
|
|
let avatarPadding: CGFloat = 1.0
|
|
|
|
let avatarDiameter = height - avatarPadding * 2.0
|
|
let avatarTextSpacing: CGFloat = 4.0
|
|
let rightTextInset: CGFloat = component.action != nil ? 26.0 : 12.0
|
|
|
|
let titleSize = self.title.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(MultilineTextComponent(
|
|
text: .plain(NSAttributedString(string: component.peer.displayTitle(strings: component.strings, displayOrder: .firstLast), font: Font.medium(15.0), textColor: component.action != nil ? component.theme.list.itemInputField.primaryColor : component.theme.list.itemInputField.primaryColor))
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width - avatarPadding - avatarDiameter - avatarTextSpacing - rightTextInset, height: height)
|
|
)
|
|
let titleFrame = CGRect(origin: CGPoint(x: avatarPadding + avatarDiameter + avatarTextSpacing, y: floorToScreenPixels((height - titleSize.height) * 0.5)), size: titleSize)
|
|
if let titleView = self.title.view {
|
|
if titleView.superview == nil {
|
|
titleView.isUserInteractionEnabled = false
|
|
self.addSubview(titleView)
|
|
}
|
|
titleView.frame = titleFrame
|
|
}
|
|
|
|
let avatarNode: AvatarNode
|
|
if let current = self.avatarNode {
|
|
avatarNode = current
|
|
} else {
|
|
avatarNode = AvatarNode(font: avatarPlaceholderFont(size: floor(avatarDiameter * 0.5)))
|
|
avatarNode.isUserInteractionEnabled = false
|
|
avatarNode.displaysAsynchronously = false
|
|
self.avatarNode = avatarNode
|
|
self.addSubview(avatarNode.view)
|
|
}
|
|
|
|
let avatarFrame = CGRect(origin: CGPoint(x: avatarPadding, y: avatarPadding), size: CGSize(width: avatarDiameter, height: avatarDiameter))
|
|
avatarNode.frame = avatarFrame
|
|
avatarNode.updateSize(size: avatarFrame.size)
|
|
avatarNode.setPeer(context: component.context, theme: component.theme, peer: component.peer)
|
|
|
|
let size = CGSize(width: avatarPadding + avatarDiameter + avatarTextSpacing + titleSize.width + rightTextInset, height: height)
|
|
|
|
if component.action != nil {
|
|
let selectorIcon: ComponentView<Empty>
|
|
if let current = self.selectorIcon {
|
|
selectorIcon = current
|
|
} else {
|
|
selectorIcon = ComponentView()
|
|
self.selectorIcon = selectorIcon
|
|
}
|
|
let selectorIconSize = selectorIcon.update(
|
|
transition: transition,
|
|
component: AnyComponent(BundleIconComponent(
|
|
name: "Item List/ContextDisclosureArrow", tintColor: component.theme.list.itemAccentColor)),
|
|
environment: {},
|
|
containerSize: CGSize(width: 10.0, height: 10.0)
|
|
)
|
|
let selectorIconFrame = CGRect(origin: CGPoint(x: size.width - 8.0 - selectorIconSize.width, y: floorToScreenPixels((size.height - selectorIconSize.height) * 0.5)), size: selectorIconSize)
|
|
if let selectorIconView = selectorIcon.view {
|
|
if selectorIconView.superview == nil {
|
|
selectorIconView.isUserInteractionEnabled = false
|
|
self.addSubview(selectorIconView)
|
|
}
|
|
transition.setFrame(view: selectorIconView, frame: selectorIconFrame)
|
|
}
|
|
} else if let selectorIcon = self.selectorIcon {
|
|
self.selectorIcon = nil
|
|
selectorIcon.view?.removeFromSuperview()
|
|
}
|
|
|
|
let _ = self.background.update(
|
|
transition: transition,
|
|
component: AnyComponent(FilledRoundedRectangleComponent(
|
|
color: component.action != nil ? component.theme.list.itemAccentColor.withMultipliedAlpha(0.1) : component.theme.list.itemInputField.backgroundColor,
|
|
cornerRadius: .minEdge,
|
|
smoothCorners: false
|
|
)),
|
|
environment: {},
|
|
containerSize: size
|
|
)
|
|
if let backgroundView = self.background.view {
|
|
if backgroundView.superview == nil {
|
|
backgroundView.isUserInteractionEnabled = false
|
|
self.insertSubview(backgroundView, at: 0)
|
|
}
|
|
transition.setFrame(view: backgroundView, frame: CGRect(origin: CGPoint(), size: size))
|
|
}
|
|
|
|
return size
|
|
}
|
|
}
|
|
|
|
func makeView() -> View {
|
|
return View(frame: CGRect())
|
|
}
|
|
|
|
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
|
}
|
|
}
|
|
|
|
private final class AvatarComponent: Component {
|
|
let context: AccountContext
|
|
let peer: EnginePeer
|
|
let size: CGSize?
|
|
|
|
init(context: AccountContext, peer: EnginePeer, size: CGSize? = nil) {
|
|
self.context = context
|
|
self.peer = peer
|
|
self.size = size
|
|
}
|
|
|
|
static func ==(lhs: AvatarComponent, rhs: AvatarComponent) -> Bool {
|
|
if lhs.context !== rhs.context {
|
|
return false
|
|
}
|
|
if lhs.peer != rhs.peer {
|
|
return false
|
|
}
|
|
if lhs.size != rhs.size {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
final class View: UIView {
|
|
private var avatarNode: AvatarNode?
|
|
|
|
private var component: AvatarComponent?
|
|
private weak var state: EmptyComponentState?
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func update(component: AvatarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
|
self.component = component
|
|
self.state = state
|
|
|
|
let size = component.size ?? availableSize
|
|
|
|
let avatarNode: AvatarNode
|
|
if let current = self.avatarNode {
|
|
avatarNode = current
|
|
} else {
|
|
avatarNode = AvatarNode(font: avatarPlaceholderFont(size: floor(size.width * 0.5)))
|
|
avatarNode.displaysAsynchronously = false
|
|
self.avatarNode = avatarNode
|
|
self.addSubview(avatarNode.view)
|
|
}
|
|
avatarNode.frame = CGRect(origin: CGPoint(), size: size)
|
|
avatarNode.setPeer(
|
|
context: component.context,
|
|
theme: component.context.sharedContext.currentPresentationData.with({ $0 }).theme,
|
|
peer: component.peer,
|
|
synchronousLoad: true,
|
|
displayDimensions: size
|
|
)
|
|
avatarNode.updateSize(size: size)
|
|
|
|
return size
|
|
}
|
|
}
|
|
|
|
func makeView() -> View {
|
|
return View(frame: CGRect())
|
|
}
|
|
|
|
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
|
}
|
|
}
|
|
|
|
private func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? {
|
|
return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in
|
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
|
|
|
context.setFillColor(backgroundColor.cgColor)
|
|
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
|
|
|
context.setLineWidth(2.0)
|
|
context.setLineCap(.round)
|
|
context.setStrokeColor(foregroundColor.cgColor)
|
|
|
|
context.move(to: CGPoint(x: 10.0, y: 10.0))
|
|
context.addLine(to: CGPoint(x: 20.0, y: 20.0))
|
|
context.strokePath()
|
|
|
|
context.move(to: CGPoint(x: 20.0, y: 10.0))
|
|
context.addLine(to: CGPoint(x: 10.0, y: 20.0))
|
|
context.strokePath()
|
|
})
|
|
}
|
|
|
|
private final class BorderedBadgeComponent: Component {
|
|
let backgroundColor: UIColor
|
|
let cutoutColor: UIColor
|
|
let content: AnyComponent<Empty>
|
|
let insets: UIEdgeInsets
|
|
let aspect: CGFloat?
|
|
let cutoutWidth: CGFloat
|
|
|
|
init(
|
|
backgroundColor: UIColor,
|
|
cutoutColor: UIColor,
|
|
content: AnyComponent<Empty>,
|
|
insets: UIEdgeInsets,
|
|
aspect: CGFloat? = nil,
|
|
cutoutWidth: CGFloat
|
|
) {
|
|
self.backgroundColor = backgroundColor
|
|
self.cutoutColor = cutoutColor
|
|
self.content = content
|
|
self.insets = insets
|
|
self.aspect = aspect
|
|
self.cutoutWidth = cutoutWidth
|
|
}
|
|
|
|
static func ==(lhs: BorderedBadgeComponent, rhs: BorderedBadgeComponent) -> Bool {
|
|
if lhs.backgroundColor !== rhs.backgroundColor {
|
|
return false
|
|
}
|
|
if lhs.cutoutColor != rhs.cutoutColor {
|
|
return false
|
|
}
|
|
if lhs.content != rhs.content {
|
|
return false
|
|
}
|
|
if lhs.insets != rhs.insets {
|
|
return false
|
|
}
|
|
if lhs.aspect != rhs.aspect {
|
|
return false
|
|
}
|
|
if lhs.cutoutWidth != rhs.cutoutWidth {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
final class View: UIView {
|
|
private let cutoutBackground = ComponentView<Empty>()
|
|
private let background = ComponentView<Empty>()
|
|
private let content = ComponentView<Empty>()
|
|
|
|
private var component: BorderedBadgeComponent?
|
|
private weak var state: EmptyComponentState?
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func update(component: BorderedBadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
|
self.component = component
|
|
self.state = state
|
|
|
|
let contentSize = self.content.update(
|
|
transition: transition,
|
|
component: component.content,
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width - component.insets.left - component.insets.right, height: availableSize.height - component.insets.top - component.insets.bottom)
|
|
)
|
|
var size = CGSize(width: contentSize.width + component.insets.left + component.insets.right, height: contentSize.height + component.insets.top + component.insets.bottom)
|
|
if let aspect = component.aspect {
|
|
size.width = size.height * aspect
|
|
}
|
|
|
|
let backgroundFrame = CGRect(origin: CGPoint(), size: size)
|
|
let cutoutBackgroundFrame = backgroundFrame.insetBy(dx: -component.cutoutWidth, dy: -component.cutoutWidth)
|
|
|
|
let _ = self.cutoutBackground.update(
|
|
transition: transition,
|
|
component: AnyComponent(FilledRoundedRectangleComponent(
|
|
color: component.cutoutColor,
|
|
cornerRadius: .minEdge,
|
|
smoothCorners: false
|
|
)),
|
|
environment: {}, containerSize: cutoutBackgroundFrame.size
|
|
)
|
|
let _ = self.background.update(
|
|
transition: transition,
|
|
component: AnyComponent(FilledRoundedRectangleComponent(
|
|
color: component.backgroundColor,
|
|
cornerRadius: .minEdge,
|
|
smoothCorners: false
|
|
)),
|
|
environment: {}, containerSize: backgroundFrame.size
|
|
)
|
|
|
|
if let cutoutBackgroundView = self.cutoutBackground.view {
|
|
if cutoutBackgroundView.superview == nil {
|
|
self.addSubview(cutoutBackgroundView)
|
|
}
|
|
transition.setFrame(view: cutoutBackgroundView, frame: cutoutBackgroundFrame)
|
|
}
|
|
if let backgroundView = self.background.view {
|
|
if backgroundView.superview == nil {
|
|
self.addSubview(backgroundView)
|
|
}
|
|
transition.setFrame(view: backgroundView, frame: backgroundFrame)
|
|
}
|
|
|
|
let contentFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + component.insets.left, y: backgroundFrame.minY + component.insets.top), size: contentSize)
|
|
if let contentView = self.content.view {
|
|
if contentView.superview == nil {
|
|
self.addSubview(contentView)
|
|
}
|
|
transition.setFrame(view: contentView, frame: contentFrame)
|
|
}
|
|
|
|
return size
|
|
}
|
|
}
|
|
|
|
func makeView() -> View {
|
|
return View(frame: CGRect())
|
|
}
|
|
|
|
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
|
}
|
|
}
|
|
|
|
final class AffiliatePeerSubtitleComponent: Component {
|
|
let theme: PresentationTheme
|
|
let percentText: String
|
|
let text: String
|
|
|
|
init(
|
|
theme: PresentationTheme,
|
|
percentText: String,
|
|
text: String
|
|
) {
|
|
self.theme = theme
|
|
self.percentText = percentText
|
|
self.text = text
|
|
}
|
|
|
|
static func ==(lhs: AffiliatePeerSubtitleComponent, rhs: AffiliatePeerSubtitleComponent) -> Bool {
|
|
if lhs.theme !== rhs.theme {
|
|
return false
|
|
}
|
|
if lhs.percentText != rhs.percentText {
|
|
return false
|
|
}
|
|
if lhs.text != rhs.text {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
final class View: UIView {
|
|
private let badgeBackground = ComponentView<Empty>()
|
|
private let badgeText = ComponentView<Empty>()
|
|
private let text = ComponentView<Empty>()
|
|
|
|
private var component: AffiliatePeerSubtitleComponent?
|
|
private weak var state: EmptyComponentState?
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func update(component: AffiliatePeerSubtitleComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
|
self.component = component
|
|
self.state = state
|
|
|
|
let badgeSpacing: CGFloat = 5.0
|
|
let badgeInsets = UIEdgeInsets(top: 2.0, left: 4.0, bottom: 2.0, right: 4.0)
|
|
|
|
let badgeTextSize = self.badgeText.update(
|
|
transition: transition,
|
|
component: AnyComponent(MultilineTextComponent(
|
|
text: .plain(NSAttributedString(string: component.percentText, font: Font.regular(13.0), textColor: component.theme.list.itemCheckColors.foregroundColor))
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width, height: 100.0)
|
|
)
|
|
|
|
let badgeSize = CGSize(width: badgeTextSize.width + badgeInsets.left + badgeInsets.right, height: badgeTextSize.height + badgeInsets.top + badgeInsets.bottom)
|
|
|
|
let textSize = self.text.update(
|
|
transition: transition,
|
|
component: AnyComponent(MultilineTextComponent(
|
|
text: .plain(NSAttributedString(string: component.text, font: Font.regular(15.0), textColor: component.theme.list.itemSecondaryTextColor))
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width, height: 100.0)
|
|
)
|
|
|
|
let size = CGSize(width: badgeSize.width + badgeSpacing + textSize.width, height: textSize.height)
|
|
|
|
let badgeFrame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((size.height - badgeSize.height) * 0.5)), size: badgeSize)
|
|
let _ = self.badgeBackground.update(
|
|
transition: transition,
|
|
component: AnyComponent(FilledRoundedRectangleComponent(
|
|
color: component.theme.list.itemCheckColors.fillColor,
|
|
cornerRadius: .value(5.0),
|
|
smoothCorners: true
|
|
)),
|
|
environment: {},
|
|
containerSize: badgeFrame.size
|
|
)
|
|
if let badgeBackgroundView = self.badgeBackground.view {
|
|
if badgeBackgroundView.superview == nil {
|
|
self.addSubview(badgeBackgroundView)
|
|
}
|
|
transition.setFrame(view: badgeBackgroundView, frame: badgeFrame)
|
|
}
|
|
|
|
let badgeTextFrame = CGRect(origin: CGPoint(x: badgeFrame.minX + badgeInsets.left, y: badgeFrame.minY + badgeInsets.top), size: badgeTextSize)
|
|
if let badgeTextView = self.badgeText.view {
|
|
if badgeTextView.superview == nil {
|
|
self.addSubview(badgeTextView)
|
|
}
|
|
transition.setPosition(view: badgeTextView, position: badgeTextFrame.center)
|
|
badgeTextView.bounds = CGRect(origin: CGPoint(), size: badgeTextFrame.size)
|
|
}
|
|
|
|
let textFrame = CGRect(origin: CGPoint(x: badgeSize.width + badgeSpacing, y: 0.0), size: textSize)
|
|
if let textView = self.text.view {
|
|
if textView.superview == nil {
|
|
self.addSubview(textView)
|
|
}
|
|
transition.setFrame(view: textView, frame: textFrame)
|
|
}
|
|
|
|
return size
|
|
}
|
|
}
|
|
|
|
func makeView() -> View {
|
|
return View(frame: CGRect())
|
|
}
|
|
|
|
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
|
}
|
|
}
|
|
|
|
final class BotSectionSortButtonComponent: Component {
|
|
let theme: PresentationTheme
|
|
let strings: PresentationStrings
|
|
let sortMode: TelegramSuggestedStarRefBotList.SortMode
|
|
let action: (UIView) -> Void
|
|
|
|
init(
|
|
theme: PresentationTheme,
|
|
strings: PresentationStrings,
|
|
sortMode: TelegramSuggestedStarRefBotList.SortMode,
|
|
action: @escaping (UIView) -> Void
|
|
) {
|
|
self.theme = theme
|
|
self.strings = strings
|
|
self.sortMode = sortMode
|
|
self.action = action
|
|
}
|
|
|
|
static func ==(lhs: BotSectionSortButtonComponent, rhs: BotSectionSortButtonComponent) -> Bool {
|
|
if lhs.theme !== rhs.theme {
|
|
return false
|
|
}
|
|
if lhs.strings !== rhs.strings {
|
|
return false
|
|
}
|
|
if lhs.sortMode != rhs.sortMode {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
final class View: HighlightableButton {
|
|
private let text = ComponentView<Empty>()
|
|
private let icon = ComponentView<Empty>()
|
|
|
|
private var component: BotSectionSortButtonComponent?
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
|
|
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
@objc private func pressed() {
|
|
guard let component = self.component else {
|
|
return
|
|
}
|
|
component.action(self)
|
|
}
|
|
|
|
func update(component: BotSectionSortButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
|
self.component = component
|
|
|
|
let sortByString: String
|
|
switch component.sortMode {
|
|
case .date:
|
|
sortByString = "SORT BY [DATE]()"
|
|
case .profitability:
|
|
sortByString = "SORT BY [PROFITABILITY]()"
|
|
case .revenue:
|
|
sortByString = "SORT BY [REVENUE]()"
|
|
}
|
|
let textSize = self.text.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(MultilineTextComponent(
|
|
text: .markdown(text: sortByString, attributes: MarkdownAttributes(
|
|
body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: component.theme.list.freeTextColor),
|
|
bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: component.theme.list.freeTextColor),
|
|
link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: component.theme.list.itemAccentColor),
|
|
linkAttribute: { url in
|
|
return ("URL", url)
|
|
}
|
|
))
|
|
)),
|
|
environment: {},
|
|
containerSize: availableSize
|
|
)
|
|
|
|
let iconSize = self.icon.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(BundleIconComponent(
|
|
name: "Item List/ContextDisclosureArrow",
|
|
tintColor: component.theme.list.itemAccentColor,
|
|
scaleFactor: 0.7
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: 100.0, height: 100.0)
|
|
)
|
|
|
|
let spacing: CGFloat = 2.0
|
|
|
|
let size = CGSize(width: textSize.width + spacing + iconSize.width, height: textSize.height)
|
|
|
|
let textFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: textSize)
|
|
if let textView = self.text.view {
|
|
if textView.superview == nil {
|
|
textView.isUserInteractionEnabled = false
|
|
self.addSubview(textView)
|
|
}
|
|
transition.setPosition(view: textView, position: textFrame.center)
|
|
textView.bounds = CGRect(origin: CGPoint(), size: textFrame.size)
|
|
}
|
|
|
|
let iconFrame = CGRect(origin: CGPoint(x: textFrame.maxX + spacing, y: floorToScreenPixels((size.height - iconSize.height) * 0.5)), size: iconSize)
|
|
if let iconView = self.icon.view {
|
|
if iconView.superview == nil {
|
|
iconView.isUserInteractionEnabled = false
|
|
self.addSubview(iconView)
|
|
}
|
|
transition.setFrame(view: iconView, frame: iconFrame)
|
|
}
|
|
|
|
return size
|
|
}
|
|
}
|
|
|
|
func makeView() -> View {
|
|
return View(frame: CGRect())
|
|
}
|
|
|
|
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
|
}
|
|
}
|
|
|
|
final class PeerBadgeAvatarComponent: Component {
|
|
let context: AccountContext
|
|
let peer: EnginePeer
|
|
let theme: PresentationTheme
|
|
let hasBadge: Bool
|
|
|
|
init(context: AccountContext, peer: EnginePeer, theme: PresentationTheme, hasBadge: Bool) {
|
|
self.context = context
|
|
self.peer = peer
|
|
self.theme = theme
|
|
self.hasBadge = hasBadge
|
|
}
|
|
|
|
static func ==(lhs: PeerBadgeAvatarComponent, rhs: PeerBadgeAvatarComponent) -> Bool {
|
|
if lhs.context !== rhs.context {
|
|
return false
|
|
}
|
|
if lhs.peer != rhs.peer {
|
|
return false
|
|
}
|
|
if lhs.theme !== rhs.theme {
|
|
return false
|
|
}
|
|
if lhs.hasBadge != rhs.hasBadge {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
final class View: UIView {
|
|
private var avatarNode: AvatarNode?
|
|
|
|
private let badgeBackground = UIImageView()
|
|
private let badgeIcon = UIImageView()
|
|
|
|
private var component: PeerBadgeAvatarComponent?
|
|
private weak var state: EmptyComponentState?
|
|
|
|
private static let badgeBackgroundImage = generateFilledCircleImage(diameter: 18.0, color: .white)?.withRenderingMode(.alwaysTemplate)
|
|
private static let badgeIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Links/Link"), color: .white)?.withRenderingMode(.alwaysTemplate)
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func update(component: PeerBadgeAvatarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
|
self.component = component
|
|
self.state = state
|
|
|
|
let size = CGSize(width: 40.0, height: 40.0)
|
|
let badgeSize: CGFloat = 18.0
|
|
|
|
let badgeFrame = CGRect(origin: CGPoint(x: size.width - badgeSize, y: size.height - badgeSize), size: CGSize(width: badgeSize, height: badgeSize))
|
|
|
|
let avatarNode: AvatarNode
|
|
if let current = self.avatarNode {
|
|
avatarNode = current
|
|
} else {
|
|
avatarNode = AvatarNode(font: avatarPlaceholderFont(size: floor(size.width * 0.5)))
|
|
avatarNode.displaysAsynchronously = false
|
|
self.avatarNode = avatarNode
|
|
self.addSubview(avatarNode.view)
|
|
}
|
|
|
|
avatarNode.frame = CGRect(origin: CGPoint(), size: size)
|
|
avatarNode.setPeer(
|
|
context: component.context,
|
|
theme: component.context.sharedContext.currentPresentationData.with({ $0 }).theme,
|
|
peer: component.peer,
|
|
synchronousLoad: false,
|
|
displayDimensions: size,
|
|
cutoutRect: component.hasBadge ? badgeFrame.insetBy(dx: -(1.0 + UIScreenPixel), dy: -(1.0 + UIScreenPixel)) : nil
|
|
)
|
|
|
|
if self.badgeBackground.image == nil {
|
|
self.badgeBackground.image = View.badgeBackgroundImage
|
|
}
|
|
if self.badgeBackground.superview == nil {
|
|
self.addSubview(self.badgeBackground)
|
|
}
|
|
if self.badgeIcon.image == nil {
|
|
self.badgeIcon.image = View.badgeIconImage
|
|
}
|
|
if self.badgeIcon.superview == nil {
|
|
self.addSubview(self.badgeIcon)
|
|
}
|
|
|
|
self.badgeBackground.tintColor = component.theme.list.itemCheckColors.fillColor
|
|
self.badgeIcon.tintColor = component.theme.list.itemCheckColors.foregroundColor
|
|
|
|
transition.setFrame(view: self.badgeBackground, frame: badgeFrame)
|
|
|
|
if let badgeIconSize = self.badgeIcon.image?.size {
|
|
let badgeIconFactor: CGFloat = 0.45
|
|
let badgeIconSize = CGSize(width: badgeIconSize.width * badgeIconFactor, height: badgeIconSize.height * badgeIconFactor)
|
|
let badgeIconFrame = CGRect(origin: CGPoint(x: badgeFrame.minX + floorToScreenPixels((badgeSize - badgeIconSize.width) * 0.5), y: badgeFrame.minY + floorToScreenPixels((badgeSize - badgeIconSize.height) * 0.5)), size: badgeIconSize)
|
|
transition.setFrame(view: self.badgeIcon, frame: badgeIconFrame)
|
|
}
|
|
|
|
self.badgeBackground.isHidden = !component.hasBadge
|
|
self.badgeIcon.isHidden = !component.hasBadge
|
|
|
|
return size
|
|
}
|
|
}
|
|
|
|
func makeView() -> View {
|
|
return View(frame: CGRect())
|
|
}
|
|
|
|
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
|
}
|
|
}
|