Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
+34
View File
@@ -0,0 +1,34 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "TelegramPermissionsUI",
module_name = "TelegramPermissionsUI",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/AccountContext:AccountContext",
"//submodules/TextFormat:TextFormat",
"//submodules/Markdown:Markdown",
"//submodules/TelegramPermissions:TelegramPermissions",
"//submodules/DeviceAccess:DeviceAccess",
"//submodules/PeersNearbyIconNode:PeersNearbyIconNode",
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
"//submodules/AppBundle:AppBundle",
"//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,319 @@
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, isShimmering: 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
}
}
@@ -0,0 +1,263 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import DeviceAccess
import AccountContext
public final class PermissionController: ViewController {
private let context: AccountContext
private let splitTest: PermissionUISplitTest?
private var state: PermissionControllerContent?
private var splashScreen = false
private var locationManager: LocationManager?
private var controllerNode: PermissionControllerNode {
return self.displayNode as! PermissionControllerNode
}
private var didPlayPresentationAnimation = false
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
private var allow: (() -> Void)?
private var skip: (() -> Void)?
public var proceed: ((Bool) -> Void)?
public init(context: AccountContext, splashScreen: Bool = true, splitTest: PermissionUISplitTest? = nil) {
self.context = context
self.splitTest = splitTest
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.splashScreen = splashScreen
let navigationBarPresentationData: NavigationBarPresentationData
if splashScreen {
navigationBarPresentationData = NavigationBarPresentationData(theme: NavigationBarTheme(buttonColor: self.presentationData.theme.rootController.navigationBar.accentTextColor, disabledButtonColor: self.presentationData.theme.rootController.navigationBar.disabledButtonColor, primaryTextColor: self.presentationData.theme.rootController.navigationBar.primaryTextColor, backgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear), strings: NavigationBarStrings(presentationStrings: self.presentationData.strings))
} else {
navigationBarPresentationData = NavigationBarPresentationData(presentationData: self.presentationData)
}
super.init(navigationBarPresentationData: navigationBarPresentationData)
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
self.updateThemeAndStrings()
self.presentationDataDisposable = (context.sharedContext.presentationData
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
if let strongSelf = self {
let previousTheme = strongSelf.presentationData.theme
let previousStrings = strongSelf.presentationData.strings
strongSelf.presentationData = presentationData
if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings {
strongSelf.updateThemeAndStrings()
}
}
})
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.presentationDataDisposable?.dispose()
}
public override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if let presentationArguments = self.presentationArguments as? ViewControllerPresentationArguments, !self.didPlayPresentationAnimation {
self.didPlayPresentationAnimation = true
if case .modalSheet = presentationArguments.presentationAnimation {
self.controllerNode.animateIn()
}
}
}
private func updateThemeAndStrings() {
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
let navigationBarPresentationData: NavigationBarPresentationData
if self.splashScreen {
navigationBarPresentationData = NavigationBarPresentationData(theme: NavigationBarTheme(buttonColor: self.presentationData.theme.rootController.navigationBar.accentTextColor, disabledButtonColor: self.presentationData.theme.rootController.navigationBar.disabledButtonColor, primaryTextColor: self.presentationData.theme.rootController.navigationBar.primaryTextColor, backgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear), strings: NavigationBarStrings(presentationStrings: self.presentationData.strings))
} else {
navigationBarPresentationData = NavigationBarPresentationData(presentationData: self.presentationData)
}
self.navigationBar?.updatePresentationData(navigationBarPresentationData)
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: nil, style: .plain, target: nil, action: nil)
if self.navigationItem.rightBarButtonItem != nil {
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Permissions_Skip, style: .plain, target: self, action: #selector(PermissionController.nextPressed))
}
self.controllerNode.updatePresentationData(self.presentationData)
}
private func openAppSettings() {
self.context.sharedContext.applicationBindings.openSettings()
}
public func setState(_ state: PermissionControllerContent, animated: Bool) {
guard state != self.state else {
return
}
self.state = state
if case let .permission(permission) = state, let state = permission {
if case .nearbyLocation = state {
} else {
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Permissions_Skip, style: .plain, target: self, action: #selector(PermissionController.nextPressed))
}
switch state {
case let .contacts(status):
self.splitTest?.addEvent(.ContactsModalRequest)
self.allow = { [weak self] in
if let strongSelf = self {
switch status {
case .requestable:
strongSelf.splitTest?.addEvent(.ContactsRequest)
DeviceAccess.authorizeAccess(to: .contacts, { [weak self] result in
if let strongSelf = self {
if result {
strongSelf.splitTest?.addEvent(.ContactsAllowed)
} else {
strongSelf.splitTest?.addEvent(.ContactsDenied)
}
strongSelf.proceed?(true)
}
})
case .denied:
strongSelf.openAppSettings()
strongSelf.proceed?(true)
default:
break
}
}
}
case let .notifications(status):
self.splitTest?.addEvent(.NotificationsModalRequest)
self.allow = { [weak self] in
if let strongSelf = self {
switch status {
case .requestable:
strongSelf.splitTest?.addEvent(.NotificationsRequest)
let context = strongSelf.context
DeviceAccess.authorizeAccess(to: .notifications, registerForNotifications: { [weak context] result in
context?.sharedContext.applicationBindings.registerForNotifications(result)
}, { [weak self] result in
if let strongSelf = self {
if result {
strongSelf.splitTest?.addEvent(.NotificationsAllowed)
} else {
strongSelf.splitTest?.addEvent(.NotificationsDenied)
}
strongSelf.proceed?(true)
}
})
case .denied, .unreachable:
strongSelf.openAppSettings()
strongSelf.proceed?(true)
default:
break
}
}
}
case .siri:
self.allow = { [weak self] in
self?.proceed?(true)
}
case .cellularData:
self.allow = { [weak self] in
if let strongSelf = self {
strongSelf.openAppSettings()
strongSelf.proceed?(true)
}
}
case let .nearbyLocation(status):
self.title = self.presentationData.strings.Permissions_PeopleNearbyTitle_v0
if self.locationManager == nil {
self.locationManager = LocationManager()
}
self.allow = { [weak self] in
if let strongSelf = self {
switch status {
case .requestable:
DeviceAccess.authorizeAccess(to: .location(.tracking), locationManager: strongSelf.locationManager, presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, { [weak self] result in
self?.proceed?(result)
})
case .denied, .unreachable:
strongSelf.openAppSettings()
strongSelf.proceed?(false)
default:
break
}
}
}
}
} else if case let .custom(icon, _, _, _, _, _, _) = state {
if case .animation = icon, case .modal = self.navigationPresentation {
self.navigationItem.leftBarButtonItem = UIBarButtonItem(customDisplayNode: ASDisplayNode())
}
self.allow = { [weak self] in
if let strongSelf = self {
strongSelf.proceed?(true)
}
}
}
self.skip = { [weak self] in
self?.proceed?(false)
}
self.controllerNode.setState(state, transition: animated ? .animated(duration: 0.4, curve: .spring) : .immediate)
}
public override func loadDisplayNode() {
self.displayNode = PermissionControllerNode(context: self.context, splitTest: self.splitTest)
self.displayNodeDidLoad()
self.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate)
self.controllerNode.allow = { [weak self] in
self?.allow?()
}
self.controllerNode.dismiss = { [weak self] in
self?.dismiss()
}
self.controllerNode.openPrivacyPolicy = { [weak self] in
if let strongSelf = self {
strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: "https://telegram.org/privacy", forceExternal: true, presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {})
}
}
}
@objc private func cancelPressed() {
self.dismiss()
}
public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
if let state = self.state, case .custom(.animation, _, _, _, _, _, _) = state, layout.size.width <= 320.0 {
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))
}
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.splashScreen ? 0.0 : self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
}
@objc private func nextPressed() {
self.skip?()
}
}
@@ -0,0 +1,265 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import AccountContext
import TelegramPermissions
import AppBundle
public struct PermissionControllerCustomIcon: Equatable {
let light: UIImage?
let dark: UIImage?
public init(light: UIImage?, dark: UIImage?) {
self.light = light
self.dark = dark
}
}
public enum PermissionControllerContent: Equatable {
case permission(PermissionState?)
case custom(icon: PermissionContentIcon, title: String, subtitle: String?, text: String, buttonTitle: String, secondaryButtonTitle: String?, footerText: String?)
}
private struct PermissionControllerDataState: Equatable {
var state: PermissionControllerContent?
}
private struct PermissionControllerLayoutState: Equatable {
let layout: ContainerViewLayout
let navigationHeight: CGFloat
}
private struct PermissionControllerInnerState: Equatable {
var layout: PermissionControllerLayoutState?
var data: PermissionControllerDataState
}
private struct PermissionControllerState: Equatable {
var layout: PermissionControllerLayoutState
var data: PermissionControllerDataState
}
extension PermissionControllerState {
init?(_ state: PermissionControllerInnerState) {
guard let layout = state.layout else {
return nil
}
self.init(layout: layout, data: state.data)
}
}
private func localizedString(for key: String, strings: PresentationStrings, fallback: String = "") -> String {
if let string = strings.primaryComponent.dict[key] {
return string
} else if let string = strings.secondaryComponent?.dict[key] {
return string
} else {
return fallback
}
}
final class PermissionControllerNode: ASDisplayNode {
private let context: AccountContext
private var presentationData: PresentationData
private let splitTest: PermissionUISplitTest?
private var innerState: PermissionControllerInnerState
private var contentNode: PermissionContentNode?
var allow: (() -> Void)?
var openPrivacyPolicy: (() -> Void)?
var dismiss: (() -> Void)?
init(context: AccountContext, splitTest: PermissionUISplitTest?) {
self.context = context
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.splitTest = splitTest
self.innerState = PermissionControllerInnerState(layout: nil, data: PermissionControllerDataState(state: nil))
super.init()
self.setViewBlock({
return UITracingLayerView()
})
self.updatePresentationData(self.presentationData)
}
func updatePresentationData(_ presentationData: PresentationData) {
self.presentationData = presentationData
self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.contentNode?.updatePresentationData(self.presentationData)
}
func animateIn(completion: (() -> Void)? = nil) {
self.layer.animatePosition(from: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
}
func animateOut(completion: (() -> Void)? = nil) {
self.layer.animatePosition(from: self.layer.position, to: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { _ in
completion?()
})
}
public func setState(_ state: PermissionControllerContent, transition: ContainedViewLayoutTransition) {
self.updateState({ currentState -> PermissionControllerInnerState in
return PermissionControllerInnerState(layout: currentState.layout, data: PermissionControllerDataState(state: state))
}, transition: transition)
}
private func updateState(_ f: (PermissionControllerInnerState) -> PermissionControllerInnerState, transition: ContainedViewLayoutTransition) {
let updatedState = f(self.innerState)
if updatedState != self.innerState {
self.innerState = updatedState
if let state = PermissionControllerState(updatedState) {
self.transition(state: state, transition: transition)
}
}
}
private func transition(state: PermissionControllerState, transition: ContainedViewLayoutTransition) {
let insets = state.layout.layout.insets(options: [.statusBar])
let contentFrame = CGRect(origin: CGPoint(x: 0.0, y: state.layout.navigationHeight), size: CGSize(width: state.layout.layout.size.width, height: state.layout.layout.size.height))
if let state = state.data.state {
switch state {
case let .permission(permission):
if permission?.kind.rawValue != self.contentNode?.kind {
if let dataState = permission {
let icon: UIImage?
let title: String
let text: String
let buttonTitle: String
let hasPrivacyPolicy: Bool
switch dataState {
case let .contacts(status):
icon = UIImage(bundleImageName: "Settings/Permissions/Contacts")
if let splitTest = self.splitTest, case let .modal(titleKey, textKey, allowTitleKey, allowInSettingsTitleKey) = splitTest.configuration.contacts {
title = localizedString(for: titleKey, strings: self.presentationData.strings)
text = localizedString(for: textKey, strings: self.presentationData.strings)
if status == .denied {
buttonTitle = localizedString(for: allowInSettingsTitleKey, strings: self.presentationData.strings)
} else {
buttonTitle = localizedString(for: allowTitleKey, strings: self.presentationData.strings)
}
} else {
title = self.presentationData.strings.Permissions_ContactsTitle_v0
text = self.presentationData.strings.Permissions_ContactsText_v0
if status == .denied {
buttonTitle = self.presentationData.strings.Permissions_ContactsAllowInSettings_v0
} else {
buttonTitle = self.presentationData.strings.Permissions_ContactsAllow_v0
}
}
hasPrivacyPolicy = true
case let .notifications(status):
icon = UIImage(bundleImageName: "Settings/Permissions/Notifications")
if let splitTest = self.splitTest, case let .modal(titleKey, textKey, allowTitleKey, allowInSettingsTitleKey) = splitTest.configuration.notifications {
title = localizedString(for: titleKey, strings: self.presentationData.strings, fallback: self.presentationData.strings.Permissions_NotificationsTitle_v0)
text = localizedString(for: textKey, strings: self.presentationData.strings, fallback: self.presentationData.strings.Permissions_NotificationsText_v0)
if status == .denied {
buttonTitle = localizedString(for: allowInSettingsTitleKey, strings: self.presentationData.strings, fallback: self.presentationData.strings.Permissions_NotificationsAllowInSettings_v0)
} else {
buttonTitle = localizedString(for: allowTitleKey, strings: self.presentationData.strings, fallback: self.presentationData.strings.Permissions_NotificationsAllow_v0)
}
} else {
title = self.presentationData.strings.Permissions_NotificationsTitle_v0
text = self.presentationData.strings.Permissions_NotificationsText_v0
if status == .denied {
buttonTitle = self.presentationData.strings.Permissions_NotificationsAllowInSettings_v0
} else {
buttonTitle = self.presentationData.strings.Permissions_NotificationsAllow_v0
}
}
hasPrivacyPolicy = false
case let .siri(status):
icon = UIImage(bundleImageName: "Settings/Permissions/Siri")
title = self.presentationData.strings.Permissions_SiriTitle_v0
text = self.presentationData.strings.Permissions_SiriText_v0
if status == .denied {
buttonTitle = self.presentationData.strings.Permissions_SiriAllowInSettings_v0
} else {
buttonTitle = self.presentationData.strings.Permissions_SiriAllow_v0
}
hasPrivacyPolicy = false
case .cellularData:
icon = UIImage(bundleImageName: "Settings/Permissions/CellularData")
title = self.presentationData.strings.Permissions_CellularDataTitle_v0
text = self.presentationData.strings.Permissions_CellularDataText_v0
buttonTitle = self.presentationData.strings.Permissions_CellularDataAllowInSettings_v0
hasPrivacyPolicy = false
case let .nearbyLocation(status):
icon = nil
title = self.presentationData.strings.Permissions_PeopleNearbyTitle_v0
text = self.presentationData.strings.Permissions_PeopleNearbyText_v0
if status == .denied {
buttonTitle = self.presentationData.strings.Permissions_PeopleNearbyAllowInSettings_v0
} else {
buttonTitle = self.presentationData.strings.Permissions_PeopleNearbyAllow_v0
}
hasPrivacyPolicy = false
}
let contentNode = PermissionContentNode(context: self.context, theme: self.presentationData.theme, strings: self.presentationData.strings, kind: dataState.kind.rawValue, icon: .image(icon), title: title, text: text, buttonTitle: buttonTitle, secondaryButtonTitle: nil, buttonAction: { [weak self] in
self?.allow?()
}, openPrivacyPolicy: hasPrivacyPolicy ? self.openPrivacyPolicy : nil)
self.insertSubnode(contentNode, at: 0)
contentNode.updateLayout(size: contentFrame.size, insets: insets, transition: .immediate)
contentNode.frame = contentFrame
if let currentContentNode = self.contentNode {
transition.updatePosition(node: currentContentNode, position: CGPoint(x: -contentFrame.size.width / 2.0, y: contentFrame.midY), completion: { [weak currentContentNode] _ in
currentContentNode?.removeFromSupernode()
})
transition.animateHorizontalOffsetAdditive(node: contentNode, offset: -contentFrame.width)
} else if transition.isAnimated {
contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
}
self.contentNode = contentNode
} else if let currentContentNode = self.contentNode {
transition.updateAlpha(node: currentContentNode, alpha: 0.0, completion: { [weak currentContentNode] _ in
currentContentNode?.removeFromSupernode()
})
self.contentNode = nil
}
} else if let contentNode = self.contentNode {
transition.updateFrame(node: contentNode, frame: contentFrame)
contentNode.updateLayout(size: contentFrame.size, insets: insets, transition: transition)
}
case let .custom(icon, title, subtitle, text, buttonTitle, secondaryButtonTitle, footerText):
if let contentNode = self.contentNode {
transition.updateFrame(node: contentNode, frame: contentFrame)
contentNode.updateLayout(size: contentFrame.size, insets: insets, transition: transition)
} else {
let contentNode = PermissionContentNode(context: self.context, theme: self.presentationData.theme, strings: self.presentationData.strings, kind: 0, icon: icon, title: title, subtitle: subtitle, text: text, buttonTitle: buttonTitle, secondaryButtonTitle: secondaryButtonTitle, footerText: footerText, buttonAction: { [weak self] in
self?.allow?()
}, openPrivacyPolicy: secondaryButtonTitle != nil ? { [weak self] in
self?.dismiss?()
} : nil)
self.insertSubnode(contentNode, at: 0)
contentNode.updateLayout(size: contentFrame.size, insets: insets, transition: .immediate)
contentNode.frame = contentFrame
self.contentNode = contentNode
}
}
}
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.updateState({ state in
var state = state
state.layout = PermissionControllerLayoutState(layout: layout, navigationHeight: navigationBarHeight)
return state
}, transition: transition)
}
@objc func privacyPolicyPressed() {
self.openPrivacyPolicy?()
}
}
@@ -0,0 +1,109 @@
import Foundation
import UIKit
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPermissions
extension PermissionKind {
fileprivate static var defaultOrder: [PermissionKind] {
return [.contacts, .notifications]
}
}
public enum PermissionUIRequestVariation {
case `default`
case modal(title: String, text: String, allowTitle: String, allowInSettingsTitle: String)
}
public struct PermissionUISplitTest: SplitTest {
public typealias Configuration = PermissionUIConfiguration
public typealias Event = PermissionUIEvent
public let postbox: Postbox
public let bucket: String?
public let configuration: Configuration
public init(postbox: Postbox, bucket: String?, configuration: Configuration) {
self.postbox = postbox
self.bucket = bucket
self.configuration = configuration
}
public struct PermissionUIConfiguration: SplitTestConfiguration {
public static var defaultValue: PermissionUIConfiguration {
return PermissionUIConfiguration(contacts: .default, notifications: .default, order: PermissionKind.defaultOrder)
}
public let contacts: PermissionUIRequestVariation
public let notifications: PermissionUIRequestVariation
public let order: [PermissionKind]
fileprivate init(contacts: PermissionUIRequestVariation, notifications: PermissionUIRequestVariation, order: [PermissionKind]) {
self.contacts = contacts
self.notifications = notifications
self.order = order
}
static func with(appConfiguration: AppConfiguration) -> (PermissionUIConfiguration, String?) {
if let data = appConfiguration.data, let permissions = data["ui_permissions_modals"] as? [String: Any] {
let contacts: PermissionUIRequestVariation
if let modal = permissions["phonebook_modal"] as? [String: Any] {
contacts = .modal(title: modal["popup_title_lang"] as? String ?? "", text: modal["popup_text_lang"] as? String ?? "", allowTitle: modal["popup_allowbtn_lang"] as? String ?? "", allowInSettingsTitle: modal["popup_allowbtn_settings_lang"] as? String ?? "")
} else {
contacts = .default
}
let notifications: PermissionUIRequestVariation
if let modal = permissions["notifications_modal"] as? [String: Any] {
notifications = .modal(title: modal["popup_title_lang"] as? String ?? "", text: modal["popup_text_lang"] as? String ?? "", allowTitle: modal["popup_allowbtn_lang"] as? String ?? "", allowInSettingsTitle: modal["popup_allowbtn_settings_lang"] as? String ?? "")
} else {
notifications = .default
}
let order: [PermissionKind]
if let values = permissions["order"] as? [String] {
order = values.compactMap { value in
switch value {
case "phonebook":
return .contacts
case "notifications":
return .notifications
default:
return nil
}
}
} else {
order = PermissionKind.defaultOrder
}
return (PermissionUIConfiguration(contacts: contacts, notifications: notifications, order: order), permissions["bucket"] as? String)
} else {
return (.defaultValue, nil)
}
}
}
public enum PermissionUIEvent: String, SplitTestEvent {
case ContactsModalRequest = "phbmodal_request"
case ContactsRequest = "phbperm_request"
case ContactsAllowed = "phbperm_allow"
case ContactsDenied = "phbperm_disallow"
case NotificationsModalRequest = "ntfmodal_request"
case NotificationsRequest = "ntfperm_request"
case NotificationsAllowed = "ntfperm_allow"
case NotificationsDenied = "ntfperm_disallow"
}
}
public func permissionUISplitTest(postbox: Postbox) -> Signal<PermissionUISplitTest, NoError> {
return postbox.preferencesView(keys: [PreferencesKeys.appConfiguration])
|> mapToSignal { view -> Signal<PermissionUISplitTest, NoError> in
if let appConfiguration = view.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self), appConfiguration.data != nil {
let (config, bucket) = PermissionUISplitTest.Configuration.with(appConfiguration: appConfiguration)
return .single(PermissionUISplitTest(postbox: postbox, bucket: bucket, configuration: config))
} else {
return .never()
}
} |> take(1)
}