import Foundation import UIKit import AsyncDisplayKit import Display import ComponentFlow import SwiftSignalKit import Postbox import TelegramCore import Markdown import TextFormat import TelegramPresentationData import ViewControllerComponent import SheetComponent import BalancedTextComponent import MultilineTextComponent import BundleIconComponent import ItemListUI import AccountContext import PresentationDataUtils import ListSectionComponent import TelegramStringFormatting import MediaEditor import UrlEscaping private let linkTag = GenericComponentViewTag() private final class SheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext let isEdit: Bool let link: String let webpage: TelegramMediaWebpage? let state: CreateLinkSheetComponent.State let dismiss: () -> Void init( context: AccountContext, isEdit: Bool, link: String, webpage: TelegramMediaWebpage?, state: CreateLinkSheetComponent.State, dismiss: @escaping () -> Void ) { self.context = context self.isEdit = isEdit self.link = link self.webpage = webpage self.state = state self.dismiss = dismiss } static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool { if lhs.context !== rhs.context { return false } if lhs.isEdit != rhs.isEdit { return false } if lhs.link != rhs.link { return false } if lhs.webpage != rhs.webpage { return false } return true } static var body: Body { let background = Child(RoundedRectangle.self) let cancelButton = Child(Button.self) let doneButton = Child(Button.self) let title = Child(Text.self) let urlSection = Child(ListSectionComponent.self) let nameSection = Child(ListSectionComponent.self) return { context in let environment = context.environment[EnvironmentType.self] let component = context.component let state = component.state let theme = environment.theme.withModalBlocksBackground() let strings = environment.strings let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } let sideInset: CGFloat = 16.0 var contentSize = CGSize(width: context.availableSize.width, height: 18.0) let background = background.update( component: RoundedRectangle(color: theme.list.blocksBackgroundColor, cornerRadius: 8.0), availableSize: CGSize(width: context.availableSize.width, height: 1000.0), transition: .immediate ) context.add(background .position(CGPoint(x: context.availableSize.width / 2.0, y: background.size.height / 2.0)) ) let constrainedTitleWidth = context.availableSize.width - 16.0 * 2.0 let cancelButton = cancelButton.update( component: Button( content: AnyComponent( Text( text: strings.Common_Cancel, font: Font.regular(17.0), color: theme.actionSheet.controlAccentColor ) ), action: { component.dismiss() } ), availableSize: context.availableSize, transition: .immediate ) context.add(cancelButton .position(CGPoint(x: sideInset + cancelButton.size.width / 2.0, y: contentSize.height + cancelButton.size.height / 2.0)) ) let explicitLink = explicitUrl(context.component.link) var isValidLink = false if isValidUrl(explicitLink) { isValidLink = true } let controller = environment.controller let doneButton = doneButton.update( component: Button( content: AnyComponent( Text( text: strings.Common_Done, font: Font.bold(17.0), color: state.link.isEmpty ? theme.actionSheet.secondaryTextColor : theme.actionSheet.controlAccentColor ) ), isEnabled: isValidLink, action: { [weak state] in if let controller = controller() as? CreateLinkScreen { state?.complete(controller: controller) } component.dismiss() } ), availableSize: context.availableSize, transition: .immediate ) context.add(doneButton .position(CGPoint(x: context.availableSize.width - sideInset - doneButton.size.width / 2.0, y: contentSize.height + doneButton.size.height / 2.0)) ) let title = title.update( component: Text(text: component.isEdit ? strings.MediaEditor_Link_EditTitle : strings.MediaEditor_Link_CreateTitle, font: Font.bold(17.0), color: theme.list.itemPrimaryTextColor), availableSize: CGSize(width: constrainedTitleWidth, height: context.availableSize.height), transition: .immediate ) context.add(title .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height / 2.0)) ) contentSize.height += title.size.height contentSize.height += 40.0 var urlItems: [AnyComponentWithIdentity] = [] if let webpage = state.webpage, case .Loaded = webpage.content, !state.dismissed { urlItems.append( AnyComponentWithIdentity( id: "webpage", component: AnyComponent( LinkPreviewComponent( webpage: webpage, theme: theme, strings: strings, presentLinkOptions: { [weak state] sourceNode in if let controller = controller() as? CreateLinkScreen { state?.presentLinkOptions(controller: controller, sourceNode: sourceNode) } }, dismiss: { [weak state] in state?.dismissed = true state?.updated(transition: .easeInOut(duration: 0.25)) } ) ) ) ) } urlItems.append( AnyComponentWithIdentity( id: "url", component: AnyComponent( LinkFieldComponent( textColor: theme.list.itemPrimaryTextColor, placeholderColor: theme.list.itemPlaceholderTextColor, text: state.link, link: true, placeholderText: strings.MediaEditor_Link_LinkTo_Placeholder, textUpdated: { [weak state] text in state?.link = text state?.updated() }, tag: linkTag ) ) ) ) let urlSection = urlSection.update( component: ListSectionComponent( theme: theme, header: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: strings.MediaEditor_Link_LinkTo_Title.uppercased(), font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: theme.list.freeTextColor )), maximumNumberOfLines: 0 )), footer: nil, items: urlItems, displaySeparators: false ), environment: {}, availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude), transition: context.transition ) context.add(urlSection .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + urlSection.size.height / 2.0)) .clipsToBounds(true) .cornerRadius(10.0) ) contentSize.height += urlSection.size.height contentSize.height += 30.0 let nameSection = nameSection.update( component: ListSectionComponent( theme: theme, header: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: strings.MediaEditor_Link_LinkName_Title.uppercased(), font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: theme.list.freeTextColor )), maximumNumberOfLines: 0 )), footer: nil, items: [ AnyComponentWithIdentity( id: "name", component: AnyComponent( LinkFieldComponent( textColor: theme.list.itemPrimaryTextColor, placeholderColor: theme.list.itemPlaceholderTextColor, text: state.name, link: false, placeholderText: strings.MediaEditor_Link_LinkTo_Placeholder, textUpdated: { [weak state] text in state?.name = text } ) ) ) ] ), environment: {}, availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude), transition: context.transition ) context.add(nameSection .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + nameSection.size.height / 2.0)) .clipsToBounds(true) .cornerRadius(10.0) ) contentSize.height += nameSection.size.height contentSize.height += 32.0 contentSize.height += max(environment.inputHeight, environment.safeInsets.bottom) return contentSize } } } private final class CreateLinkSheetComponent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment private let context: AccountContext private let link: CreateLinkScreen.Link? init( context: AccountContext, link: CreateLinkScreen.Link? ) { self.context = context self.link = link } static func ==(lhs: CreateLinkSheetComponent, rhs: CreateLinkSheetComponent) -> Bool { if lhs.context !== rhs.context { return false } if lhs.link != rhs.link { return false } return true } final class State: ComponentState { private let context: AccountContext fileprivate var link: String = "" { didSet { self.linkPromise.set(self.link) } } fileprivate var name: String = "" fileprivate var webpage: TelegramMediaWebpage? fileprivate var isDark = false fileprivate var dismissed = false private var positionBelowText = true private var largeMedia: Bool? = nil private let previewDisposable = MetaDisposable() private let linkDisposable = MetaDisposable() private let linkPromise = ValuePromise() init( context: AccountContext, link: CreateLinkScreen.Link? ) { self.context = context self.link = link?.url ?? "" self.name = link?.name ?? "" self.webpage = link?.webpage self.isDark = link?.isDark ?? false self.positionBelowText = link?.positionBelowText ?? true self.largeMedia = link?.largeMedia super.init() self.linkDisposable.set((self.linkPromise.get() |> delay(1.5, queue: Queue.mainQueue()) |> deliverOnMainQueue).startStrict(next: { [weak self] link in guard let self else { return } guard !link.isEmpty else { self.dismissed = false self.previewDisposable.set(nil) self.webpage = nil self.updated(transition: .easeInOut(duration: 0.25)) return } let link = explicitUrl(link) if self.dismissed { self.dismissed = false self.webpage = nil } self.previewDisposable.set( (webpagePreview(account: context.account, urls: [link]) |> deliverOnMainQueue).startStrict(next: { [weak self] result in guard let self else { return } switch result { case let .result(result): self.webpage = result?.webpage case .progress: self.webpage = nil } self.updated(transition: .easeInOut(duration: 0.25)) }) ) })) } deinit { self.previewDisposable.dispose() self.linkDisposable.dispose() } func presentLinkOptions(controller: CreateLinkScreen, sourceNode: ASDisplayNode) { guard let webpage = self.webpage else { return } let link = explicitUrl(self.link) var name: String = self.name if name.isEmpty { name = self.link } presentLinkOptionsController(context: self.context, selfController: controller, snapshotImage: controller.snapshotImage, isDark: self.isDark, sourceNode: sourceNode, url: link, name: name, positionBelowText: self.positionBelowText, largeMedia: self.largeMedia, webPage: webpage, completion: { [weak self] positionBelowText, largeMedia in guard let self else { return } self.positionBelowText = positionBelowText self.largeMedia = largeMedia }, remove: { [weak self] in guard let self else { return } self.dismissed = true self.updated(transition: .easeInOut(duration: 0.25)) }) } func complete(controller: CreateLinkScreen) { let text = !self.name.isEmpty ? self.name : self.link var effectiveMedia: TelegramMediaWebpage? if let webpage = self.webpage, case .Loaded = webpage.content, !self.dismissed { effectiveMedia = webpage } var attributes: [MessageAttribute] = [] attributes.append(TextEntitiesMessageAttribute(entities: [.init(range: 0 ..< (text as NSString).length, type: .Url)])) if !self.dismissed { attributes.append(WebpagePreviewMessageAttribute(leadingPreview: !self.positionBelowText, forceLargeMedia: self.largeMedia, isManuallyAdded: false, isSafe: true)) } let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1)) let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: text, attributes: attributes, media: effectiveMedia.flatMap { [$0] } ?? [], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) let completion = controller.completion let renderer = DrawingMessageRenderer(context: self.context, messages: [message], parentView: controller.view, isLink: true) renderer.render(completion: { result in completion( CreateLinkScreen.Result( url: self.link, name: self.name, webpage: effectiveMedia, positionBelowText: self.positionBelowText, largeMedia: self.largeMedia, image: effectiveMedia != nil ? result.dayImage : nil, nightImage: effectiveMedia != nil ? result.nightImage : nil ) ) }) } } func makeState() -> State { return State(context: self.context, link: self.link) } static var body: Body { let sheet = Child(SheetComponent<(EnvironmentType)>.self) let animateOut = StoredActionSlot(Action.self) return { context in let environment = context.environment[EnvironmentType.self] let controller = environment.controller var webpage = context.state.webpage if context.state.dismissed { webpage = nil } let link = context.state.link let sheet = sheet.update( component: SheetComponent( content: AnyComponent(SheetContent( context: context.component.context, isEdit: context.component.link != nil, link: link, webpage: webpage, state: context.state, dismiss: { animateOut.invoke(Action { _ in if let controller = controller() { controller.dismiss(completion: nil) } }) } )), backgroundColor: .blur(.dark), followContentSizeChanges: true, clipsContent: true, isScrollEnabled: false, animateOut: animateOut ), environment: { environment SheetComponentEnvironment( isDisplaying: environment.value.isVisible, isCentered: environment.metrics.widthClass == .regular, hasInputHeight: !environment.inputHeight.isZero, regularMetricsSize: CGSize(width: 430.0, height: 900.0), dismiss: { animated in if animated { animateOut.invoke(Action { _ in if let controller = controller() { controller.dismiss(completion: nil) } }) } else { if let controller = controller() { controller.dismiss(completion: nil) } } } ) }, availableSize: context.availableSize, transition: context.transition ) context.add(sheet .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) ) return context.availableSize } } } public final class CreateLinkScreen: ViewControllerComponentContainer { public struct Link: Equatable { let url: String let name: String? let webpage: TelegramMediaWebpage? let positionBelowText: Bool let largeMedia: Bool? let isDark: Bool init( url: String, name: String?, webpage: TelegramMediaWebpage?, positionBelowText: Bool, largeMedia: Bool?, isDark: Bool ) { self.url = url self.name = name self.webpage = webpage self.positionBelowText = positionBelowText self.largeMedia = largeMedia self.isDark = isDark } } public struct Result { let url: String let name: String let webpage: TelegramMediaWebpage? let positionBelowText: Bool let largeMedia: Bool? let image: UIImage? let nightImage: UIImage? } private let context: AccountContext fileprivate let snapshotImage: UIImage? fileprivate let completion: (CreateLinkScreen.Result) -> Void public init( context: AccountContext, link: CreateLinkScreen.Link?, snapshotImage: UIImage?, completion: @escaping (CreateLinkScreen.Result) -> Void ) { self.context = context self.snapshotImage = snapshotImage self.completion = completion super.init( context: context, component: CreateLinkSheetComponent( context: context, link: link ), navigationBarAppearance: .none, statusBarStyle: .ignore, theme: .dark ) self.navigationPresentation = .flatModal } required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } public override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) if let view = self.node.hostView.findTaggedView(tag: linkTag) as? LinkFieldComponent.View { view.activateInput() } } public func dismissAnimated() { if let view = self.node.hostView.findTaggedView(tag: SheetComponent.View.Tag()) as? SheetComponent.View { view.dismissAnimated() } } } private final class LinkFieldComponent: Component { typealias EnvironmentType = Empty let textColor: UIColor let placeholderColor: UIColor let text: String let link: Bool let placeholderText: String let textUpdated: (String) -> Void let tag: AnyObject? init( textColor: UIColor, placeholderColor: UIColor, text: String, link: Bool, placeholderText: String, textUpdated: @escaping (String) -> Void, tag: AnyObject? = nil ) { self.textColor = textColor self.placeholderColor = placeholderColor self.text = text self.link = link self.placeholderText = placeholderText self.textUpdated = textUpdated self.tag = tag } static func ==(lhs: LinkFieldComponent, rhs: LinkFieldComponent) -> Bool { if lhs.textColor != rhs.textColor { return false } if lhs.placeholderColor != rhs.placeholderColor { return false } if lhs.text != rhs.text { return false } if lhs.placeholderText != rhs.placeholderText { return false } return true } final class View: UIView, UITextFieldDelegate, ComponentTaggedView { public func matches(tag: Any) -> Bool { if let component = self.component, let componentTag = component.tag { let tag = tag as AnyObject if componentTag === tag { return true } } return false } private let placeholderView: ComponentView private let textField: TextFieldNodeView private var component: LinkFieldComponent? private weak var state: EmptyComponentState? override init(frame: CGRect) { self.placeholderView = ComponentView() self.textField = TextFieldNodeView(frame: .zero) super.init(frame: frame) self.textField.delegate = self self.textField.addTarget(self, action: #selector(self.textChanged(_:)), for: .editingChanged) self.addSubview(self.textField) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @objc func textChanged(_ sender: Any) { let text = self.textField.text ?? "" self.component?.textUpdated(text) self.placeholderView.view?.isHidden = !text.isEmpty } func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { let newText = ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string) if let component = self.component, !component.link && newText.count > 48 { textField.layer.addShakeAnimation() let hapticFeedback = HapticFeedback() hapticFeedback.error() DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.0, execute: { let _ = hapticFeedback }) return false } return true } func activateInput() { self.textField.becomeFirstResponder() } func update(component: LinkFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.textField.textColor = component.textColor self.textField.text = component.text self.textField.font = Font.regular(17.0) self.textField.keyboardAppearance = .dark if component.link { self.textField.keyboardType = .default self.textField.returnKeyType = .next self.textField.autocorrectionType = .no self.textField.autocapitalizationType = .none self.textField.textContentType = .URL } self.component = component self.state = state let placeholderSize = self.placeholderView.update( transition: .easeInOut(duration: 0.2), component: AnyComponent( Text( text: component.placeholderText, font: Font.regular(17.0), color: component.placeholderColor ) ), environment: {}, containerSize: availableSize ) let size = CGSize(width: availableSize.width, height: 44.0) if let placeholderComponentView = self.placeholderView.view { if placeholderComponentView.superview == nil { self.insertSubview(placeholderComponentView, at: 0) } placeholderComponentView.frame = CGRect(origin: CGPoint(x: 15.0, y: floorToScreenPixels((size.height - placeholderSize.height) / 2.0) + 1.0 - UIScreenPixel), size: placeholderSize) placeholderComponentView.isHidden = !component.text.isEmpty } self.textField.frame = CGRect(x: 15.0, y: 0.0, width: size.width - 30.0, height: 44.0) return size } } public func makeView() -> View { return View(frame: CGRect()) } public 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 LinkPreviewComponent: Component { typealias EnvironmentType = Empty let webpage: TelegramMediaWebpage let theme: PresentationTheme let strings: PresentationStrings let presentLinkOptions: (ASDisplayNode) -> Void let dismiss: () -> Void init( webpage: TelegramMediaWebpage, theme: PresentationTheme, strings: PresentationStrings, presentLinkOptions: @escaping (ASDisplayNode) -> Void, dismiss: @escaping () -> Void ) { self.webpage = webpage self.theme = theme self.strings = strings self.presentLinkOptions = presentLinkOptions self.dismiss = dismiss } static func ==(lhs: LinkPreviewComponent, rhs: LinkPreviewComponent) -> Bool { if lhs.webpage != rhs.webpage { return false } if lhs.theme !== rhs.theme { return false } return true } final class View: UIView, UITextFieldDelegate { let closeButton: HighlightableButtonNode let lineNode: ASImageNode let iconView: UIImageView let titleNode: TextNode private var titleString: NSAttributedString? let textNode: TextNode private var textString: NSAttributedString? private var component: LinkPreviewComponent? private weak var state: EmptyComponentState? override init(frame: CGRect) { self.closeButton = HighlightableButtonNode() self.closeButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0) self.closeButton.displaysAsynchronously = false self.lineNode = ASImageNode() self.lineNode.displayWithoutProcessing = true self.lineNode.displaysAsynchronously = false self.iconView = UIImageView() self.iconView.image = UIImage(bundleImageName: "Chat/Input/Accessory Panels/LinkSettingsIcon")?.withRenderingMode(.alwaysTemplate) self.titleNode = TextNode() self.titleNode.displaysAsynchronously = false self.textNode = TextNode() self.textNode.displaysAsynchronously = false super.init(frame: frame) self.closeButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: [.touchUpInside]) self.addSubnode(self.closeButton) self.addSubnode(self.lineNode) self.addSubview(self.iconView) self.addSubnode(self.titleNode) self.addSubnode(self.textNode) self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @objc private func closePressed() { guard let component = self.component else { return } component.dismiss() } private var previousTapTimestamp: Double? @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state, let component = self.component { let timestamp = CFAbsoluteTimeGetCurrent() if let previousTapTimestamp = self.previousTapTimestamp, previousTapTimestamp + 1.0 > timestamp { return } self.previousTapTimestamp = CFAbsoluteTimeGetCurrent() component.presentLinkOptions(self.textNode) } } func update(component: LinkPreviewComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let themeUpdated = self.component?.theme !== component.theme self.component = component self.state = state if themeUpdated { self.closeButton.setImage(PresentationResourcesChat.chatInputPanelCloseIconImage(component.theme), for: []) self.lineNode.image = PresentationResourcesChat.chatInputPanelVerticalSeparatorLineImage(component.theme) self.iconView.tintColor = component.theme.chat.inputPanel.panelControlAccentColor } let bounds = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: 45.0)) var authorName = "" var text = "" switch component.webpage.content { case .Pending: authorName = component.strings.Channel_NotificationLoading text = ""//component.url case let .Loaded(content): if let contentText = content.text { text = contentText } else { if let file = content.file, let mediaKind = mediaContentKind(EngineMedia(file)) { if content.type == "telegram_background" { text = component.strings.Message_Wallpaper } else if content.type == "telegram_theme" { text = component.strings.Message_Theme } else { text = stringForMediaKind(mediaKind, strings: component.strings).0.string } } else if content.type == "telegram_theme" { text = component.strings.Message_Theme } else if content.type == "video" { text = stringForMediaKind(.video, strings: component.strings).0.string } else if content.type == "telegram_story" { text = stringForMediaKind(.story, strings: component.strings).0.string } else if let _ = content.image { text = stringForMediaKind(.image, strings: component.strings).0.string } } if let title = content.title { authorName = title } else if let websiteName = content.websiteName { authorName = websiteName } else { authorName = content.displayUrl } } self.titleString = NSAttributedString(string: authorName, font: Font.medium(15.0), textColor: component.theme.chat.inputPanel.panelControlAccentColor) self.textString = NSAttributedString(string: text, font: Font.regular(15.0), textColor: component.theme.chat.inputPanel.primaryTextColor) let inset: CGFloat = 0.0 let leftInset: CGFloat = 55.0 let textLineInset: CGFloat = 10.0 let rightInset: CGFloat = 55.0 let textRightInset: CGFloat = 20.0 let closeButtonSize = CGSize(width: 44.0, height: bounds.height) self.closeButton.frame = CGRect(origin: CGPoint(x: bounds.size.width - closeButtonSize.width - inset, y: 2.0), size: closeButtonSize) self.lineNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 8.0), size: CGSize(width: 2.0, height: bounds.size.height - 10.0)) if let icon = self.iconView.image { self.iconView.frame = CGRect(origin: CGPoint(x: 7.0 + inset, y: 10.0), size: icon.size) } let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeTextLayout = TextNode.asyncLayout(self.textNode) let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: self.titleString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: bounds.size.width - leftInset - textLineInset - rightInset - textRightInset, height: bounds.size.height), alignment: .natural, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: self.textString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: bounds.size.width - leftInset - textLineInset - rightInset - textRightInset, height: bounds.size.height), alignment: .natural, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) self.titleNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset, y: 7.0), size: titleLayout.size) self.textNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset, y: 25.0), size: textLayout.size) let _ = titleApply() let _ = textApply() return bounds.size } } public func makeView() -> View { return View(frame: CGRect()) } public 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) } }