import Foundation import UIKit import Display import AsyncDisplayKit import TelegramCore import TelegramPresentationData import TextFormat import TelegramPermissions import PeersNearbyIconNode import SolidRoundedButtonNode import PresentationDataUtils import Markdown import AnimatedStickerNode import TelegramAnimatedStickerNode import AppBundle import AccountContext public enum PermissionContentIcon: Equatable { case image(UIImage?) case icon(PermissionControllerCustomIcon) case animation(String) public func imageForTheme(_ theme: PresentationTheme) -> UIImage? { switch self { case let .image(image): return image case let .icon(icon): return theme.overallDarkAppearance ? (icon.dark ?? icon.light) : icon.light case .animation: return nil } } } public final class PermissionContentNode: ASDisplayNode { private var theme: PresentationTheme public let kind: Int32 private let filterHitTest: Bool private let iconNode: ASImageNode private let nearbyIconNode: PeersNearbyIconNode? private let animationNode: AnimatedStickerNode? private let titleNode: ImmediateTextNode private let subtitleNode: ImmediateTextNode private let textNode: ImmediateTextNode private let actionButton: SolidRoundedButtonNode private let footerNode: ImmediateTextNode private let privacyPolicyButton: HighlightableButtonNode private let icon: PermissionContentIcon private var title: String private var text: String public var buttonAction: (() -> Void)? public var openPrivacyPolicy: (() -> Void)? public var validLayout: (CGSize, UIEdgeInsets)? public init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, kind: Int32, icon: PermissionContentIcon, title: String, subtitle: String? = nil, text: String, buttonTitle: String, secondaryButtonTitle: String? = nil, footerText: String? = nil, buttonAction: @escaping () -> Void, openPrivacyPolicy: (() -> Void)?, filterHitTest: Bool = false) { self.theme = theme self.kind = kind self.buttonAction = buttonAction self.openPrivacyPolicy = openPrivacyPolicy self.filterHitTest = filterHitTest self.icon = icon self.title = title self.text = text self.iconNode = ASImageNode() self.iconNode.isLayerBacked = true self.iconNode.displayWithoutProcessing = true self.iconNode.displaysAsynchronously = false if case let .animation(animation) = icon { self.animationNode = DefaultAnimatedStickerNodeImpl() self.animationNode?.setup(source: AnimatedStickerNodeLocalFileSource(name: animation), width: 320, height: 320, playbackMode: .once, mode: .direct(cachePathPrefix: nil)) self.animationNode?.visibility = true self.nearbyIconNode = nil } else if kind == PermissionKind.nearbyLocation.rawValue { self.nearbyIconNode = PeersNearbyIconNode(theme: theme) self.animationNode = nil } else { self.nearbyIconNode = nil self.animationNode = nil } self.titleNode = ImmediateTextNode() self.titleNode.maximumNumberOfLines = 0 self.titleNode.textAlignment = .center self.titleNode.isUserInteractionEnabled = false self.titleNode.displaysAsynchronously = false self.titleNode.isAccessibilityElement = true self.subtitleNode = ImmediateTextNode() self.subtitleNode.maximumNumberOfLines = 1 self.subtitleNode.textAlignment = .center self.subtitleNode.isUserInteractionEnabled = false self.subtitleNode.displaysAsynchronously = false self.subtitleNode.isAccessibilityElement = true self.textNode = ImmediateTextNode() self.textNode.maximumNumberOfLines = 0 self.textNode.displaysAsynchronously = false self.textNode.isAccessibilityElement = true self.actionButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: theme), height: 52.0, cornerRadius: 9.0, gloss: true) self.footerNode = ImmediateTextNode() self.footerNode.textAlignment = .center self.footerNode.maximumNumberOfLines = 0 self.footerNode.displaysAsynchronously = false self.footerNode.isAccessibilityElement = true self.privacyPolicyButton = HighlightableButtonNode() self.privacyPolicyButton.setTitle(secondaryButtonTitle ?? strings.Permissions_PrivacyPolicy, with: Font.regular(17.0), with: theme.list.itemAccentColor, for: .normal) self.privacyPolicyButton.accessibilityLabel = secondaryButtonTitle ?? strings.Permissions_PrivacyPolicy super.init() self.iconNode.image = icon.imageForTheme(theme) self.title = title var secondaryText = false if case .animation = icon { secondaryText = true } self.textNode.textAlignment = secondaryButtonTitle != nil ? .natural : .center let body = MarkdownAttributeSet(font: Font.regular(16.0), textColor: secondaryButtonTitle != nil ? theme.list.itemSecondaryTextColor : theme.list.itemPrimaryTextColor) let link = MarkdownAttributeSet(font: Font.regular(16.0), textColor: theme.list.itemAccentColor, additionalAttributes: [TelegramTextAttributes.URL: ""]) self.textNode.attributedText = parseMarkdownIntoAttributedString(text.replacingOccurrences(of: "]", with: "]()"), attributes: MarkdownAttributes(body: body, bold: body, link: link, linkAttribute: { _ in nil }), textAlignment: secondaryText ? .natural : .center) self.actionButton.title = buttonTitle self.privacyPolicyButton.isHidden = openPrivacyPolicy == nil if let subtitle = subtitle { self.subtitleNode.attributedText = NSAttributedString(string: subtitle, font: Font.regular(13.0), textColor: theme.list.freeTextColor, paragraphAlignment: .center) self.subtitleNode.accessibilityLabel = subtitle } if let footerText = footerText { self.footerNode.attributedText = NSAttributedString(string: footerText, font: Font.regular(13.0), textColor: theme.list.freeTextColor, paragraphAlignment: .center) self.footerNode.accessibilityLabel = footerText } self.addSubnode(self.iconNode) self.nearbyIconNode.flatMap { self.addSubnode($0) } self.animationNode.flatMap { self.addSubnode($0) } self.addSubnode(self.titleNode) self.addSubnode(self.subtitleNode) self.addSubnode(self.textNode) self.addSubnode(self.actionButton) self.addSubnode(self.footerNode) self.addSubnode(self.privacyPolicyButton) self.actionButton.pressed = { [weak self] in self?.buttonAction?() } self.privacyPolicyButton.addTarget(self, action: #selector(self.privacyPolicyPressed), forControlEvents: .touchUpInside) } override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { guard let result = super.hitTest(point, with: event) else { return nil } if self.filterHitTest { if result === self.view { return nil } } return result } public func updatePresentationData(_ presentationData: PresentationData) { let theme = presentationData.theme self.theme = theme self.iconNode.image = self.icon.imageForTheme(theme) let body = MarkdownAttributeSet(font: Font.regular(16.0), textColor: theme.list.itemPrimaryTextColor) let link = MarkdownAttributeSet(font: Font.regular(16.0), textColor: theme.list.itemAccentColor, additionalAttributes: [TelegramTextAttributes.URL: ""]) self.textNode.attributedText = parseMarkdownIntoAttributedString(self.text.replacingOccurrences(of: "]", with: "]()"), attributes: MarkdownAttributes(body: body, bold: body, link: link, linkAttribute: { _ in nil }), textAlignment: .center) self.textNode.accessibilityLabel = self.textNode.attributedText?.string if let subtitle = self.subtitleNode.attributedText?.string { self.subtitleNode.attributedText = NSAttributedString(string: subtitle, font: Font.regular(13.0), textColor: theme.list.freeTextColor, paragraphAlignment: .center) self.subtitleNode.accessibilityLabel = subtitle } if let footerText = self.footerNode.attributedText?.string { self.footerNode.attributedText = NSAttributedString(string: footerText, font: Font.regular(13.0), textColor: theme.list.freeTextColor, paragraphAlignment: .center) self.footerNode.accessibilityLabel = footerText } if let privacyPolicyTitle = self.privacyPolicyButton.attributedTitle(for: .normal)?.string { self.privacyPolicyButton.setTitle(privacyPolicyTitle, with: Font.regular(16.0), with: theme.list.itemAccentColor, for: .normal) } if let validLayout = self.validLayout { self.updateLayout(size: validLayout.0, insets: validLayout.1, transition: .immediate) } } @objc private func privacyPolicyPressed() { self.openPrivacyPolicy?() } public func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ContainedViewLayoutTransition) { self.validLayout = (size, insets) var sidePadding: CGFloat let fontSize: CGFloat if min(size.width, size.height) > 330.0 { fontSize = 24.0 sidePadding = 36.0 } else { fontSize = 20.0 sidePadding = 20.0 } sidePadding += insets.left let smallerSidePadding: CGFloat = 20.0 + insets.left self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.bold(fontSize), textColor: self.theme.list.itemPrimaryTextColor) self.titleNode.accessibilityLabel = self.title let titleSize = self.titleNode.updateLayout(CGSize(width: size.width - sidePadding * 2.0, height: .greatestFiniteMagnitude)) let subtitleSize = self.subtitleNode.updateLayout(CGSize(width: size.width - smallerSidePadding * 2.0, height: .greatestFiniteMagnitude)) let textSize = self.textNode.updateLayout(CGSize(width: size.width - sidePadding * 2.0, height: .greatestFiniteMagnitude)) let buttonInset: CGFloat = 16.0 let buttonWidth = min(size.width, size.height) - buttonInset * 2.0 - insets.left - insets.right let buttonHeight = self.actionButton.updateLayout(width: buttonWidth, transition: transition) let footerSize = self.footerNode.updateLayout(CGSize(width: size.width - smallerSidePadding * 2.0, height: .greatestFiniteMagnitude)) let privacyButtonSize = self.privacyPolicyButton.measure(CGSize(width: size.width - sidePadding * 2.0, height: .greatestFiniteMagnitude)) let availableHeight = floor(size.height - insets.top - insets.bottom - titleSize.height - subtitleSize.height - textSize.height - buttonHeight) let titleTextSpacing: CGFloat = max(15.0, floor(availableHeight * 0.045)) let titleSubtitleSpacing: CGFloat = 6.0 let buttonSpacing: CGFloat = max(19.0, floor(availableHeight * 0.075)) var contentHeight = titleSize.height + titleTextSpacing + textSize.height + buttonHeight + buttonSpacing if subtitleSize.height > 0.0 { contentHeight += titleSubtitleSpacing + subtitleSize.height } var imageSize = CGSize() var imageSpacing: CGFloat = 0.0 if let icon = self.iconNode.image, size.width < size.height { imageSpacing = floor(availableHeight * 0.12) imageSize = icon.size contentHeight += imageSize.height + imageSpacing } if let _ = self.nearbyIconNode, size.width < size.height { imageSpacing = floor(availableHeight * 0.12) imageSize = CGSize(width: 120.0, height: 120.0) contentHeight += imageSize.height + imageSpacing } if let _ = self.animationNode, size.width < size.height { imageSpacing = floor(availableHeight * 0.12) imageSize = CGSize(width: 240.0, height: 240.0) contentHeight += imageSize.height + imageSpacing } let privacySpacing: CGFloat = max(30.0 + privacyButtonSize.height, (availableHeight - titleTextSpacing - buttonSpacing - imageSize.height - imageSpacing) / 2.0) var verticalOffset: CGFloat = 0.0 if size.height >= 568.0 { verticalOffset = availableHeight * 0.05 } let contentOrigin = insets.top + floor((size.height - insets.top - insets.bottom - contentHeight) / 2.0) - verticalOffset let iconFrame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: contentOrigin), size: imageSize) let nearbyIconFrame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: contentOrigin), size: imageSize) let animationFrame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: contentOrigin), size: imageSize) let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: iconFrame.maxY + imageSpacing), size: titleSize) let subtitleFrame: CGRect if subtitleSize.height > 0.0 { subtitleFrame = CGRect(origin: CGPoint(x: floor((size.width - subtitleSize.width) / 2.0), y: titleFrame.maxY + titleSubtitleSpacing), size: subtitleSize) } else { subtitleFrame = titleFrame } let textFrame = CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: subtitleFrame.maxY + titleTextSpacing), size: textSize) let footerFrame = CGRect(origin: CGPoint(x: floor((size.width - footerSize.width) / 2.0), y: size.height - footerSize.height - insets.bottom - 8.0), size: footerSize) let buttonFrame: CGRect let privacyButtonFrame: CGRect if self.textNode.textAlignment == .natural { buttonFrame = CGRect(origin: CGPoint(x: floor((size.width - buttonWidth) / 2.0), y: max(textFrame.maxY + buttonSpacing ,size.height - buttonHeight - insets.bottom - 70.0)), size: CGSize(width: buttonWidth, height: buttonHeight)) privacyButtonFrame = CGRect(origin: CGPoint(x: floor((size.width - privacyButtonSize.width) / 2.0), y: buttonFrame.maxY + 29.0), size: privacyButtonSize) } else { buttonFrame = CGRect(origin: CGPoint(x: floor((size.width - buttonWidth) / 2.0), y: textFrame.maxY + buttonSpacing), size: CGSize(width: buttonWidth, height: buttonHeight)) privacyButtonFrame = CGRect(origin: CGPoint(x: floor((size.width - privacyButtonSize.width) / 2.0), y: buttonFrame.maxY + floor((privacySpacing - privacyButtonSize.height) / 2.0)), size: privacyButtonSize) } transition.updateFrame(node: self.iconNode, frame: iconFrame) if let nearbyIconNode = self.nearbyIconNode { transition.updateFrame(node: nearbyIconNode, frame: nearbyIconFrame) } if let animationNode = self.animationNode { transition.updateFrame(node: animationNode, frame: animationFrame) animationNode.updateLayout(size: animationFrame.size) } transition.updateFrame(node: self.titleNode, frame: titleFrame) transition.updateFrame(node: self.subtitleNode, frame: subtitleFrame) transition.updateFrame(node: self.textNode, frame: textFrame) transition.updateFrame(node: self.actionButton, frame: buttonFrame) transition.updateFrame(node: self.footerNode, frame: footerFrame) transition.updateFrame(node: self.privacyPolicyButton, frame: privacyButtonFrame) self.footerNode.isHidden = size.height < 568.0 } }