2020-05-28 03:01:42 +02:00
import NVActivityIndicatorView
2020-06-02 03:11:09 +02:00
import SwiftCSV
2020-05-27 08:48:29 +02:00
final class PathVC : BaseVC {
2020-05-28 03:01:42 +02:00
2020-06-02 03:11:09 +02:00
static let ipv4Table = try ! CSV ( name : " GeoLite2-Country-Blocks-IPv4 " , extension : " csv " , bundle : . main , delimiter : " , " , encoding : . utf8 , loadColumns : true ) !
static let countryNamesTable = try ! CSV ( name : " GeoLite2-Country-Locations-English " , extension : " csv " , bundle : . main , delimiter : " , " , encoding : . utf8 , loadColumns : true ) !
static var countryNamesCache : [ String : String ] = [ : ]
2020-05-28 03:01:42 +02:00
// MARK: C o m p o n e n t s
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
} ( )
2020-05-27 08:48:29 +02:00
// MARK: L i f e c y c l e
override func viewDidLoad ( ) {
super . viewDidLoad ( )
2020-05-30 00:20:30 +02:00
setUpGradientBackground ( )
2020-05-28 03:01:42 +02:00
setUpNavBar ( )
setUpViewHierarchy ( )
registerObservers ( )
}
private func setUpNavBar ( ) {
2020-05-30 00:20:30 +02:00
setUpNavBarStyle ( )
setNavBarTitle ( NSLocalizedString ( " Path " , comment : " " ) )
2020-05-27 08:48:29 +02:00
// S e t u p c l o s e b u t t o n
let closeButton = UIBarButtonItem ( image : # imageLiteral ( resourceName : " X " ) , style : . plain , target : self , action : #selector ( close ) )
closeButton . tintColor = Colors . text
navigationItem . leftBarButtonItem = closeButton
2020-05-29 02:11:01 +02:00
let learnMoreButton = UIBarButtonItem ( image : # imageLiteral ( resourceName : " QuestionMark " ) . scaled ( to : CGSize ( width : 24 , height : 24 ) ) , style : . plain , target : self , action : #selector ( learnMore ) )
learnMoreButton . tintColor = Colors . text
navigationItem . rightBarButtonItem = learnMoreButton
2020-05-28 03:01:42 +02:00
}
private func setUpViewHierarchy ( ) {
2020-05-27 08:48:29 +02:00
// S e t u p e x p l a n a t i o n l a b e l
let explanationLabel = UILabel ( )
explanationLabel . textColor = Colors . text . withAlphaComponent ( Values . unimportantElementOpacity )
explanationLabel . font = . systemFont ( ofSize : Values . smallFontSize )
2020-05-28 03:43:49 +02:00
explanationLabel . text = NSLocalizedString ( " Session hides your IP by bouncing your messages through several Service Nodes in Session’ s decentralized network. These are the Service Nodes currently being used by your device: " , comment : " " )
2020-05-27 08:48:29 +02:00
explanationLabel . numberOfLines = 0
explanationLabel . textAlignment = . center
explanationLabel . lineBreakMode = . byWordWrapping
// S e t u p p a t h s t a c k v i e w
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
// S e t u p r e b u i l d p a t h b u t t o n
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 )
2020-05-28 03:01:42 +02:00
// S e t u p s p a c e r s
let topSpacer = UIView . vStretchingSpacer ( )
let bottomSpacer = UIView . vStretchingSpacer ( )
2020-05-28 01:52:13 +02:00
// S e t u p m a i n s t a c k v i e w
2020-05-28 03:01:42 +02:00
let mainStackView = UIStackView ( arrangedSubviews : [ explanationLabel , topSpacer , pathStackViewContainer , bottomSpacer , rebuildPathButtonContainer ] )
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
// S e t u p s p a c e r c o n s t r a i n t s
topSpacer . heightAnchor . constraint ( equalTo : bottomSpacer . heightAnchor ) . isActive = true
// P e r f o r m i n i t i a l u p d a t e
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: U p d a t i n g
@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
2020-05-28 05:40:18 +02:00
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 )
2020-05-28 03:01:42 +02:00
}
2020-05-28 05:40:18 +02:00
let youRow = getPathRow ( title : NSLocalizedString ( " You " , comment : " " ) , subtitle : nil , location : . top , dotAnimationStartDelay : 1 , dotAnimationRepeatInterval : dotAnimationRepeatInterval )
let destinationRow = getPathRow ( title : NSLocalizedString ( " Destination " , comment : " " ) , subtitle : nil , location : . bottom , dotAnimationStartDelay : Double ( pathToDisplay . count ) + 2 , dotAnimationRepeatInterval : dotAnimationRepeatInterval )
let rows = [ youRow ] + snodeRows + [ destinationRow ]
2020-05-28 03:01:42 +02:00
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
}
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: G e n e r a l
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 )
2020-05-27 08:48:29 +02:00
lineView . set ( . width , to : Values . pathRowDotSize )
2020-05-28 01:52:13 +02:00
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 )
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
2020-05-28 05:40:18 +02:00
private func getPathRow ( snode : LokiAPITarget , location : LineView . Location , dotAnimationStartDelay : Double , dotAnimationRepeatInterval : Double , isGuardSnode : Bool ) -> UIStackView {
2020-06-02 03:11:09 +02:00
var snodeIP = snode . ip
func getCountry ( ) -> String {
if let country = PathVC . countryNamesCache [ snode . address ] { return country }
if let ipv4TableIndex = PathVC . ipv4Table . namedColumns [ " network " ] ! . firstIndex ( where : { $0 . starts ( with : snodeIP ) } ) {
let countryID = PathVC . ipv4Table . namedColumns [ " registered_country_geoname_id " ] ! [ ipv4TableIndex ]
if let countryNamesTableIndex = PathVC . countryNamesTable . namedColumns [ " geoname_id " ] ! . firstIndex ( of : countryID ) {
let country = PathVC . countryNamesTable . namedColumns [ " country_name " ] ! [ countryNamesTableIndex ]
PathVC . countryNamesCache [ snode . address ] = country
return country
}
}
if snodeIP . contains ( " . " ) && ! snodeIP . hasSuffix ( " . " ) { // T h e f u z z i e s t w e w a n t t o g o i s x x x . x
snodeIP . removeLast ( )
if snodeIP . hasSuffix ( " . " ) { snodeIP . removeLast ( ) }
return getCountry ( )
} else {
return " Unknown Country "
}
2020-05-28 01:52:13 +02:00
}
2020-05-28 05:40:18 +02:00
let title = isGuardSnode ? NSLocalizedString ( " Guard Node " , comment : " " ) : NSLocalizedString ( " Service Node " , comment : " " )
2020-06-02 03:11:09 +02:00
return getPathRow ( title : title , subtitle : getCountry ( ) , location : location , dotAnimationStartDelay : dotAnimationStartDelay , dotAnimationRepeatInterval : dotAnimationRepeatInterval )
2020-05-28 01:52:13 +02:00
}
2020-05-27 08:48:29 +02:00
// MARK: I n t e r a c t i o n
@objc private func close ( ) {
dismiss ( animated : true , completion : nil )
}
2020-05-28 01:52:13 +02:00
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-28 01:52:13 +02:00
@objc private func rebuildPath ( ) {
2020-05-30 00:20:30 +02:00
// D i s p a t c h a s y n c o n t h e m a i n q u e u e t o a v o i d n e s t e d w r i t e t r a n s a c t i o n s
DispatchQueue . main . async {
let storage = OWSPrimaryStorage . shared ( )
storage . dbReadWriteConnection . readWrite { transaction in
storage . clearOnionRequestPaths ( in : transaction )
}
}
2020-05-28 03:01:42 +02:00
OnionRequestAPI . guardSnodes = [ ]
OnionRequestAPI . paths = [ ]
let _ = OnionRequestAPI . buildPaths ( )
2020-05-28 01:52:13 +02:00
}
2020-05-27 08:48:29 +02:00
}
// MARK: L i n e V i e w
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 ( )
result . layer . cornerRadius = Values . pathRowDotSize / 2
let glowConfiguration = UIView . CircularGlowConfiguration ( size : Values . pathRowDotSize , color : Colors . accent , isAnimated : true , radius : isLightMode ? 2 : 4 )
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 )
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 ( )
lineView . set ( . width , to : Values . pathRowLineThickness )
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
}
let dotSize = Values . pathRowDotSize
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 ( ) {
let newSize = Values . pathRowExpandedDotSize
let newGlowRadius : CGFloat = isLightMode ? 6 : 8
updateDotView ( size : newSize , glowRadius : newGlowRadius )
}
private func collapseDot ( ) {
let newSize = Values . pathRowDotSize
let newGlowRadius : CGFloat = isLightMode ? 2 : 4
updateDotView ( size : newSize , glowRadius : newGlowRadius )
}
private func updateDotView ( size : CGFloat , glowRadius : CGFloat ) {
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
let glowColor = Colors . accent
let glowConfiguration = UIView . CircularGlowConfiguration ( size : size , color : glowColor , isAnimated : true , radius : glowRadius )
self . dotView . setCircularGlow ( with : glowConfiguration )
self . dotView . backgroundColor = Colors . accent
}
2020-05-27 08:48:29 +02:00
}
}