import Foundation import UIKit import Display import TelegramCore import Postbox import AsyncDisplayKit import UIKit import SwiftSignalKit import TelegramPresentationData import ActivityIndicator import OverlayStatusController import AccountContext public final class ProxyServerActionSheetController: ActionSheetController { private var presentationDisposable: Disposable? private let _ready = Promise() override public var ready: Promise { return self._ready } private var isDismissed: Bool = false convenience public init(context: AccountContext, server: ProxyServerSettings) { let presentationData = context.sharedContext.currentPresentationData.with { $0 } self.init(theme: presentationData.theme, strings: presentationData.strings, accountManager: context.sharedContext.accountManager, postbox: context.account.postbox, network: context.account.network, server: server, presentationData: context.sharedContext.presentationData) } public init(theme: PresentationTheme, strings: PresentationStrings, accountManager: AccountManager, postbox: Postbox, network: Network, server: ProxyServerSettings, presentationData: Signal?) { let sheetTheme = ActionSheetControllerTheme(presentationTheme: theme) super.init(theme: sheetTheme) self._ready.set(.single(true)) var items: [ActionSheetItem] = [] if case .mtp = server.connection { items.append(ActionSheetTextItem(title: strings.SocksProxySetup_AdNoticeHelp)) } items.append(ProxyServerInfoItem(strings: strings, network: network, server: server)) items.append(ProxyServerActionItem(accountManager:accountManager, postbox: postbox, network: network, presentationTheme: theme, strings: strings, server: server, dismiss: { [weak self] success in guard let strongSelf = self, !strongSelf.isDismissed else { return } strongSelf.isDismissed = true if success { strongSelf.present(OverlayStatusController(theme: theme, strings: strings, type: .shieldSuccess(strings.SocksProxySetup_ProxyEnabled, false)), in: .window(.root)) } strongSelf.dismissAnimated() }, present: { [weak self] c, a in self?.present(c, in: .window(.root), with: a) })) self.setItemGroups([ ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: strings.Common_Cancel, action: { [weak self] in self?.dismissAnimated() }) ]) ]) if let presentationData = presentationData { self.presentationDisposable = presentationData.start(next: { [weak self] presentationData in if let strongSelf = self { strongSelf.theme = ActionSheetControllerTheme(presentationTheme: presentationData.theme) } }) } } required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { self.presentationDisposable?.dispose() } } private final class ProxyServerInfoItem: ActionSheetItem { private let strings: PresentationStrings private let network: Network private let server: ProxyServerSettings init(strings: PresentationStrings, network: Network, server: ProxyServerSettings) { self.strings = strings self.network = network self.server = server } func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode { return ProxyServerInfoItemNode(theme: theme, strings: self.strings, network: self.network, server: self.server) } func updateNode(_ node: ActionSheetItemNode) { } } private let textFont = Font.regular(16.0) private enum ProxyServerInfoStatusType { case generic(String) case failed(String) } private final class ProxyServerInfoItemNode: ActionSheetItemNode { private let theme: ActionSheetControllerTheme private let strings: PresentationStrings private let network: Network private let server: ProxyServerSettings private let fieldNodes: [(ImmediateTextNode, ImmediateTextNode)] private let statusTextNode: ImmediateTextNode private let statusDisposable = MetaDisposable() init(theme: ActionSheetControllerTheme, strings: PresentationStrings, network: Network, server: ProxyServerSettings) { self.theme = theme self.strings = strings self.network = network self.server = server var fieldNodes: [(ImmediateTextNode, ImmediateTextNode)] = [] let serverTitleNode = ImmediateTextNode() serverTitleNode.isUserInteractionEnabled = false serverTitleNode.displaysAsynchronously = false serverTitleNode.attributedText = NSAttributedString(string: strings.SocksProxySetup_Hostname, font: textFont, textColor: theme.secondaryTextColor) let serverTextNode = ImmediateTextNode() serverTextNode.isUserInteractionEnabled = false serverTextNode.displaysAsynchronously = false serverTextNode.attributedText = NSAttributedString(string: server.host, font: textFont, textColor: theme.primaryTextColor) fieldNodes.append((serverTitleNode, serverTextNode)) let portTitleNode = ImmediateTextNode() portTitleNode.isUserInteractionEnabled = false portTitleNode.displaysAsynchronously = false portTitleNode.attributedText = NSAttributedString(string: strings.SocksProxySetup_Port, font: textFont, textColor: theme.secondaryTextColor) let portTextNode = ImmediateTextNode() portTextNode.isUserInteractionEnabled = false portTextNode.displaysAsynchronously = false portTextNode.attributedText = NSAttributedString(string: "\(server.port)", font: textFont, textColor: theme.primaryTextColor) fieldNodes.append((portTitleNode, portTextNode)) switch server.connection { case let .socks5(username, password): if let username = username { let usernameTitleNode = ImmediateTextNode() usernameTitleNode.isUserInteractionEnabled = false usernameTitleNode.displaysAsynchronously = false usernameTitleNode.attributedText = NSAttributedString(string: strings.SocksProxySetup_Username, font: textFont, textColor: theme.secondaryTextColor) let usernameTextNode = ImmediateTextNode() usernameTextNode.isUserInteractionEnabled = false usernameTextNode.displaysAsynchronously = false usernameTextNode.attributedText = NSAttributedString(string: username, font: textFont, textColor: theme.primaryTextColor) fieldNodes.append((usernameTitleNode, usernameTextNode)) } if let password = password { let passwordTitleNode = ImmediateTextNode() passwordTitleNode.isUserInteractionEnabled = false passwordTitleNode.displaysAsynchronously = false passwordTitleNode.attributedText = NSAttributedString(string: strings.SocksProxySetup_Password, font: textFont, textColor: theme.secondaryTextColor) let passwordTextNode = ImmediateTextNode() passwordTextNode.isUserInteractionEnabled = false passwordTextNode.displaysAsynchronously = false passwordTextNode.attributedText = NSAttributedString(string: password, font: textFont, textColor: theme.primaryTextColor) fieldNodes.append((passwordTitleNode, passwordTextNode)) } case .mtp: let passwordTitleNode = ImmediateTextNode() passwordTitleNode.isUserInteractionEnabled = false passwordTitleNode.displaysAsynchronously = false passwordTitleNode.attributedText = NSAttributedString(string: strings.SocksProxySetup_Secret, font: textFont, textColor: theme.secondaryTextColor) let passwordTextNode = ImmediateTextNode() passwordTextNode.isUserInteractionEnabled = false passwordTextNode.displaysAsynchronously = false passwordTextNode.attributedText = NSAttributedString(string: "•••••", font: textFont, textColor: theme.primaryTextColor) fieldNodes.append((passwordTitleNode, passwordTextNode)) } let statusTitleNode = ImmediateTextNode() statusTitleNode.isUserInteractionEnabled = false statusTitleNode.displaysAsynchronously = false statusTitleNode.attributedText = NSAttributedString(string: strings.SocksProxySetup_Status, font: textFont, textColor: theme.secondaryTextColor) let statusTextNode = ImmediateTextNode() statusTextNode.isUserInteractionEnabled = false statusTextNode.displaysAsynchronously = false statusTextNode.attributedText = NSAttributedString(string: strings.SocksProxySetup_ProxyStatusChecking, font: textFont, textColor: theme.primaryTextColor) fieldNodes.append((statusTitleNode, statusTextNode)) self.fieldNodes = fieldNodes self.statusTextNode = statusTextNode super.init(theme: theme) for (lhs, rhs) in fieldNodes { self.addSubnode(lhs) self.addSubnode(rhs) } } deinit { self.statusDisposable.dispose() } override func didLoad() { super.didLoad() let statusesContext = ProxyServersStatuses(network: network, servers: .single([self.server])) self.statusDisposable.set((statusesContext.statuses() |> map { return $0.first?.value } |> distinctUntilChanged |> deliverOnMainQueue).start(next: { [weak self] status in if let strongSelf = self, let status = status { let statusType: ProxyServerInfoStatusType switch status { case .checking: statusType = .generic(strongSelf.strings.SocksProxySetup_ProxyStatusChecking) case let .available(rtt): let pingTime = Int(rtt * 1000.0) statusType = .generic(strongSelf.strings.SocksProxySetup_ProxyStatusPing("\(pingTime)").0) case .notAvailable: statusType = .failed(strongSelf.strings.SocksProxySetup_ProxyStatusUnavailable) } strongSelf.setStatus(statusType) } })) } func setStatus(_ status: ProxyServerInfoStatusType) { let attributedString: NSAttributedString switch status { case let .generic(text): attributedString = NSAttributedString(string: text, font: textFont, textColor: theme.primaryTextColor) case let .failed(text): attributedString = NSAttributedString(string: text, font: textFont, textColor: theme.destructiveActionTextColor) } self.statusTextNode.attributedText = attributedString self.setNeedsLayout() } override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { return CGSize(width: constrainedSize.width, height: 36.0 * CGFloat(self.fieldNodes.count) + 12.0) } override func layout() { super.layout() let size = self.bounds.size var offset: CGFloat = 15.0 for (lhs, rhs) in self.fieldNodes { let lhsSize = lhs.updateLayout(CGSize(width: size.width - 18.0 * 2.0, height: CGFloat.greatestFiniteMagnitude)) lhs.frame = CGRect(origin: CGPoint(x: 18, y: offset), size: lhsSize) let rhsSize = rhs.updateLayout(CGSize(width: max(1.0, size.width - 18 * 2.0 - lhsSize.width - 4.0), height: CGFloat.greatestFiniteMagnitude)) rhs.frame = CGRect(origin: CGPoint(x: size.width - 18 - rhsSize.width, y: offset), size: rhsSize) offset += 36.0 } } } private final class ProxyServerActionItem: ActionSheetItem { private let accountManager: AccountManager private let postbox: Postbox private let network: Network private let presentationTheme: PresentationTheme private let strings: PresentationStrings private let server: ProxyServerSettings private let dismiss: (Bool) -> Void private let present: (ViewController, Any?) -> Void init(accountManager: AccountManager, postbox: Postbox, network: Network, presentationTheme: PresentationTheme, strings: PresentationStrings, server: ProxyServerSettings, dismiss: @escaping (Bool) -> Void, present: @escaping (ViewController, Any?) -> Void) { self.accountManager = accountManager self.postbox = postbox self.network = network self.presentationTheme = presentationTheme self.strings = strings self.server = server self.dismiss = dismiss self.present = present } func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode { return ProxyServerActionItemNode(accountManager: self.accountManager, postbox: self.postbox, network: self.network, presentationTheme: self.presentationTheme, theme: theme, strings: self.strings, server: self.server, dismiss: self.dismiss, present: self.present) } func updateNode(_ node: ActionSheetItemNode) { } } private final class ProxyServerActionItemNode: ActionSheetItemNode { private let accountManager: AccountManager private let postbox: Postbox private let network: Network private let presentationTheme: PresentationTheme private let theme: ActionSheetControllerTheme private let strings: PresentationStrings private let server: ProxyServerSettings private let dismiss: (Bool) -> Void private let present: (ViewController, Any?) -> Void private let buttonNode: HighlightableButtonNode private let titleNode: ImmediateTextNode private let activityIndicator: ActivityIndicator private let disposable = MetaDisposable() private var revertSettings: ProxySettings? init(accountManager: AccountManager, postbox: Postbox, network: Network, presentationTheme: PresentationTheme, theme: ActionSheetControllerTheme, strings: PresentationStrings, server: ProxyServerSettings, dismiss: @escaping (Bool) -> Void, present: @escaping (ViewController, Any?) -> Void) { self.accountManager = accountManager self.postbox = postbox self.network = network self.theme = theme self.presentationTheme = presentationTheme self.strings = strings self.server = server self.dismiss = dismiss self.present = present self.titleNode = ImmediateTextNode() self.titleNode.isUserInteractionEnabled = false self.titleNode.displaysAsynchronously = false self.titleNode.attributedText = NSAttributedString(string: strings.SocksProxySetup_ConnectAndSave, font: Font.regular(20.0), textColor: theme.controlAccentColor) self.activityIndicator = ActivityIndicator(type: .custom(theme.controlAccentColor, 22.0, 1.5, false)) self.activityIndicator.isHidden = true self.buttonNode = HighlightableButtonNode() super.init(theme: theme) self.addSubnode(self.titleNode) self.addSubnode(self.activityIndicator) self.addSubnode(self.buttonNode) self.buttonNode.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { if highlighted { strongSelf.backgroundNode.backgroundColor = strongSelf.theme.itemHighlightedBackgroundColor } else { UIView.animate(withDuration: 0.3, animations: { strongSelf.backgroundNode.backgroundColor = strongSelf.theme.itemBackgroundColor }) } } } self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) } deinit { self.disposable.dispose() if let revertSettings = self.revertSettings { let _ = updateProxySettingsInteractively(accountManager: self.accountManager, { _ in return revertSettings }) } } override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { return CGSize(width: constrainedSize.width, height: 57.0) } override func layout() { super.layout() let size = self.bounds.size self.buttonNode.frame = CGRect(origin: CGPoint(), size: size) let labelSize = self.titleNode.updateLayout(CGSize(width: max(1.0, size.width - 10.0), height: size.height)) let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - labelSize.width) / 2.0), y: floorToScreenPixels((size.height - labelSize.height) / 2.0)), size: labelSize) let activitySize = self.activityIndicator.measure(CGSize(width: 100.0, height: 100.0)) self.titleNode.frame = titleFrame self.activityIndicator.frame = CGRect(origin: CGPoint(x: 14.0, y: titleFrame.minY - 0.0), size: activitySize) } @objc private func buttonPressed() { let proxyServerSettings = self.server let _ = (self.accountManager.transaction { transaction -> ProxySettings in var currentSettings: ProxySettings? updateProxySettingsInteractively(transaction: transaction, { settings in currentSettings = settings var settings = settings if let index = settings.servers.firstIndex(of: proxyServerSettings) { settings.servers[index] = proxyServerSettings settings.activeServer = proxyServerSettings } else { settings.servers.insert(proxyServerSettings, at: 0) settings.activeServer = proxyServerSettings } settings.enabled = true return settings }) return currentSettings ?? ProxySettings.defaultSettings } |> deliverOnMainQueue).start(next: { [weak self] previousSettings in if let strongSelf = self { strongSelf.revertSettings = previousSettings strongSelf.buttonNode.isUserInteractionEnabled = false strongSelf.titleNode.attributedText = NSAttributedString(string: strongSelf.strings.SocksProxySetup_Connecting, font: Font.regular(20.0), textColor: strongSelf.theme.primaryTextColor) strongSelf.activityIndicator.isHidden = false strongSelf.setNeedsLayout() let signal = strongSelf.network.connectionStatus |> filter { status in switch status { case let .online(proxyAddress): if proxyAddress == proxyServerSettings.host { return true } else { return false } default: return false } } |> map { _ -> Bool in return true } |> timeout(15.0, queue: Queue.mainQueue(), alternate: .single(false)) |> deliverOnMainQueue strongSelf.disposable.set(signal.start(next: { value in if let strongSelf = self { strongSelf.activityIndicator.isHidden = true strongSelf.revertSettings = nil if value { strongSelf.dismiss(true) } else { let _ = updateProxySettingsInteractively(accountManager: strongSelf.accountManager, { _ in return previousSettings }) strongSelf.titleNode.attributedText = NSAttributedString(string: strongSelf.strings.SocksProxySetup_ConnectAndSave, font: Font.regular(20.0), textColor: strongSelf.theme.controlAccentColor) strongSelf.buttonNode.isUserInteractionEnabled = true strongSelf.setNeedsLayout() strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationTheme), title: nil, text: strongSelf.strings.SocksProxySetup_FailedToConnect, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), nil) } } })) } }) } }