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 AvatarNode import AvatarStoryIndicatorComponent private final class AvatarComponent: Component { let context: AccountContext let theme: PresentationTheme let peer: EnginePeer init(context: AccountContext, theme: PresentationTheme, peer: EnginePeer) { self.context = context self.theme = theme self.peer = peer } static func ==(lhs: AvatarComponent, rhs: AvatarComponent) -> Bool { if lhs.context !== rhs.context { return false } if lhs.theme !== rhs.theme { return false } if lhs.peer != rhs.peer { return false } return true } final class View: UIView { private let avatarNode: AvatarNode private let indicator = ComponentView() private var component: AvatarComponent? private weak var state: EmptyComponentState? override init(frame: CGRect) { self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0)) 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: Transition) -> CGSize { self.component = component self.state = state let size = CGSize(width: 78.0, height: 78.0) self.avatarNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - size.width) / 2.0), y: -22.0), size: size) self.avatarNode.setPeer( context: component.context, theme: component.theme, peer: component.peer, synchronousLoad: true ) let colors = [ UIColor(rgb: 0xbb6de8), UIColor(rgb: 0x738cff), UIColor(rgb: 0x8f76ff) ] let indicatorSize = self.indicator.update( transition: .immediate, component: AnyComponent( AvatarStoryIndicatorComponent( hasUnseen: true, hasUnseenCloseFriendsItems: false, colors: AvatarStoryIndicatorComponent.Colors(unseenColors: colors, unseenCloseFriendsColors: colors, seenColors: colors), activeLineWidth: 3.0, inactiveLineWidth: 3.0, counters: AvatarStoryIndicatorComponent.Counters(totalCount: 8, unseenCount: 8) ) ), environment: {}, containerSize: CGSize(width: 78.0, height: 78.0) ) if let view = self.indicator.view { if view.superview == nil { self.addSubview(view) } view.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - indicatorSize.width) / 2.0), y: -22.0), size: indicatorSize) } return CGSize(width: availableSize.width, height: 122.0) } } func makeView() -> View { return View(frame: CGRect()) } func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> 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 StoriesListComponent: 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: StoriesListComponent, rhs: StoriesListComponent) -> 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: 0x0275f3), UIColor(rgb: 0x8698ff), UIColor(rgb: 0xc871ff), UIColor(rgb: 0xc356ad), UIColor(rgb: 0xe85c44), UIColor(rgb: 0xff932b), UIColor(rgb: 0xe9af18) ] let titleColor = theme.list.itemPrimaryTextColor let textColor = theme.list.itemSecondaryTextColor var items: [AnyComponentWithIdentity] = [] if let accountPeer = context.state.accountPeer { items.append( AnyComponentWithIdentity( id: "avatar", component: AnyComponent(AvatarComponent( context: context.component.context, theme: theme, peer: accountPeer )) ) ) } items.append( AnyComponentWithIdentity( id: "order", component: AnyComponent(ParagraphComponent( title: "Priority Order", titleColor: titleColor, text: "Get more views as your stories are always displayed first.", textColor: textColor, iconName: "Premium/Stories/Order", iconColor: colors[0] )) ) ) items.append( AnyComponentWithIdentity( id: "stealth", component: AnyComponent(ParagraphComponent( title: "Stealth Mode", titleColor: titleColor, text: "Hide the fact that you viewed other people's stories.", textColor: textColor, iconName: "Premium/Stories/Stealth", iconColor: colors[1] )) ) ) items.append( AnyComponentWithIdentity( id: "views", component: AnyComponent(ParagraphComponent( title: "Permanent Views History", titleColor: titleColor, text: "Check who opens your stories — even after they expire.", textColor: textColor, iconName: "Premium/Stories/Views", iconColor: colors[2] )) ) ) items.append( AnyComponentWithIdentity( id: "expiration", component: AnyComponent(ParagraphComponent( title: "Expiration Durations", titleColor: titleColor, text: "Set custom expiration durations like 6 or 48 hours for your stories.", textColor: textColor, iconName: "Premium/Stories/Expire", iconColor: colors[3] )) ) ) items.append( AnyComponentWithIdentity( id: "save", component: AnyComponent(ParagraphComponent( title: "Save Stories to Gallery", titleColor: titleColor, text: "Save other people's unprotected stories to your Gallery.", textColor: textColor, iconName: "Premium/Stories/Save", iconColor: colors[4] )) ) ) items.append( AnyComponentWithIdentity( id: "captions", component: AnyComponent(ParagraphComponent( title: "Longer Captions", titleColor: titleColor, text: "Add ten times longer captions to your stories.", textColor: textColor, iconName: "Premium/Stories/Caption", iconColor: colors[5] )) ) ) items.append( AnyComponentWithIdentity( id: "format", component: AnyComponent(ParagraphComponent( title: "Links and Formatting", titleColor: titleColor, text: "Add links and formatting in captions to your stories.", textColor: textColor, iconName: "Premium/Stories/Format", iconColor: colors[6] )) ) ) let list = list.update( component: List(items), availableSize: CGSize(width: context.availableSize.width, height: 10000.0), transition: context.transition ) let contentHeight = context.component.topInset + 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) } } } struct PageNeighbors: Equatable { var leftIsList: Bool var rightIsList: Bool } final class StoriesPageComponent: 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: StoriesPageComponent, rhs: StoriesPageComponent) -> 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(Void()) } } } } 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 secondaryTitle = 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( StoriesListComponent( 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.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: "Upgraded Stories", font: Font.semibold(20.0), textColor: theme.rootController.navigationBar.primaryTextColor)), horizontalAlignment: .center, truncationType: .end, maximumNumberOfLines: 1 ), availableSize: context.availableSize, transition: context.transition ) let secondaryTitle = secondaryTitle.update( component: MultilineTextComponent( text: .plain(NSAttributedString(string: "Exclusive Features in Stories", font: Font.semibold(17.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 = 144.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) let titleAlpha: CGFloat if fraction > 0.78 { titleAlpha = max(0.0, 1.0 - (fraction - 0.78) / 0.16) } else { titleAlpha = 1.0 } let secondaryTitleAlpha: CGFloat = 1.0 - titleAlpha context.add(title .position(CGPoint(x: context.availableSize.width / 2.0, y: titleOriginY)) .scale(titleScale) .opacity(titleAlpha) ) context.add(secondaryTitle .position(CGPoint(x: context.availableSize.width / 2.0, y: titleOriginY)) .opacity(secondaryTitleAlpha) ) return scroll.size } } }