@ -28,7 +28,7 @@ final class PathVC : BaseVC {
let explanationLabel = UILabel ( )
explanationLabel . textColor = Colors . text . withAlphaComponent ( Values . unimportantElementOpacity )
explanationLabel . font = . systemFont ( ofSize : Values . smallFontSize )
explanationLabel . text = NSLocalizedString ( " Session hides your IP by onion routing your messages through Session's decentralized Service Node network. The Service Nodes currently being used for this are shown below." , comment : " " )
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
@ -37,84 +37,128 @@ final class PathVC : BaseVC {
explanationLabel . pin ( . top , to : . top , of : view , withInset : Values . mediumSpacing )
explanationLabel . pin ( . trailing , to : . trailing , of : view , withInset : - Values . largeSpacing )
// S e t u p p a t h s t a c k v i e w
guard var mainPath = OnionRequestAPI . paths . first else {
guard let mainPath = OnionRequestAPI . paths . first else {
return close ( ) // TODO: S h o w p a t h e s t a b l i s h i n g U I
}
let rows : [ UIStackView ]
switch mainPath . count {
case 1 : return // TODO: D o w e w a n t t o h a n d l e t h i s c a s e ?
case 2 :
let topPathRow = getPathRow ( forSnode : mainPath [ 1 ] , at : . top )
let bottomPathRow = getPathRow ( forSnode : mainPath [ 0 ] , at : . bottom )
rows = [ topPathRow , bottomPathRow ]
default :
let topPathRow = getPathRow ( forSnode : mainPath . removeLast ( ) , at : . top )
let bottomPathRow = getPathRow ( forSnode : mainPath . removeFirst ( ) , at : . bottom )
let middlePathRows = mainPath . map {
getPathRow ( forSnode : $0 , at : . middle )
}
rows = [ topPathRow ] + middlePathRows + [ bottomPathRow ]
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
view . addSubview ( pathStackView )
pathStackView . pin ( . top , to : . bottom , of : explanationLabel , withInset : Values . largeSpacing )
pathStackView . center ( . horizontal , in : view )
pathStackView . leadingAnchor . constraint ( greaterThanOrEqualTo : view . leadingAnchor , constant : Values . largeSpacing ) . isActive = true
view . trailingAnchor . constraint ( greaterThanOrEqualTo : pathStackView . trailingAnchor , constant : Values . largeSpacing ) . isActive = true
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
// S e t u p r e b u i l d p a t h b u t t o n
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 )
// S e t u p m a i n s t a c k v i e w
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 ( forSnode snode : LokiAPITarget , at location : LineView . Location ) -> UIStackView {
let lineView = LineView ( location : location )
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 )
let snodeLabel = UILabel ( )
snodeLabel . textColor = Colors . text
snodeLabel . font = . systemFont ( ofSize : Values . mediumFontSize )
var snodeDescription = snode . description
if snodeDescription . hasPrefix ( " https:// " ) {
snodeDescription . removeFirst ( 8 )
}
if let colonIndex = snodeDescription . lastIndex ( of : " : " ) {
snodeDescription = String ( snodeDescription [ snodeDescription . startIndex . . < colonIndex ] )
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 )
}
snodeLabel . text = snodeDescription
snodeLabel . lineBreakMode = . byTruncatingTail
let stackView = UIStackView ( arrangedSubviews : [ lineView , snodeLabel ] )
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 . . < colonIndex ] )
}
return getPathRow ( title : NSLocalizedString ( " Service Node " , comment : " " ) , subtitle : snodeIP , location : location , dotAnimationStartDelay : dotAnimationStartDelay , dotAnimationRepeatInterval : dotAnimationRepeatInterval )
}
// MARK: I n t e r a c t i o n
@objc private func close ( ) {
dismiss ( animated : true , completion : nil )
}
@objc private func rebuildPath ( ) {
// TODO: I m p l e m e n t
}
}
// MARK: L i n e V i e w
private final class LineView : UIView {
private let location : Location
private let dotAnimationStartDelay : Double
private let dotAnimationRepeatInterval : Double
private var dotViewWidthConstraint : NSLayoutConstraint !
private var dotViewHeightConstraint : NSLayoutConstraint !
private var dotViewAnimationTimer : Timer !
enum Location {
case top , middle , bottom
}
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
} ( )
init ( location : Location ) {
init ( location : Location , dotAnimationStartDelay : Double , dotAnimationRepeatInterval : Double ) {
self . location = location
self . dotAnimationStartDelay = dotAnimationStartDelay
self . dotAnimationRepeatInterval = dotAnimationRepeatInterval
super . init ( frame : CGRect . zero )
setUpViewHierarchy ( )
}
override init ( frame : CGRect ) {
preconditionFailure ( " Use init(location:) instead. " )
preconditionFailure ( " Use init(location: dotAnimationStartDelay:dotAnimationRepeatInterval: ) instead." )
}
required init ? ( coder : NSCoder ) {
preconditionFailure ( " Use init(location: ) instead." )
preconditionFailure ( " Use init(location: dotAnimationStartDelay:dotAnimationRepeatInterval: ) instead." )
}
private func setUpViewHierarchy ( ) {
set ( . height , to : Values . pathRowHeight )
let lineView = UIView ( )
lineView . set ( . width , to : Values . pathRowLineThickness )
lineView . backgroundColor = Colors . text
@ -128,13 +172,56 @@ private final class LineView : UIView {
case . top , . middle : lineView . pin ( . bottom , to : . bottom , of : self )
case . bottom : lineView . bottomAnchor . constraint ( equalTo : centerYAnchor ) . isActive = true
}
let dotView = UIView ( )
let dotSize = Values . pathRowDotSize
dotView . set ( . width , to : dotSize )
dotView . set ( . height , to : dotSize )
dotView . layer . cornerRadius = dotSize / 2
dotView . backgroundColor = Colors . text
dotViewWidthConstraint = dotView . set ( . width , to : dotSize )
dotViewHeightConstraint = dotView . set ( . height , to : dotSize )
addSubview ( dotView )
dotView . center ( in : self )
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 ( )
Timer . scheduledTimer ( withTimeInterval : 0.5 , repeats : false ) { [ weak self ] _ in
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
UIView . animate ( withDuration : 0.25 ) {
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
}
}
}