final class PathVC : BaseVC { // MARK: Lifecycle override func viewDidLoad() { super.viewDidLoad() // Set gradient background view.backgroundColor = .clear let gradient = Gradients.defaultLokiBackground view.setGradient(gradient) // Set up navigation bar let navigationBar = navigationController!.navigationBar navigationBar.setBackgroundImage(UIImage(), for: UIBarMetrics.default) navigationBar.shadowImage = UIImage() navigationBar.isTranslucent = false navigationBar.barTintColor = Colors.navigationBarBackground // Set up close button let closeButton = UIBarButtonItem(image: #imageLiteral(resourceName: "X"), style: .plain, target: self, action: #selector(close)) closeButton.tintColor = Colors.text navigationItem.leftBarButtonItem = closeButton // Customize title let titleLabel = UILabel() titleLabel.text = NSLocalizedString("Path", comment: "") titleLabel.textColor = Colors.text titleLabel.font = .boldSystemFont(ofSize: Values.veryLargeFontSize) navigationItem.titleView = titleLabel // Set up explanation label let explanationLabel = UILabel() explanationLabel.textColor = Colors.text.withAlphaComponent(Values.unimportantElementOpacity) explanationLabel.font = .systemFont(ofSize: Values.smallFontSize) explanationLabel.text = NSLocalizedString("Session hides your IP by routing your messages through several Service Nodes in Session's decentralized Service Node network before sending them to their destination. The Service Nodes currently being used by your device are shown below.", comment: "") explanationLabel.numberOfLines = 0 explanationLabel.textAlignment = .center explanationLabel.lineBreakMode = .byWordWrapping view.addSubview(explanationLabel) explanationLabel.pin(.leading, to: .leading, of: view, withInset: Values.largeSpacing) explanationLabel.pin(.top, to: .top, of: view, withInset: Values.mediumSpacing) explanationLabel.pin(.trailing, to: .trailing, of: view, withInset: -Values.largeSpacing) // Set up path stack view guard let mainPath = OnionRequestAPI.paths.first else { return close() // TODO: Show path establishing UI } let dotAnimationRepeatInterval = (Double(mainPath.count) + 2) * 0.5 let snodeRows = mainPath.enumerated().reversed().map { index, snode in getPathRow(snode: snode, location: .middle, dotAnimationStartDelay: (Double(index) + 1) * 0.5, dotAnimationRepeatInterval: dotAnimationRepeatInterval) } let destinationRow = getPathRow(title: NSLocalizedString("Destination", comment: ""), subtitle: nil, location: .top, dotAnimationStartDelay: (Double(mainPath.count) + 1) * 0.5, dotAnimationRepeatInterval: dotAnimationRepeatInterval) let youRow = getPathRow(title: NSLocalizedString("You", comment: ""), subtitle: nil, location: .bottom, dotAnimationStartDelay: 0, dotAnimationRepeatInterval: dotAnimationRepeatInterval) let rows = [ destinationRow ] + snodeRows + [ youRow ] let pathStackView = UIStackView(arrangedSubviews: rows) pathStackView.axis = .vertical let pathStackViewContainer = UIView() pathStackViewContainer.addSubview(pathStackView) pathStackView.pin([ UIView.VerticalEdge.top, UIView.VerticalEdge.bottom ], to: pathStackViewContainer) pathStackView.center(in: pathStackViewContainer) pathStackView.leadingAnchor.constraint(greaterThanOrEqualTo: pathStackViewContainer.leadingAnchor).isActive = true pathStackViewContainer.trailingAnchor.constraint(greaterThanOrEqualTo: pathStackView.trailingAnchor).isActive = true // Set up rebuild path button let rebuildPathButton = Button(style: .prominentOutline, size: .large) rebuildPathButton.setTitle(NSLocalizedString("Rebuild Path", comment: ""), for: UIControl.State.normal) rebuildPathButton.addTarget(self, action: #selector(rebuildPath), for: UIControl.Event.touchUpInside) let rebuildPathButtonContainer = UIView() rebuildPathButtonContainer.addSubview(rebuildPathButton) rebuildPathButton.pin(.leading, to: .leading, of: rebuildPathButtonContainer, withInset: 80) rebuildPathButton.pin(.top, to: .top, of: rebuildPathButtonContainer) rebuildPathButtonContainer.pin(.trailing, to: .trailing, of: rebuildPathButton, withInset: 80) rebuildPathButtonContainer.pin(.bottom, to: .bottom, of: rebuildPathButton) // Set up main stack view let mainStackView = UIStackView(arrangedSubviews: [ explanationLabel, UIView.spacer(withHeight: Values.mediumSpacing), pathStackViewContainer, UIView.vStretchingSpacer(), rebuildPathButtonContainer ]) mainStackView.axis = .vertical mainStackView.alignment = .fill mainStackView.layoutMargins = UIEdgeInsets(top: Values.largeSpacing, left: Values.largeSpacing, bottom: Values.largeSpacing, right: Values.largeSpacing) mainStackView.isLayoutMarginsRelativeArrangement = true view.addSubview(mainStackView) mainStackView.pin(to: view) } private func getPathRow(title: String, subtitle: String?, location: LineView.Location, dotAnimationStartDelay: Double, dotAnimationRepeatInterval: Double) -> UIStackView { let lineView = LineView(location: location, dotAnimationStartDelay: dotAnimationStartDelay, dotAnimationRepeatInterval: dotAnimationRepeatInterval) lineView.set(.width, to: Values.pathRowDotSize) lineView.set(.height, to: Values.pathRowHeight) let titleLabel = UILabel() titleLabel.textColor = Colors.text titleLabel.font = .systemFont(ofSize: Values.mediumFontSize) titleLabel.text = title titleLabel.lineBreakMode = .byTruncatingTail let titleStackView = UIStackView(arrangedSubviews: [ titleLabel ]) titleStackView.axis = .vertical if let subtitle = subtitle { let subtitleLabel = UILabel() subtitleLabel.textColor = Colors.text subtitleLabel.font = .systemFont(ofSize: Values.verySmallFontSize) subtitleLabel.text = subtitle subtitleLabel.lineBreakMode = .byTruncatingTail titleStackView.addArrangedSubview(subtitleLabel) } let stackView = UIStackView(arrangedSubviews: [ lineView, titleStackView ]) stackView.axis = .horizontal stackView.spacing = Values.largeSpacing stackView.alignment = .center return stackView } private func getPathRow(snode: LokiAPITarget, location: LineView.Location, dotAnimationStartDelay: Double, dotAnimationRepeatInterval: Double) -> UIStackView { var snodeIP = snode.description if snodeIP.hasPrefix("https://") { snodeIP.removeFirst(8) } if let colonIndex = snodeIP.lastIndex(of: ":") { snodeIP = String(snodeIP[snodeIP.startIndex..