WinterGram/submodules/TelegramPermissionsUI/Sources/PermissionContentNode.swift
2026-05-14 12:45:26 +02:00

331 lines
16 KiB
Swift

import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramCore
import TelegramPresentationData
import TextFormat
import TelegramPermissions
import ComponentFlow
import ButtonComponent
import ComponentDisplayAdapters
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 animationNode: AnimatedStickerNode?
private let titleNode: ImmediateTextNode
private let subtitleNode: ImmediateTextNode
private let textNode: ImmediateTextNode
private let actionButton = ComponentView<Empty>()
private let footerNode: ImmediateTextNode
private let privacyPolicyButton: HighlightableButtonNode
private let icon: PermissionContentIcon
private var title: String
private var text: String
private let buttonTitle: 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.buttonTitle = buttonTitle
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
} else {
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.textNode.lineSpacing = 0.1
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.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.animationNode.flatMap { self.addSubnode($0) }
self.addSubnode(self.titleNode)
self.addSubnode(self.subtitleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.footerNode)
self.addSubnode(self.privacyPolicyButton)
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 buttonSize = self.actionButton.update(
transition: ComponentTransition(transition),
component: AnyComponent(ButtonComponent(
background: ButtonComponent.Background(
style: .glass,
color: self.theme.list.itemCheckColors.fillColor,
foreground: self.theme.list.itemCheckColors.foregroundColor,
pressedColor: self.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9),
cornerRadius: 26.0,
isShimmering: true
),
content: AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(Text(
text: self.buttonTitle,
font: Font.semibold(17.0),
color: self.theme.list.itemCheckColors.foregroundColor
))
),
action: { [weak self] in
self?.buttonAction?()
}
)),
environment: {},
containerSize: CGSize(width: buttonWidth, height: 52.0)
)
let buttonHeight = buttonSize.height
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.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 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 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)
if let buttonView = self.actionButton.view {
if buttonView.superview == nil {
self.view.addSubview(buttonView)
}
buttonView.isAccessibilityElement = true
buttonView.accessibilityLabel = self.buttonTitle
buttonView.accessibilityTraits = [.button]
transition.updateFrame(view: buttonView, frame: buttonFrame)
}
transition.updateFrame(node: self.footerNode, frame: footerFrame)
transition.updateFrame(node: self.privacyPolicyButton, frame: privacyButtonFrame)
self.footerNode.isHidden = size.height < 568.0
}
}