import Foundation import UIKit import Display import TelegramCore import SyncCore import Postbox import AsyncDisplayKit import UIKit import SwiftSignalKit import TelegramPresentationData import ActivityIndicator import OverlayStatusController import AccountContext import PresentationDataUtils 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(presentationData: presentationData, accountManager: context.sharedContext.accountManager, postbox: context.account.postbox, network: context.account.network, server: server, updatedPresentationData: context.sharedContext.presentationData) } public init(presentationData: PresentationData, accountManager: AccountManager, postbox: Postbox, network: Network, server: ProxyServerSettings, updatedPresentationData: Signal?) { let sheetTheme = ActionSheetControllerTheme(presentationData: presentationData) super.init(theme: sheetTheme) self._ready.set(.single(true)) var items: [ActionSheetItem] = [] if case .mtp = server.connection { items.append(ActionSheetTextItem(title: presentationData.strings.SocksProxySetup_AdNoticeHelp)) } items.append(ProxyServerInfoItem(strings: presentationData.strings, network: network, server: server)) items.append(ProxyServerActionItem(accountManager:accountManager, postbox: postbox, network: network, presentationData: presentationData, server: server, dismiss: { [weak self] success in guard let strongSelf = self, !strongSelf.isDismissed else { return } strongSelf.isDismissed = true if success { strongSelf.present(OverlayStatusController(theme: presentationData.theme, type: .shieldSuccess(presentationData.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: presentationData.strings.Common_Cancel, action: { [weak self] in self?.dismissAnimated() }) ]) ]) if let updatedPresentationData = updatedPresentationData { self.presentationDisposable = updatedPresentationData.start(next: { [weak self] presentationData in if let strongSelf = self { strongSelf.theme = ActionSheetControllerTheme(presentationData: presentationData) } }) } } 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 enum ProxyServerInfoStatusType { case generic(String) case failed(String) } private final class ProxyServerInfoItemNode: ActionSheetItemNode { private let theme: ActionSheetControllerTheme private let strings: PresentationStrings private let textFont: UIFont 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 self.textFont = Font.regular(floor(theme.baseFontSize * 16.0 / 17.0)) 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() } public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { let size = CGSize(width: constrainedSize.width, height: 36.0 * CGFloat(self.fieldNodes.count) + 12.0) 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 } self.updateInternalLayout(size, constrainedSize: constrainedSize) return size } } private final class ProxyServerActionItem: ActionSheetItem { private let accountManager: AccountManager private let postbox: Postbox private let network: Network private let presentationData: PresentationData private let server: ProxyServerSettings private let dismiss: (Bool) -> Void private let present: (ViewController, Any?) -> Void init(accountManager: AccountManager, postbox: Postbox, network: Network, presentationData: PresentationData, server: ProxyServerSettings, dismiss: @escaping (Bool) -> Void, present: @escaping (ViewController, Any?) -> Void) { self.accountManager = accountManager self.postbox = postbox self.network = network self.presentationData = presentationData 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, presentationData: self.presentationData, theme: theme, 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 presentationData: PresentationData private let theme: ActionSheetControllerTheme 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, presentationData: PresentationData, theme: ActionSheetControllerTheme, server: ProxyServerSettings, dismiss: @escaping (Bool) -> Void, present: @escaping (ViewController, Any?) -> Void) { self.accountManager = accountManager self.postbox = postbox self.network = network self.theme = theme self.presentationData = presentationData self.server = server self.dismiss = dismiss self.present = present let titleFont = Font.regular(floor(theme.baseFontSize * 20.0 / 17.0)) self.titleNode = ImmediateTextNode() self.titleNode.isUserInteractionEnabled = false self.titleNode.displaysAsynchronously = false self.titleNode.attributedText = NSAttributedString(string: presentationData.strings.SocksProxySetup_ConnectAndSave, font: titleFont, 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 }) } } public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { let size = CGSize(width: constrainedSize.width, height: 57.0) 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) self.updateInternalLayout(size, constrainedSize: constrainedSize) return size } @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.presentationData.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.presentationData.strings.SocksProxySetup_ConnectAndSave, font: Font.regular(20.0), textColor: strongSelf.theme.controlAccentColor) strongSelf.buttonNode.isUserInteractionEnabled = true strongSelf.setNeedsLayout() strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.SocksProxySetup_FailedToConnect, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), nil) } } })) } }) } }