session-ios/Session/Path/PathVC.swift

315 lines
14 KiB
Swift
Raw Normal View History

// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import NVActivityIndicatorView
import SessionMessagingKit
import SessionUIKit
2020-05-27 08:48:29 +02:00
final class PathVC: BaseVC {
public static let dotSize: CGFloat = 8
public static let expandedDotSize: CGFloat = 16
private static let rowHeight: CGFloat = (isIPhone5OrSmaller ? 52 : 75)
2020-05-28 03:01:42 +02:00
// MARK: - Components
2020-05-28 03:01:42 +02:00
private lazy var pathStackView: UIStackView = {
let result = UIStackView()
result.axis = .vertical
return result
}()
private lazy var spinner: NVActivityIndicatorView = {
let result = NVActivityIndicatorView(frame: CGRect.zero, type: .circleStrokeSpin, color: Colors.text, padding: nil)
result.set(.width, to: 64)
result.set(.height, to: 64)
return result
}()
private lazy var learnMoreButton: OutlineButton = {
let result = OutlineButton(style: .regular, size: .large)
result.setTitle("vc_path_learn_more_button_title".localized(), for: UIControl.State.normal)
2020-06-02 03:25:19 +02:00
result.addTarget(self, action: #selector(learnMore), for: UIControl.Event.touchUpInside)
2020-05-28 03:01:42 +02:00
return result
}()
// MARK: - Lifecycle
2020-05-27 08:48:29 +02:00
override func viewDidLoad() {
super.viewDidLoad()
2020-05-28 03:01:42 +02:00
setUpNavBar()
setUpViewHierarchy()
registerObservers()
}
private func setUpNavBar() {
setNavBarTitle("vc_path_title".localized())
2020-05-28 03:01:42 +02:00
}
private func setUpViewHierarchy() {
2020-05-27 08:48:29 +02:00
// Set up explanation label
let explanationLabel = UILabel()
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
explanationLabel.textColor = Colors.text.withAlphaComponent(Values.mediumOpacity)
explanationLabel.text = "vc_path_explanation".localized()
2020-05-27 08:48:29 +02:00
explanationLabel.numberOfLines = 0
explanationLabel.textAlignment = .center
explanationLabel.lineBreakMode = .byWordWrapping
// Set up path stack view
2020-05-28 01:52:13 +02:00
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
2020-05-28 03:01:42 +02:00
pathStackViewContainer.addSubview(spinner)
spinner.leadingAnchor.constraint(greaterThanOrEqualTo: pathStackViewContainer.leadingAnchor).isActive = true
spinner.topAnchor.constraint(greaterThanOrEqualTo: pathStackViewContainer.topAnchor).isActive = true
pathStackViewContainer.trailingAnchor.constraint(greaterThanOrEqualTo: spinner.trailingAnchor).isActive = true
pathStackViewContainer.bottomAnchor.constraint(greaterThanOrEqualTo: spinner.bottomAnchor).isActive = true
spinner.center(in: pathStackViewContainer)
2020-05-28 01:52:13 +02:00
// Set up rebuild path button
let inset: CGFloat = isIPhone5OrSmaller ? 64 : 80
2022-03-03 04:31:18 +01:00
let learnMoreButtonContainer = UIView(wrapping: learnMoreButton, withInsets: UIEdgeInsets(top: 0, leading: inset, bottom: 0, trailing: inset), shouldAdaptForIPadWithWidth: Values.iPadButtonWidth)
2020-05-28 03:01:42 +02:00
// Set up spacers
let topSpacer = UIView.vStretchingSpacer()
let bottomSpacer = UIView.vStretchingSpacer()
2020-05-28 01:52:13 +02:00
// Set up main stack view
2020-06-02 03:25:19 +02:00
let mainStackView = UIStackView(arrangedSubviews: [ explanationLabel, topSpacer, pathStackViewContainer, bottomSpacer, learnMoreButtonContainer ])
2020-05-28 01:52:13 +02:00
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)
2020-05-28 03:01:42 +02:00
// Set up spacer constraints
topSpacer.heightAnchor.constraint(equalTo: bottomSpacer.heightAnchor).isActive = true
// Perform initial update
update()
}
private func registerObservers() {
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self, selector: #selector(handleBuildingPathsNotification), name: .buildingPaths, object: nil)
notificationCenter.addObserver(self, selector: #selector(handlePathsBuiltNotification), name: .pathsBuilt, object: nil)
notificationCenter.addObserver(self, selector: #selector(handleOnionRequestPathCountriesLoadedNotification), name: .onionRequestPathCountriesLoaded, object: nil)
2020-05-28 03:01:42 +02:00
}
deinit {
NotificationCenter.default.removeObserver(self)
}
// MARK: Updating
@objc private func handleBuildingPathsNotification() { update() }
@objc private func handlePathsBuiltNotification() { update() }
@objc private func handleOnionRequestPathCountriesLoadedNotification() { update() }
2020-05-28 03:01:42 +02:00
private func update() {
pathStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
guard let pathToDisplay: [Snode] = OnionRequestAPI.paths.first else {
2020-05-28 03:01:42 +02:00
spinner.startAnimating()
2020-05-28 03:01:42 +02:00
UIView.animate(withDuration: 0.25) {
self.spinner.alpha = 1
}
return
}
let dotAnimationRepeatInterval = Double(pathToDisplay.count) + 2
let snodeRows: [UIStackView] = pathToDisplay.enumerated().map { index, snode in
let isGuardSnode = (snode == pathToDisplay.first)
return getPathRow(
snode: snode,
location: .middle,
dotAnimationStartDelay: Double(index) + 2,
dotAnimationRepeatInterval: dotAnimationRepeatInterval,
isGuardSnode: isGuardSnode
)
}
let youRow = getPathRow(
title: NSLocalizedString("vc_path_device_row_title", comment: ""),
subtitle: nil,
location: .top,
dotAnimationStartDelay: 1,
dotAnimationRepeatInterval: dotAnimationRepeatInterval
)
let destinationRow = getPathRow(
title: NSLocalizedString("vc_path_destination_row_title", comment: ""),
subtitle: nil,
location: .bottom,
dotAnimationStartDelay: Double(pathToDisplay.count) + 2,
dotAnimationRepeatInterval: dotAnimationRepeatInterval
)
let rows = [ youRow ] + snodeRows + [ destinationRow ]
rows.forEach { pathStackView.addArrangedSubview($0) }
spinner.stopAnimating()
UIView.animate(withDuration: 0.25) {
self.spinner.alpha = 0
2020-05-28 03:01:42 +02:00
}
2020-05-27 08:48:29 +02:00
}
2020-05-28 01:52:13 +02:00
2020-05-28 03:01:42 +02:00
// MARK: General
2020-05-28 01:52:13 +02:00
private func getPathRow(title: String, subtitle: String?, location: LineView.Location, dotAnimationStartDelay: Double, dotAnimationRepeatInterval: Double) -> UIStackView {
let lineView = LineView(
location: location,
dotAnimationStartDelay: dotAnimationStartDelay,
dotAnimationRepeatInterval: dotAnimationRepeatInterval
)
2021-02-22 05:10:01 +01:00
lineView.set(.width, to: PathVC.expandedDotSize)
lineView.set(.height, to: PathVC.rowHeight)
let titleLabel: UILabel = UILabel()
2020-05-28 01:52:13 +02:00
titleLabel.font = .systemFont(ofSize: Values.mediumFontSize)
titleLabel.text = title
titleLabel.themeTextColor = .textPrimary
2020-05-28 01:52:13 +02:00
titleLabel.lineBreakMode = .byTruncatingTail
2020-05-28 01:52:13 +02:00
let titleStackView = UIStackView(arrangedSubviews: [ titleLabel ])
titleStackView.axis = .vertical
2020-05-28 01:52:13 +02:00
if let subtitle = subtitle {
let subtitleLabel = UILabel()
subtitleLabel.font = .systemFont(ofSize: Values.verySmallFontSize)
subtitleLabel.text = subtitle
subtitleLabel.themeTextColor = .textPrimary
2020-05-28 01:52:13 +02:00
subtitleLabel.lineBreakMode = .byTruncatingTail
titleStackView.addArrangedSubview(subtitleLabel)
2020-05-27 08:48:29 +02:00
}
2020-05-28 01:52:13 +02:00
let stackView = UIStackView(arrangedSubviews: [ lineView, titleStackView ])
2020-05-27 08:48:29 +02:00
stackView.axis = .horizontal
stackView.spacing = Values.largeSpacing
2020-05-28 01:52:13 +02:00
stackView.alignment = .center
2020-05-27 08:48:29 +02:00
return stackView
}
2020-05-28 01:52:13 +02:00
private func getPathRow(snode: Snode, location: LineView.Location, dotAnimationStartDelay: Double, dotAnimationRepeatInterval: Double, isGuardSnode: Bool) -> UIStackView {
2020-06-03 05:28:09 +02:00
let country = IP2Country.isInitialized ? (IP2Country.shared.countryNamesCache[snode.ip] ?? "Resolving...") : "Resolving..."
let title = isGuardSnode ? NSLocalizedString("vc_path_guard_node_row_title", comment: "") : NSLocalizedString("vc_path_service_node_row_title", comment: "")
2020-06-02 04:02:54 +02:00
return getPathRow(title: title, subtitle: country, location: location, dotAnimationStartDelay: dotAnimationStartDelay, dotAnimationRepeatInterval: dotAnimationRepeatInterval)
2020-05-28 01:52:13 +02:00
}
2020-05-27 08:48:29 +02:00
// MARK: Interaction
2020-05-29 02:11:01 +02:00
@objc private func learnMore() {
let urlAsString = "https://getsession.org/faq/#onion-routing"
let url = URL(string: urlAsString)!
UIApplication.shared.open(url)
}
2020-05-27 08:48:29 +02:00
}
// MARK: Line View
private final class LineView : UIView {
private let location: Location
2020-05-28 01:52:13 +02:00
private let dotAnimationStartDelay: Double
private let dotAnimationRepeatInterval: Double
private var dotViewWidthConstraint: NSLayoutConstraint!
private var dotViewHeightConstraint: NSLayoutConstraint!
private var dotViewAnimationTimer: Timer!
2020-05-27 08:48:29 +02:00
enum Location {
case top, middle, bottom
}
2020-05-28 01:52:13 +02:00
private lazy var dotView: UIView = {
let result = UIView()
2021-02-22 05:10:01 +01:00
result.layer.cornerRadius = PathVC.dotSize / 2
2020-08-04 03:15:53 +02:00
let glowRadius: CGFloat = isLightMode ? 1 : 2
let glowColor = isLightMode ? UIColor.black.withAlphaComponent(0.4) : UIColor.black
2021-02-22 05:10:01 +01:00
let glowConfiguration = UIView.CircularGlowConfiguration(size: PathVC.dotSize, color: glowColor, isAnimated: true, animationDuration: 0.5, radius: glowRadius)
2020-05-28 01:52:13 +02:00
result.setCircularGlow(with: glowConfiguration)
result.backgroundColor = Colors.accent
return result
}()
2020-05-27 08:48:29 +02:00
2020-05-28 01:52:13 +02:00
init(location: Location, dotAnimationStartDelay: Double, dotAnimationRepeatInterval: Double) {
2020-05-27 08:48:29 +02:00
self.location = location
2020-05-28 01:52:13 +02:00
self.dotAnimationStartDelay = dotAnimationStartDelay
self.dotAnimationRepeatInterval = dotAnimationRepeatInterval
2020-05-27 08:48:29 +02:00
super.init(frame: CGRect.zero)
2020-05-27 08:48:29 +02:00
setUpViewHierarchy()
}
override init(frame: CGRect) {
2020-05-28 01:52:13 +02:00
preconditionFailure("Use init(location:dotAnimationStartDelay:dotAnimationRepeatInterval:) instead.")
2020-05-27 08:48:29 +02:00
}
required init?(coder: NSCoder) {
2020-05-28 01:52:13 +02:00
preconditionFailure("Use init(location:dotAnimationStartDelay:dotAnimationRepeatInterval:) instead.")
2020-05-27 08:48:29 +02:00
}
private func setUpViewHierarchy() {
let lineView = UIView()
2021-02-22 05:10:01 +01:00
lineView.set(.width, to: Values.separatorThickness)
2020-05-27 08:48:29 +02:00
lineView.backgroundColor = Colors.text
addSubview(lineView)
lineView.center(.horizontal, in: self)
switch location {
case .top: lineView.topAnchor.constraint(equalTo: centerYAnchor).isActive = true
case .middle, .bottom: lineView.pin(.top, to: .top, of: self)
}
switch location {
case .top, .middle: lineView.pin(.bottom, to: .bottom, of: self)
case .bottom: lineView.bottomAnchor.constraint(equalTo: centerYAnchor).isActive = true
}
2021-02-22 05:10:01 +01:00
let dotSize = PathVC.dotSize
2020-05-28 01:52:13 +02:00
dotViewWidthConstraint = dotView.set(.width, to: dotSize)
dotViewHeightConstraint = dotView.set(.height, to: dotSize)
2020-05-27 08:48:29 +02:00
addSubview(dotView)
dotView.center(in: self)
2020-05-28 01:52:13 +02:00
Timer.scheduledTimer(withTimeInterval: dotAnimationStartDelay, repeats: false) { [weak self] _ in
guard let strongSelf = self else { return }
strongSelf.animate()
strongSelf.dotViewAnimationTimer = Timer.scheduledTimer(withTimeInterval: strongSelf.dotAnimationRepeatInterval, repeats: true) { _ in
guard let strongSelf = self else { return }
strongSelf.animate()
}
}
}
deinit {
dotViewAnimationTimer?.invalidate()
}
private func animate() {
expandDot()
2020-05-28 03:01:42 +02:00
Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { [weak self] _ in
2020-05-28 01:52:13 +02:00
self?.collapseDot()
}
}
private func expandDot() {
2021-02-22 05:10:01 +01:00
let newSize = PathVC.expandedDotSize
2020-08-04 03:15:53 +02:00
let newGlowRadius: CGFloat = isLightMode ? 4 : 6
let newGlowColor = Colors.accent.withAlphaComponent(0.6)
updateDotView(size: newSize, glowRadius: newGlowRadius, glowColor: newGlowColor)
2020-05-28 01:52:13 +02:00
}
private func collapseDot() {
2021-02-22 05:10:01 +01:00
let newSize = PathVC.dotSize
2020-08-04 03:15:53 +02:00
let newGlowRadius: CGFloat = isLightMode ? 1 : 2
let newGlowColor = isLightMode ? UIColor.black.withAlphaComponent(0.4) : UIColor.black
updateDotView(size: newSize, glowRadius: newGlowRadius, glowColor: newGlowColor)
2020-05-28 01:52:13 +02:00
}
2020-08-04 03:15:53 +02:00
private func updateDotView(size: CGFloat, glowRadius: CGFloat, glowColor: UIColor) {
2020-05-28 01:52:13 +02:00
let frame = CGRect(center: dotView.center, size: CGSize(width: size, height: size))
dotViewWidthConstraint.constant = size
dotViewHeightConstraint.constant = size
2020-05-28 03:01:42 +02:00
UIView.animate(withDuration: 0.5) {
2020-05-28 01:52:13 +02:00
self.layoutIfNeeded()
self.dotView.frame = frame
self.dotView.layer.cornerRadius = size / 2
2020-08-04 03:15:53 +02:00
let glowConfiguration = UIView.CircularGlowConfiguration(size: size, color: glowColor, isAnimated: true, animationDuration: 0.5, radius: glowRadius)
2020-05-28 01:52:13 +02:00
self.dotView.setCircularGlow(with: glowConfiguration)
self.dotView.backgroundColor = Colors.accent
}
2020-05-27 08:48:29 +02:00
}
}