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() private var toast: ComponentView? private let sourceAvatar = ComponentView() private let targetAvatar = ComponentView() private let targetAvatarBadge = ComponentView() private let sourceTargetArrow = UIImageView() private let linkIconBackground = ComponentView() private let linkIcon = ComponentView() private let linkIconBadge = ComponentView() private let title = ComponentView() private let subtitle = ComponentView() private let titleTransformContainer: UIView private let bottomPanelContainer: UIView private let actionButton = ComponentView() private let bottomText = ComponentView() private let linkText = ComponentView() private let targetText = ComponentView() private let targetPeer = ComponentView() 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 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") } func scrollViewDidScroll(_ scrollView: UIScrollView) { if !self.ignoreScrolling { self.updateScrolling(transition: .immediate) } } func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { } 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 case let .join(join) = component.mode else { return } var items: [ContextMenuItem] = [] let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }) let peers: [EnginePeer] = [ join.initialTargetPeer ] let avatarSize = CGSize(width: 30.0, height: 30.0) for peer in peers { let peerLabel: String if peer.id == component.context.account.peerId { peerLabel = "personal account" } else if case .channel = peer { peerLabel = "channel" } else { peerLabel = "bot" } items.append(.action(ContextMenuActionItem(text: peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder), textLayout: .secondLineWithValue(peerLabel), icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: peerAvatarCompleteImage(account: component.context.account, peer: peer, size: avatarSize)), action: { [weak self] c, _ in c?.dismiss(completion: {}) guard let self else { return } self.currentTargetPeer = peer self.state?.updated(transition: .immediate) }))) } let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView, actionsOnTop: true)), items: .single(ContextController.Items(id: AnyHashable(0), content: .list(items))), gesture: nil) controller.presentInGlobalOverlay(contextController) } func update(component: JoinAffiliateProgramScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { 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 if self.component == nil { switch component.mode { case let .join(join): self.currentTargetPeer = join.initialTargetPeer case let .active(active): self.currentTargetPeer = active.targetPeer } } 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 = component.mode { 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.userCount != 0 { let linkIconBadgeSize = self.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.userCount)", 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 = self.linkIconBadge.view { if linkIconBadgeView.superview == nil { self.scrollContentView.addSubview(linkIconBadgeView) } transition.setFrame(view: linkIconBadgeView, frame: linkIconBadgeFrame) } } 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 component.mode { 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.userCount == 0 { termsString = "No one opened \(component.sourcePeer.compactDisplayTitle) through this link yet." } else if active.userCount == 1 { termsString = "1 user opened \(component.sourcePeer.compactDisplayTitle) through this link." } else { termsString = "\(active.userCount) 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 } 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) = component.mode { var cleanLink = active.link 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() }, 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) } contentHeight += linkTextSize.height contentHeight += 24.0 } let actionButtonTitle: String switch component.mode { 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() } } )), 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 = component.mode { let toast: ComponentView 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, 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() private let title = ComponentView() private var avatarNode: AvatarNode? private var selectorIcon: ComponentView? private var component: PeerBadgeComponent? override init(frame: CGRect) { super.init(frame: frame) self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @objc private func pressed() { guard let component = self.component else { return } component.action?(self) } func update(component: PeerBadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.isEnabled = component.action != nil let height: CGFloat = 32.0 let avatarPadding: CGFloat = 1.0 let avatarDiameter = height - avatarPadding * 2.0 let avatarTextSpacing: CGFloat = 4.0 let rightTextInset: CGFloat = component.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: 15.0)) 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 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, 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 let avatarNode: AvatarNode private var component: AvatarComponent? private weak var state: EmptyComponentState? override init(frame: CGRect) { self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 18.0)) self.avatarNode.displaysAsynchronously = false super.init(frame: frame) self.addSubnode(self.avatarNode) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func update(component: AvatarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state let size = component.size ?? availableSize self.avatarNode.frame = CGRect(origin: CGPoint(), size: size) self.avatarNode.setPeer( context: component.context, theme: component.context.sharedContext.currentPresentationData.with({ $0 }).theme, peer: component.peer, synchronousLoad: true, displayDimensions: size ) return size } } func makeView() -> View { return View(frame: CGRect()) } 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) } } 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 let insets: UIEdgeInsets let aspect: CGFloat? let cutoutWidth: CGFloat init( backgroundColor: UIColor, cutoutColor: UIColor, content: AnyComponent, 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() private let background = ComponentView() private let content = ComponentView() 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, 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, 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() private let badgeText = ComponentView() private let text = ComponentView() 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, 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, 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() private let icon = ComponentView() 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, transition: ComponentTransition) -> CGSize { self.component = component let sortByString: String switch component.sortMode { case .date: sortByString = "SORT BY [DATE]()" case .commission: sortByString = "SORT BY [COMMISSION]()" 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, 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 let 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) { self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 15.0)) self.avatarNode.displaysAsynchronously = false super.init(frame: frame) self.addSubnode(self.avatarNode) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func update(component: PeerBadgeAvatarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, 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)) self.avatarNode.frame = CGRect(origin: CGPoint(), size: size) self.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, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } }