import Foundation import UIKit import Display import ComponentFlow import SwiftSignalKit import TelegramCore import AccountContext import MultilineTextComponent import BlurredBackgroundComponent import Markdown import TelegramPresentationData import BundleIconComponent import ScrollComponent private final class HeaderComponent: Component { let context: AccountContext let theme: PresentationTheme let strings: PresentationStrings init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings) { self.context = context self.theme = theme self.strings = strings } static func ==(lhs: HeaderComponent, rhs: HeaderComponent) -> Bool { if lhs.context !== rhs.context { return false } if lhs.theme !== rhs.theme { return false } return true } final class View: UIView { private let coin = ComponentView() private let text = ComponentView() private var component: HeaderComponent? 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: HeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state let containerSize = CGSize(width: min(414.0, availableSize.width), height: 220.0) let coinSize = self.coin.update( transition: .immediate, component: AnyComponent(PremiumCoinComponent(isIntro: true, isVisible: true, hasIdleAnimations: true)), environment: {}, containerSize: containerSize ) if let view = self.coin.view { if view.superview == nil { self.addSubview(view) } view.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - coinSize.width) / 2.0), y: -84.0), size: coinSize) } let textSize = self.text.update( transition: .immediate, component: AnyComponent( MultilineTextComponent( text: .plain(NSAttributedString(string: component.strings.Premium_Business_Description, font: Font.regular(15.0), textColor: component.theme.list.itemPrimaryTextColor)), horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.2 ) ), environment: {}, containerSize: CGSize(width: availableSize.width - 32.0, height: 1000.0) ) if let view = self.text.view { if view.superview == nil { self.addSubview(view) } view.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - textSize.width) / 2.0), y: 139.0), size: textSize) } return CGSize(width: availableSize.width, height: 210.0) } } 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 ParagraphComponent: CombinedComponent { let title: String let titleColor: UIColor let text: String let textColor: UIColor let iconName: String let iconColor: UIColor public init( title: String, titleColor: UIColor, text: String, textColor: UIColor, iconName: String, iconColor: UIColor ) { self.title = title self.titleColor = titleColor self.text = text self.textColor = textColor self.iconName = iconName self.iconColor = iconColor } static func ==(lhs: ParagraphComponent, rhs: ParagraphComponent) -> Bool { if lhs.title != rhs.title { return false } if lhs.titleColor != rhs.titleColor { return false } if lhs.text != rhs.text { return false } if lhs.textColor != rhs.textColor { return false } if lhs.iconName != rhs.iconName { return false } if lhs.iconColor != rhs.iconColor { return false } return true } static var body: Body { let title = Child(MultilineTextComponent.self) let text = Child(MultilineTextComponent.self) let icon = Child(BundleIconComponent.self) return { context in let component = context.component let leftInset: CGFloat = 64.0 let rightInset: CGFloat = 32.0 let textSideInset: CGFloat = leftInset + 8.0 let spacing: CGFloat = 5.0 let textTopInset: CGFloat = 9.0 let title = title.update( component: MultilineTextComponent( text: .plain(NSAttributedString( string: component.title, font: Font.semibold(15.0), textColor: component.titleColor, paragraphAlignment: .natural )), horizontalAlignment: .center, maximumNumberOfLines: 1 ), availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), transition: .immediate ) let textFont = Font.regular(15.0) let boldTextFont = Font.semibold(15.0) let textColor = component.textColor let markdownAttributes = MarkdownAttributes( body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: textColor), linkAttribute: { _ in return nil } ) let text = text.update( component: MultilineTextComponent( text: .markdown(text: component.text, attributes: markdownAttributes), horizontalAlignment: .natural, maximumNumberOfLines: 0, lineSpacing: 0.2 ), availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: context.availableSize.height), transition: .immediate ) let icon = icon.update( component: BundleIconComponent( name: component.iconName, tintColor: component.iconColor ), availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height), transition: .immediate ) context.add(title .position(CGPoint(x: textSideInset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0)) ) context.add(text .position(CGPoint(x: textSideInset + text.size.width / 2.0, y: textTopInset + title.size.height + spacing + text.size.height / 2.0)) ) context.add(icon .position(CGPoint(x: 47.0, y: textTopInset + 18.0)) ) return CGSize(width: context.availableSize.width, height: textTopInset + title.size.height + text.size.height + 25.0) } } } private final class BusinessListComponent: CombinedComponent { typealias EnvironmentType = (Empty, ScrollChildEnvironment) let context: AccountContext let theme: PresentationTheme let topInset: CGFloat let bottomInset: CGFloat init(context: AccountContext, theme: PresentationTheme, topInset: CGFloat, bottomInset: CGFloat) { self.context = context self.theme = theme self.topInset = topInset self.bottomInset = bottomInset } static func ==(lhs: BusinessListComponent, rhs: BusinessListComponent) -> Bool { if lhs.context !== rhs.context { return false } if lhs.theme !== rhs.theme { return false } if lhs.topInset != rhs.topInset { return false } if lhs.bottomInset != rhs.bottomInset { return false } return true } final class State: ComponentState { private let context: AccountContext private var disposable: Disposable? var limits: EngineConfiguration.UserLimits = .defaultValue var premiumLimits: EngineConfiguration.UserLimits = .defaultValue var accountPeer: EnginePeer? init(context: AccountContext) { self.context = context super.init() self.disposable = (context.engine.data.get( TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false), TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true), TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId) ) |> deliverOnMainQueue).start(next: { [weak self] limits, premiumLimits, accountPeer in if let strongSelf = self { strongSelf.limits = limits strongSelf.premiumLimits = premiumLimits strongSelf.accountPeer = accountPeer strongSelf.updated(transition: .immediate) } }) } deinit { self.disposable?.dispose() } } func makeState() -> State { return State(context: self.context) } static var body: Body { let list = Child(List.self) return { context in let theme = context.component.theme let strings = context.component.context.sharedContext.currentPresentationData.with { $0 }.strings let colors = [ UIColor(rgb: 0xef6922), UIColor(rgb: 0xe54937), UIColor(rgb: 0xdb374b), UIColor(rgb: 0xbc4395), UIColor(rgb: 0x9b4fed), UIColor(rgb: 0x8958ff), UIColor(rgb: 0x676bff), UIColor(rgb: 0x007aff) ] let titleColor = theme.list.itemPrimaryTextColor let textColor = theme.list.itemSecondaryTextColor var items: [AnyComponentWithIdentity] = [] items.append( AnyComponentWithIdentity( id: "header", component: AnyComponent(HeaderComponent( context: context.component.context, theme: theme, strings: strings )) ) ) items.append( AnyComponentWithIdentity( id: "location", component: AnyComponent(ParagraphComponent( title: strings.Premium_Business_Location_Title, titleColor: titleColor, text: strings.Premium_Business_Location_Text, textColor: textColor, iconName: "Premium/Business/Location", iconColor: colors[0] )) ) ) items.append( AnyComponentWithIdentity( id: "hours", component: AnyComponent(ParagraphComponent( title: strings.Premium_Business_Hours_Title, titleColor: titleColor, text: strings.Premium_Business_Hours_Text, textColor: textColor, iconName: "Premium/Business/Hours", iconColor: colors[1] )) ) ) items.append( AnyComponentWithIdentity( id: "replies", component: AnyComponent(ParagraphComponent( title: strings.Premium_Business_Replies_Title, titleColor: titleColor, text: strings.Premium_Business_Replies_Text, textColor: textColor, iconName: "Premium/Business/Replies", iconColor: colors[2] )) ) ) items.append( AnyComponentWithIdentity( id: "greetings", component: AnyComponent(ParagraphComponent( title: strings.Premium_Business_Greetings_Title, titleColor: titleColor, text: strings.Premium_Business_Greetings_Text, textColor: textColor, iconName: "Premium/Business/Greetings", iconColor: colors[3] )) ) ) items.append( AnyComponentWithIdentity( id: "away", component: AnyComponent(ParagraphComponent( title: strings.Premium_Business_Away_Title, titleColor: titleColor, text: strings.Premium_Business_Away_Text, textColor: textColor, iconName: "Premium/Business/Away", iconColor: colors[4] )) ) ) items.append( AnyComponentWithIdentity( id: "links", component: AnyComponent(ParagraphComponent( title: strings.Premium_Business_Links_Title, titleColor: titleColor, text: strings.Premium_Business_Links_Text, textColor: textColor, iconName: "Premium/Business/Links", iconColor: colors[5] )) ) ) items.append( AnyComponentWithIdentity( id: "intro", component: AnyComponent(ParagraphComponent( title: strings.Premium_Business_Intro_Title, titleColor: titleColor, text: strings.Premium_Business_Intro_Text, textColor: textColor, iconName: "Premium/Business/Intro", iconColor: colors[6] )) ) ) items.append( AnyComponentWithIdentity( id: "chatbots", component: AnyComponent(ParagraphComponent( title: strings.Premium_Business_Chatbots_Title, titleColor: titleColor, text: strings.Premium_Business_Chatbots_Text, textColor: textColor, iconName: "Premium/Business/Chatbots", iconColor: colors[7] )) ) ) let list = list.update( component: List(items), availableSize: CGSize(width: context.availableSize.width, height: 10000.0), transition: context.transition ) let contentHeight = context.component.topInset - 56.0 + list.size.height + context.component.bottomInset context.add(list .position(CGPoint(x: list.size.width / 2.0, y: context.component.topInset + list.size.height / 2.0)) ) return CGSize(width: context.availableSize.width, height: contentHeight) } } } final class BusinessPageComponent: CombinedComponent { typealias EnvironmentType = DemoPageEnvironment let context: AccountContext let theme: PresentationTheme let neighbors: PageNeighbors let bottomInset: CGFloat let updatedBottomAlpha: (CGFloat) -> Void let updatedDismissOffset: (CGFloat) -> Void let updatedIsDisplaying: (Bool) -> Void init(context: AccountContext, theme: PresentationTheme, neighbors: PageNeighbors, bottomInset: CGFloat, updatedBottomAlpha: @escaping (CGFloat) -> Void, updatedDismissOffset: @escaping (CGFloat) -> Void, updatedIsDisplaying: @escaping (Bool) -> Void) { self.context = context self.theme = theme self.neighbors = neighbors self.bottomInset = bottomInset self.updatedBottomAlpha = updatedBottomAlpha self.updatedDismissOffset = updatedDismissOffset self.updatedIsDisplaying = updatedIsDisplaying } static func ==(lhs: BusinessPageComponent, rhs: BusinessPageComponent) -> Bool { if lhs.context !== rhs.context { return false } if lhs.theme !== rhs.theme { return false } if lhs.neighbors != rhs.neighbors { return false } if lhs.bottomInset != rhs.bottomInset { return false } return true } final class State: ComponentState { let updateBottomAlpha: (CGFloat) -> Void let updateDismissOffset: (CGFloat) -> Void let updatedIsDisplaying: (Bool) -> Void var resetScroll: ActionSlot? var topContentOffset: CGFloat = 0.0 var bottomContentOffset: CGFloat = 100.0 { didSet { self.updateAlpha() } } var position: CGFloat? { didSet { self.updateAlpha() } } var isDisplaying = false { didSet { if oldValue != self.isDisplaying { self.updatedIsDisplaying(self.isDisplaying) if !self.isDisplaying { self.resetScroll?.invoke(nil) } } } } var neighbors = PageNeighbors(leftIsList: false, rightIsList: false) init(updateBottomAlpha: @escaping (CGFloat) -> Void, updateDismissOffset: @escaping (CGFloat) -> Void, updateIsDisplaying: @escaping (Bool) -> Void) { self.updateBottomAlpha = updateBottomAlpha self.updateDismissOffset = updateDismissOffset self.updatedIsDisplaying = updateIsDisplaying super.init() } func updateAlpha() { var dismissToLeft = false if let position = self.position, position > 0.0 { dismissToLeft = true } var dismissPosition = min(1.0, abs(self.position ?? 0.0) / 1.3333) var position = min(1.0, abs(self.position ?? 0.0)) if position > 0.001, (dismissToLeft && self.neighbors.leftIsList) || (!dismissToLeft && self.neighbors.rightIsList) { dismissPosition = 0.0 position = 1.0 } self.updateDismissOffset(dismissPosition) let verticalPosition = 1.0 - min(30.0, self.bottomContentOffset) / 30.0 let backgroundAlpha: CGFloat = max(position, verticalPosition) self.updateBottomAlpha(backgroundAlpha) } } func makeState() -> State { return State(updateBottomAlpha: self.updatedBottomAlpha, updateDismissOffset: self.updatedDismissOffset, updateIsDisplaying: self.updatedIsDisplaying) } static var body: Body { let background = Child(Rectangle.self) let scroll = Child(ScrollComponent.self) let topPanel = Child(BlurredBackgroundComponent.self) let topSeparator = Child(Rectangle.self) let title = Child(MultilineTextComponent.self) let resetScroll = ActionSlot() return { context in let state = context.state let environment = context.environment[DemoPageEnvironment.self].value state.neighbors = context.component.neighbors state.resetScroll = resetScroll state.position = environment.position state.isDisplaying = environment.isDisplaying let theme = context.component.theme let strings = context.component.context.sharedContext.currentPresentationData.with { $0 }.strings let topInset: CGFloat = 56.0 let scroll = scroll.update( component: ScrollComponent( content: AnyComponent( BusinessListComponent( context: context.component.context, theme: theme, topInset: topInset, bottomInset: context.component.bottomInset + 110.0 ) ), contentInsets: UIEdgeInsets(top: topInset, left: 0.0, bottom: 0.0, right: 0.0), contentOffsetUpdated: { [weak state] topContentOffset, bottomContentOffset in state?.topContentOffset = topContentOffset state?.bottomContentOffset = bottomContentOffset Queue.mainQueue().justDispatch { state?.updated(transition: .immediate) } }, contentOffsetWillCommit: { _ in }, resetScroll: resetScroll ), availableSize: context.availableSize, transition: context.transition ) let background = background.update( component: Rectangle(color: theme.overallDarkAppearance ? theme.list.blocksBackgroundColor : theme.list.plainBackgroundColor), availableSize: scroll.size, transition: context.transition ) context.add(background .position(CGPoint(x: context.availableSize.width / 2.0, y: background.size.height / 2.0)) ) context.add(scroll .position(CGPoint(x: context.availableSize.width / 2.0, y: scroll.size.height / 2.0)) ) let topPanel = topPanel.update( component: BlurredBackgroundComponent( color: theme.rootController.navigationBar.blurredBackgroundColor ), availableSize: CGSize(width: context.availableSize.width, height: topInset), transition: context.transition ) let topSeparator = topSeparator.update( component: Rectangle( color: theme.rootController.navigationBar.separatorColor ), availableSize: CGSize(width: context.availableSize.width, height: UIScreenPixel), transition: context.transition ) let title = title.update( component: MultilineTextComponent( text: .plain(NSAttributedString(string: strings.Premium_Business, font: Font.semibold(20.0), textColor: theme.rootController.navigationBar.primaryTextColor)), horizontalAlignment: .center, truncationType: .end, maximumNumberOfLines: 1 ), availableSize: context.availableSize, transition: context.transition ) let topPanelAlpha: CGFloat if state.topContentOffset > 78.0 { topPanelAlpha = min(30.0, state.topContentOffset - 78.0) / 30.0 } else { topPanelAlpha = 0.0 } context.add(topPanel .position(CGPoint(x: context.availableSize.width / 2.0, y: topPanel.size.height / 2.0)) .opacity(topPanelAlpha) ) context.add(topSeparator .position(CGPoint(x: context.availableSize.width / 2.0, y: topPanel.size.height)) .opacity(topPanelAlpha) ) let titleTopOriginY = topPanel.size.height / 2.0 let titleBottomOriginY: CGFloat = 176.0 let titleOriginDelta = titleTopOriginY - titleBottomOriginY let fraction = min(1.0, state.topContentOffset / abs(titleOriginDelta)) let titleOriginY: CGFloat = titleBottomOriginY + fraction * titleOriginDelta let titleScale = 1.0 - max(0.0, fraction * 0.2) context.add(title .position(CGPoint(x: context.availableSize.width / 2.0, y: titleOriginY)) .scale(titleScale) ) return scroll.size } } }