Display when paths are building

pull/197/head
nielsandriesse 5 years ago
parent 112ff20c73
commit e2d0002532

@ -4,19 +4,44 @@ final class PathStatusView : UIView {
override init(frame: CGRect) { override init(frame: CGRect) {
super.init(frame: frame) super.init(frame: frame)
setUpViewHierarchy() setUpViewHierarchy()
registerObservers()
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
super.init(coder: coder) super.init(coder: coder)
setUpViewHierarchy() setUpViewHierarchy()
registerObservers()
} }
private func setUpViewHierarchy() { private func setUpViewHierarchy() {
backgroundColor = Colors.accent layer.cornerRadius = Values.pathStatusViewSize / 2
layer.masksToBounds = false
let color = (OnionRequestAPI.paths.count >= OnionRequestAPI.pathCount) ? Colors.accent : Colors.destructive
setColor(to: color, isAnimated: false)
}
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)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
private func setColor(to color: UIColor, isAnimated: Bool) {
backgroundColor = color
let size = Values.pathStatusViewSize let size = Values.pathStatusViewSize
layer.cornerRadius = size / 2 let glowConfiguration = UIView.CircularGlowConfiguration(size: size, color: color, isAnimated: isAnimated, radius: isLightMode ? 6 : 8)
let glowConfiguration = UIView.CircularGlowConfiguration(size: size, color: Colors.accent, isAnimated: false, radius: isLightMode ? 6 : 8)
setCircularGlow(with: glowConfiguration) setCircularGlow(with: glowConfiguration)
layer.masksToBounds = false }
@objc private func handleBuildingPathsNotification() {
setColor(to: Colors.destructive, isAnimated: true)
}
@objc private func handlePathsBuiltNotification() {
setColor(to: Colors.accent, isAnimated: true)
} }
} }

@ -1,14 +1,45 @@
import NVActivityIndicatorView
final class PathVC : BaseVC { final class PathVC : BaseVC {
// MARK: Components
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 rebuildPathButton: Button = {
let result = Button(style: .prominentOutline, size: .large)
result.setTitle(NSLocalizedString("Rebuild Path", comment: ""), for: UIControl.State.normal)
result.addTarget(self, action: #selector(rebuildPath), for: UIControl.Event.touchUpInside)
return result
}()
// MARK: Lifecycle // MARK: Lifecycle
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
// Set gradient background setUpBackground()
setUpNavBar()
setUpViewHierarchy()
registerObservers()
}
private func setUpBackground() {
view.backgroundColor = .clear view.backgroundColor = .clear
let gradient = Gradients.defaultLokiBackground let gradient = Gradients.defaultLokiBackground
view.setGradient(gradient) view.setGradient(gradient)
// Set up navigation bar }
private func setUpNavBar() {
// Set up navigation bar style
let navigationBar = navigationController!.navigationBar let navigationBar = navigationController!.navigationBar
navigationBar.setBackgroundImage(UIImage(), for: UIBarMetrics.default) navigationBar.setBackgroundImage(UIImage(), for: UIBarMetrics.default)
navigationBar.shadowImage = UIImage() navigationBar.shadowImage = UIImage()
@ -24,6 +55,9 @@ final class PathVC : BaseVC {
titleLabel.textColor = Colors.text titleLabel.textColor = Colors.text
titleLabel.font = .boldSystemFont(ofSize: Values.veryLargeFontSize) titleLabel.font = .boldSystemFont(ofSize: Values.veryLargeFontSize)
navigationItem.titleView = titleLabel navigationItem.titleView = titleLabel
}
private func setUpViewHierarchy() {
// Set up explanation label // Set up explanation label
let explanationLabel = UILabel() let explanationLabel = UILabel()
explanationLabel.textColor = Colors.text.withAlphaComponent(Values.unimportantElementOpacity) explanationLabel.textColor = Colors.text.withAlphaComponent(Values.unimportantElementOpacity)
@ -32,49 +66,88 @@ final class PathVC : BaseVC {
explanationLabel.numberOfLines = 0 explanationLabel.numberOfLines = 0
explanationLabel.textAlignment = .center explanationLabel.textAlignment = .center
explanationLabel.lineBreakMode = .byWordWrapping 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 // 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() let pathStackViewContainer = UIView()
pathStackViewContainer.addSubview(pathStackView) pathStackViewContainer.addSubview(pathStackView)
pathStackView.pin([ UIView.VerticalEdge.top, UIView.VerticalEdge.bottom ], to: pathStackViewContainer) pathStackView.pin([ UIView.VerticalEdge.top, UIView.VerticalEdge.bottom ], to: pathStackViewContainer)
pathStackView.center(in: pathStackViewContainer) pathStackView.center(in: pathStackViewContainer)
pathStackView.leadingAnchor.constraint(greaterThanOrEqualTo: pathStackViewContainer.leadingAnchor).isActive = true pathStackView.leadingAnchor.constraint(greaterThanOrEqualTo: pathStackViewContainer.leadingAnchor).isActive = true
pathStackViewContainer.trailingAnchor.constraint(greaterThanOrEqualTo: pathStackView.trailingAnchor).isActive = true pathStackViewContainer.trailingAnchor.constraint(greaterThanOrEqualTo: pathStackView.trailingAnchor).isActive = true
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)
// Set up rebuild path button // 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() let rebuildPathButtonContainer = UIView()
rebuildPathButtonContainer.addSubview(rebuildPathButton) rebuildPathButtonContainer.addSubview(rebuildPathButton)
rebuildPathButton.pin(.leading, to: .leading, of: rebuildPathButtonContainer, withInset: 80) rebuildPathButton.pin(.leading, to: .leading, of: rebuildPathButtonContainer, withInset: 80)
rebuildPathButton.pin(.top, to: .top, of: rebuildPathButtonContainer) rebuildPathButton.pin(.top, to: .top, of: rebuildPathButtonContainer)
rebuildPathButtonContainer.pin(.trailing, to: .trailing, of: rebuildPathButton, withInset: 80) rebuildPathButtonContainer.pin(.trailing, to: .trailing, of: rebuildPathButton, withInset: 80)
rebuildPathButtonContainer.pin(.bottom, to: .bottom, of: rebuildPathButton) rebuildPathButtonContainer.pin(.bottom, to: .bottom, of: rebuildPathButton)
// Set up spacers
let topSpacer = UIView.vStretchingSpacer()
let bottomSpacer = UIView.vStretchingSpacer()
// Set up main stack view // Set up main stack view
let mainStackView = UIStackView(arrangedSubviews: [ explanationLabel, UIView.spacer(withHeight: Values.mediumSpacing), pathStackViewContainer, UIView.vStretchingSpacer(), rebuildPathButtonContainer ]) let mainStackView = UIStackView(arrangedSubviews: [ explanationLabel, topSpacer, pathStackViewContainer, bottomSpacer, rebuildPathButtonContainer ])
mainStackView.axis = .vertical mainStackView.axis = .vertical
mainStackView.alignment = .fill mainStackView.alignment = .fill
mainStackView.layoutMargins = UIEdgeInsets(top: Values.largeSpacing, left: Values.largeSpacing, bottom: Values.largeSpacing, right: Values.largeSpacing) mainStackView.layoutMargins = UIEdgeInsets(top: Values.largeSpacing, left: Values.largeSpacing, bottom: Values.largeSpacing, right: Values.largeSpacing)
mainStackView.isLayoutMarginsRelativeArrangement = true mainStackView.isLayoutMarginsRelativeArrangement = true
view.addSubview(mainStackView) view.addSubview(mainStackView)
mainStackView.pin(to: view) mainStackView.pin(to: view)
// 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)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
// MARK: Updating
@objc private func handleBuildingPathsNotification() { update() }
@objc private func handlePathsBuiltNotification() { update() }
private func update() {
pathStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
if OnionRequestAPI.paths.count >= OnionRequestAPI.pathCount {
let pathToDisplay = OnionRequestAPI.paths.first!
let dotAnimationRepeatInterval = Double(pathToDisplay.count) + 2
let snodeRows = pathToDisplay.enumerated().reversed().map { index, snode in
getPathRow(snode: snode, location: .middle, dotAnimationStartDelay: Double(index) + 2, dotAnimationRepeatInterval: dotAnimationRepeatInterval)
}
let destinationRow = getPathRow(title: NSLocalizedString("Destination", comment: ""), subtitle: nil, location: .top, dotAnimationStartDelay: Double(pathToDisplay.count) + 2, dotAnimationRepeatInterval: dotAnimationRepeatInterval)
let youRow = getPathRow(title: NSLocalizedString("You", comment: ""), subtitle: nil, location: .bottom, dotAnimationStartDelay: 1, dotAnimationRepeatInterval: dotAnimationRepeatInterval)
let rows = [ destinationRow ] + snodeRows + [ youRow ]
rows.forEach { pathStackView.addArrangedSubview($0) }
spinner.stopAnimating()
UIView.animate(withDuration: 0.25) {
self.spinner.alpha = 0
self.rebuildPathButton.layer.borderColor = Colors.accent.cgColor
self.rebuildPathButton.setTitleColor(Colors.accent, for: UIControl.State.normal)
}
rebuildPathButton.isEnabled = true
} else {
spinner.startAnimating()
UIView.animate(withDuration: 0.25) {
self.spinner.alpha = 1
self.rebuildPathButton.layer.borderColor = Colors.text.withAlphaComponent(Values.unimportantElementOpacity).cgColor
self.rebuildPathButton.setTitleColor(Colors.text.withAlphaComponent(Values.unimportantElementOpacity), for: UIControl.State.normal)
}
rebuildPathButton.isEnabled = false
}
} }
// MARK: General
private func getPathRow(title: String, subtitle: String?, location: LineView.Location, dotAnimationStartDelay: Double, dotAnimationRepeatInterval: Double) -> UIStackView { private func getPathRow(title: String, subtitle: String?, location: LineView.Location, dotAnimationStartDelay: Double, dotAnimationRepeatInterval: Double) -> UIStackView {
let lineView = LineView(location: location, dotAnimationStartDelay: dotAnimationStartDelay, dotAnimationRepeatInterval: dotAnimationRepeatInterval) let lineView = LineView(location: location, dotAnimationStartDelay: dotAnimationStartDelay, dotAnimationRepeatInterval: dotAnimationRepeatInterval)
lineView.set(.width, to: Values.pathRowDotSize) lineView.set(.width, to: Values.pathRowDotSize)
@ -116,7 +189,9 @@ final class PathVC : BaseVC {
} }
@objc private func rebuildPath() { @objc private func rebuildPath() {
// TODO: Implement OnionRequestAPI.guardSnodes = []
OnionRequestAPI.paths = []
let _ = OnionRequestAPI.buildPaths()
} }
} }
@ -193,7 +268,7 @@ private final class LineView : UIView {
private func animate() { private func animate() {
expandDot() expandDot()
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { [weak self] _ in Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { [weak self] _ in
self?.collapseDot() self?.collapseDot()
} }
} }
@ -214,7 +289,7 @@ private final class LineView : UIView {
let frame = CGRect(center: dotView.center, size: CGSize(width: size, height: size)) let frame = CGRect(center: dotView.center, size: CGSize(width: size, height: size))
dotViewWidthConstraint.constant = size dotViewWidthConstraint.constant = size
dotViewHeightConstraint.constant = size dotViewHeightConstraint.constant = size
UIView.animate(withDuration: 0.25) { UIView.animate(withDuration: 0.5) {
self.layoutIfNeeded() self.layoutIfNeeded()
self.dotView.frame = frame self.dotView.frame = frame
self.dotView.layer.cornerRadius = size / 2 self.dotView.layer.cornerRadius = size / 2

@ -4,7 +4,7 @@ import PromiseKit
/// See the "Onion Requests" section of [The Session Whitepaper](https://arxiv.org/pdf/2002.04609.pdf) for more information. /// See the "Onion Requests" section of [The Session Whitepaper](https://arxiv.org/pdf/2002.04609.pdf) for more information.
public enum OnionRequestAPI { public enum OnionRequestAPI {
/// - Note: Must only be modified from `LokiAPI.workQueue`. /// - Note: Must only be modified from `LokiAPI.workQueue`.
private static var guardSnodes: Set<LokiAPITarget> = [] public static var guardSnodes: Set<LokiAPITarget> = []
/// - Note: Must only be modified from `LokiAPI.workQueue`. /// - Note: Must only be modified from `LokiAPI.workQueue`.
public static var paths: [Path] = [] public static var paths: [Path] = []
@ -14,9 +14,9 @@ public enum OnionRequestAPI {
} }
// MARK: Settings // MARK: Settings
private static let pathCount: UInt = 2
/// The number of snodes (including the guard snode) in a path. /// The number of snodes (including the guard snode) in a path.
private static let pathSize: UInt = 3 private static let pathSize: UInt = 3
public static let pathCount: UInt = 2
private static var guardSnodeCount: UInt { return pathCount } // One per path private static var guardSnodeCount: UInt { return pathCount } // One per path
@ -100,10 +100,13 @@ public enum OnionRequestAPI {
/// Builds and returns `pathCount` paths. The returned promise errors out with `Error.insufficientSnodes` /// Builds and returns `pathCount` paths. The returned promise errors out with `Error.insufficientSnodes`
/// if not enough (reliable) snodes are available. /// if not enough (reliable) snodes are available.
private static func buildPaths() -> Promise<[Path]> { public static func buildPaths() -> Promise<[Path]> {
print("[Loki] [Onion Request API] Building onion request paths.") print("[Loki] [Onion Request API] Building onion request paths.")
DispatchQueue.main.async {
NotificationCenter.default.post(name: .buildingPaths, object: nil)
}
return LokiAPI.getRandomSnode().then(on: LokiAPI.workQueue) { _ -> Promise<[Path]> in // Just used to populate the snode pool return LokiAPI.getRandomSnode().then(on: LokiAPI.workQueue) { _ -> Promise<[Path]> in // Just used to populate the snode pool
return getGuardSnodes().map(on: LokiAPI.workQueue) { guardSnodes in return getGuardSnodes().map(on: LokiAPI.workQueue) { guardSnodes -> [Path] in
var unusedSnodes = snodePool.subtracting(guardSnodes) var unusedSnodes = snodePool.subtracting(guardSnodes)
let pathSnodeCount = guardSnodeCount * pathSize - guardSnodeCount let pathSnodeCount = guardSnodeCount * pathSize - guardSnodeCount
guard unusedSnodes.count >= pathSnodeCount else { throw Error.insufficientSnodes } guard unusedSnodes.count >= pathSnodeCount else { throw Error.insufficientSnodes }
@ -118,6 +121,12 @@ public enum OnionRequestAPI {
print("[Loki] [Onion Request API] Built new onion request path: \(result.prettifiedDescription).") print("[Loki] [Onion Request API] Built new onion request path: \(result.prettifiedDescription).")
return result return result
} }
}.map(on: LokiAPI.workQueue) { paths in
OnionRequestAPI.paths = paths
DispatchQueue.main.async {
NotificationCenter.default.post(name: .pathsBuilt, object: nil)
}
return paths
} }
} }
} }
@ -134,9 +143,7 @@ public enum OnionRequestAPI {
} }
} else { } else {
return buildPaths().map(on: LokiAPI.workQueue) { paths in return buildPaths().map(on: LokiAPI.workQueue) { paths in
let path = paths.filter { !$0.contains(snode) }.randomElement()! return paths.filter { !$0.contains(snode) }.randomElement()!
OnionRequestAPI.paths = paths
return path
} }
} }
} }

@ -20,6 +20,9 @@ public extension Notification.Name {
public static let dataNukeRequested = Notification.Name("dataNukeRequested") public static let dataNukeRequested = Notification.Name("dataNukeRequested")
// Device linking // Device linking
public static let unexpectedDeviceLinkRequestReceived = Notification.Name("unexpectedDeviceLinkRequestReceived") public static let unexpectedDeviceLinkRequestReceived = Notification.Name("unexpectedDeviceLinkRequestReceived")
// Onion requests
public static let buildingPaths = Notification.Name("buildingPaths")
public static let pathsBuilt = Notification.Name("pathsBuilt")
} }
@objc public extension NSNotification { @objc public extension NSNotification {
@ -43,4 +46,7 @@ public extension Notification.Name {
@objc public static let dataNukeRequested = Notification.Name.dataNukeRequested.rawValue as NSString @objc public static let dataNukeRequested = Notification.Name.dataNukeRequested.rawValue as NSString
// Device linking // Device linking
@objc public static let unexpectedDeviceLinkRequestReceived = Notification.Name.unexpectedDeviceLinkRequestReceived.rawValue as NSString @objc public static let unexpectedDeviceLinkRequestReceived = Notification.Name.unexpectedDeviceLinkRequestReceived.rawValue as NSString
// Onion requests
@objc public static let buildingPaths = Notification.Name.buildingPaths.rawValue as NSString
@objc public static let pathsBuilt = Notification.Name.pathsBuilt.rawValue as NSString
} }

Loading…
Cancel
Save