Prettify onion request UI
This commit is contained in:
parent
302d7ad09a
commit
112ff20c73
|
@ -623,6 +623,8 @@
|
|||
B90418E6183E9DD40038554A /* DateUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = B90418E5183E9DD40038554A /* DateUtil.m */; };
|
||||
B9EB5ABD1884C002007CBB57 /* MessageUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B9EB5ABC1884C002007CBB57 /* MessageUI.framework */; };
|
||||
BFF3FB9730634F37D25903F4 /* Pods_Signal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D17BB5C25D615AB49813100C /* Pods_Signal.framework */; };
|
||||
C31A6C5A247F214E001123EF /* UIView+Glow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31A6C59247F214E001123EF /* UIView+Glow.swift */; };
|
||||
C31A6C5C247F2CF3001123EF /* CGRect+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31A6C5B247F2CF3001123EF /* CGRect+Utilities.swift */; };
|
||||
C34C8F7423A7830B00D82669 /* SpaceMono-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = C34C8F7323A7830A00D82669 /* SpaceMono-Bold.ttf */; };
|
||||
C353F8F7244808E90011121A /* PNModeSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C353F8F6244808E90011121A /* PNModeSheet.swift */; };
|
||||
C353F8F9244809150011121A /* PNOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C353F8F8244809150011121A /* PNOptionView.swift */; };
|
||||
|
@ -1499,6 +1501,8 @@
|
|||
B97940251832BD2400BD66CB /* UIUtil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UIUtil.h; sourceTree = "<group>"; };
|
||||
B97940261832BD2400BD66CB /* UIUtil.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIUtil.m; sourceTree = "<group>"; };
|
||||
B9EB5ABC1884C002007CBB57 /* MessageUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MessageUI.framework; path = System/Library/Frameworks/MessageUI.framework; sourceTree = SDKROOT; };
|
||||
C31A6C59247F214E001123EF /* UIView+Glow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Glow.swift"; sourceTree = "<group>"; };
|
||||
C31A6C5B247F2CF3001123EF /* CGRect+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGRect+Utilities.swift"; sourceTree = "<group>"; };
|
||||
C34C8F7323A7830A00D82669 /* SpaceMono-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SpaceMono-Bold.ttf"; sourceTree = "<group>"; };
|
||||
C353F8F6244808E90011121A /* PNModeSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PNModeSheet.swift; sourceTree = "<group>"; };
|
||||
C353F8F8244809150011121A /* PNOptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PNOptionView.swift; sourceTree = "<group>"; };
|
||||
|
@ -2896,7 +2900,9 @@
|
|||
B886B4A82398BA1500211ABE /* QRCode.swift */,
|
||||
B8783E9D23EB948D00404FB8 /* UILabel+Interaction.swift */,
|
||||
B83F2B87240CB75A000A54AB /* UIImage+Scaling.swift */,
|
||||
C31A6C59247F214E001123EF /* UIView+Glow.swift */,
|
||||
C3548F0724456AB6009433A8 /* UIView+Wrapping.swift */,
|
||||
C31A6C5B247F2CF3001123EF /* CGRect+Utilities.swift */,
|
||||
);
|
||||
path = Utilities;
|
||||
sourceTree = "<group>";
|
||||
|
@ -4203,6 +4209,7 @@
|
|||
34D1F0841F8678AA0066283D /* ConversationInputToolbar.m in Sources */,
|
||||
457F671B20746193000EABCD /* QuotedReplyPreview.swift in Sources */,
|
||||
34A6C28021E503E700B5B12E /* OWSImagePickerController.swift in Sources */,
|
||||
C31A6C5C247F2CF3001123EF /* CGRect+Utilities.swift in Sources */,
|
||||
4C21D5D6223A9DC500EF8A77 /* UIAlerts+iOS9.m in Sources */,
|
||||
34DBF004206BD5A500025978 /* OWSBubbleView.m in Sources */,
|
||||
3496957021A301A100DCFE74 /* OWSBackupIO.m in Sources */,
|
||||
|
@ -4217,6 +4224,7 @@
|
|||
34BECE2B1F74C12700D7438D /* DebugUIStress.m in Sources */,
|
||||
340FC8B9204DAC8D007AEB0F /* UpdateGroupViewController.m in Sources */,
|
||||
B8BB82A5238F627000BA5194 /* HomeVC.swift in Sources */,
|
||||
C31A6C5A247F214E001123EF /* UIView+Glow.swift in Sources */,
|
||||
3448E1662215B313004B052E /* OnboardingCaptchaViewController.swift in Sources */,
|
||||
4574A5D61DD6704700C6B692 /* CallService.swift in Sources */,
|
||||
4521C3C01F59F3BA00B4C582 /* TextFieldHelper.swift in Sources */,
|
||||
|
|
|
@ -158,7 +158,9 @@ final class NewConversationButtonSet : UIView {
|
|||
self.layoutIfNeeded()
|
||||
button.frame = frame
|
||||
button.layer.cornerRadius = size / 2
|
||||
button.setGlow(to: size, with: Colors.newConversationButtonShadow, animated: true)
|
||||
let glowColor = Colors.newConversationButtonShadow
|
||||
let glowConfiguration = UIView.CircularGlowConfiguration(size: size, color: glowColor, isAnimated: true, radius: isLightMode ? 4 : 6)
|
||||
button.setCircularGlow(with: glowConfiguration)
|
||||
button.backgroundColor = Colors.accent
|
||||
}
|
||||
}
|
||||
|
@ -183,7 +185,8 @@ final class NewConversationButtonSet : UIView {
|
|||
button.frame = frame
|
||||
button.layer.cornerRadius = size / 2
|
||||
let glowColor = isLightMode ? UIColor.black.withAlphaComponent(0.4) : UIColor.black
|
||||
button.setGlow(to: size, with: glowColor, animated: true)
|
||||
let glowConfiguration = UIView.CircularGlowConfiguration(size: size, color: glowColor, isAnimated: true, radius: isLightMode ? 4 : 6)
|
||||
button.setCircularGlow(with: glowConfiguration)
|
||||
button.backgroundColor = Colors.newConversationButtonCollapsedBackground
|
||||
}
|
||||
}
|
||||
|
@ -208,8 +211,7 @@ private final class NewConversationButton : UIImageView {
|
|||
private let icon: UIImage
|
||||
var widthConstraint: NSLayoutConstraint!
|
||||
var heightConstraint: NSLayoutConstraint!
|
||||
|
||||
// Initialization
|
||||
|
||||
init(isMainButton: Bool, icon: UIImage) {
|
||||
self.isMainButton = isMainButton
|
||||
self.icon = icon
|
||||
|
@ -230,7 +232,8 @@ private final class NewConversationButton : UIImageView {
|
|||
let size = Values.newConversationButtonCollapsedSize
|
||||
layer.cornerRadius = size / 2
|
||||
let glowColor = isMainButton ? Colors.newConversationButtonShadow : (isLightMode ? UIColor.black.withAlphaComponent(0.4) : UIColor.black)
|
||||
setGlow(to: size, with: glowColor, animated: false)
|
||||
let glowConfiguration = UIView.CircularGlowConfiguration(size: size, color: glowColor, isAnimated: false, radius: isLightMode ? 4 : 6)
|
||||
setCircularGlow(with: glowConfiguration)
|
||||
layer.masksToBounds = false
|
||||
let iconColor = (isMainButton && isLightMode) ? UIColor.white : Colors.text
|
||||
image = icon.asTintedImage(color: iconColor)!
|
||||
|
@ -238,31 +241,6 @@ private final class NewConversationButton : UIImageView {
|
|||
widthConstraint = set(.width, to: size)
|
||||
heightConstraint = set(.height, to: size)
|
||||
}
|
||||
|
||||
// General
|
||||
func setGlow(to size: CGFloat, with color: UIColor, animated isAnimated: Bool) {
|
||||
let newPath = UIBezierPath(ovalIn: CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: size, height: size))).cgPath
|
||||
if isAnimated {
|
||||
let pathAnimation = CABasicAnimation(keyPath: "shadowPath")
|
||||
pathAnimation.fromValue = layer.shadowPath
|
||||
pathAnimation.toValue = newPath
|
||||
pathAnimation.duration = 0.25
|
||||
layer.add(pathAnimation, forKey: pathAnimation.keyPath)
|
||||
}
|
||||
layer.shadowPath = newPath
|
||||
let newColor = color.cgColor
|
||||
if isAnimated {
|
||||
let colorAnimation = CABasicAnimation(keyPath: "shadowColor")
|
||||
colorAnimation.fromValue = layer.shadowColor
|
||||
colorAnimation.toValue = newColor
|
||||
colorAnimation.duration = 0.25
|
||||
layer.add(colorAnimation, forKey: colorAnimation.keyPath)
|
||||
}
|
||||
layer.shadowColor = newColor
|
||||
layer.shadowOffset = CGSize(width: 0, height: 0.8)
|
||||
layer.shadowOpacity = isLightMode ? 0.4 : 1
|
||||
layer.shadowRadius = isLightMode ? 4 : 6
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Convenience
|
||||
|
@ -306,13 +284,3 @@ private extension CGPoint {
|
|||
return sqrt(pow(self.x - otherPoint.x, 2) + pow(self.y - otherPoint.y, 2))
|
||||
}
|
||||
}
|
||||
|
||||
private extension CGRect {
|
||||
|
||||
init(center: CGPoint, size: CGSize) {
|
||||
let originX = center.x - size.width / 2
|
||||
let originY = center.y - size.height / 2
|
||||
let origin = CGPoint(x: originX, y: originY)
|
||||
self.init(origin: origin, size: size)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,31 +15,8 @@ final class PathStatusView : UIView {
|
|||
backgroundColor = Colors.accent
|
||||
let size = Values.pathStatusViewSize
|
||||
layer.cornerRadius = size / 2
|
||||
setGlow(to: size, with: Colors.accent, animated: false)
|
||||
let glowConfiguration = UIView.CircularGlowConfiguration(size: size, color: Colors.accent, isAnimated: false, radius: isLightMode ? 6 : 8)
|
||||
setCircularGlow(with: glowConfiguration)
|
||||
layer.masksToBounds = false
|
||||
}
|
||||
|
||||
func setGlow(to size: CGFloat, with color: UIColor, animated isAnimated: Bool) {
|
||||
let newPath = UIBezierPath(ovalIn: CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: size, height: size))).cgPath
|
||||
if isAnimated {
|
||||
let pathAnimation = CABasicAnimation(keyPath: "shadowPath")
|
||||
pathAnimation.fromValue = layer.shadowPath
|
||||
pathAnimation.toValue = newPath
|
||||
pathAnimation.duration = 0.25
|
||||
layer.add(pathAnimation, forKey: pathAnimation.keyPath)
|
||||
}
|
||||
layer.shadowPath = newPath
|
||||
let newColor = color.cgColor
|
||||
if isAnimated {
|
||||
let colorAnimation = CABasicAnimation(keyPath: "shadowColor")
|
||||
colorAnimation.fromValue = layer.shadowColor
|
||||
colorAnimation.toValue = newColor
|
||||
colorAnimation.duration = 0.25
|
||||
layer.add(colorAnimation, forKey: colorAnimation.keyPath)
|
||||
}
|
||||
layer.shadowColor = newColor
|
||||
layer.shadowOffset = CGSize(width: 0, height: 0.8)
|
||||
layer.shadowOpacity = isLightMode ? 0.4 : 1
|
||||
layer.shadowRadius = isLightMode ? 6 : 8
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
|
||||
extension CGRect {
|
||||
|
||||
init(center: CGPoint, size: CGSize) {
|
||||
let originX = center.x - size.width / 2
|
||||
let originY = center.y - size.height / 2
|
||||
let origin = CGPoint(x: originX, y: originY)
|
||||
self.init(origin: origin, size: size)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
|
||||
extension UIView {
|
||||
|
||||
struct CircularGlowConfiguration {
|
||||
let size: CGFloat
|
||||
let color: UIColor
|
||||
let isAnimated: Bool
|
||||
let offset: CGSize
|
||||
let opacity: Float
|
||||
let radius: CGFloat
|
||||
|
||||
init(size: CGFloat, color: UIColor, isAnimated: Bool, offset: CGSize = CGSize(width: 0, height: 0.8), opacity: Float = isLightMode ? 0.4 : 1, radius: CGFloat) {
|
||||
self.size = size
|
||||
self.color = color
|
||||
self.isAnimated = isAnimated
|
||||
self.offset = offset
|
||||
self.opacity = opacity
|
||||
self.radius = radius
|
||||
}
|
||||
}
|
||||
|
||||
func setCircularGlow(with configuration: CircularGlowConfiguration) {
|
||||
let newSize = configuration.size
|
||||
let newPath = UIBezierPath(ovalIn: CGRect(origin: CGPoint.zero, size: CGSize(width: newSize, height: newSize))).cgPath
|
||||
if configuration.isAnimated {
|
||||
let pathAnimation = CABasicAnimation(keyPath: "shadowPath")
|
||||
pathAnimation.fromValue = layer.shadowPath
|
||||
pathAnimation.toValue = newPath
|
||||
pathAnimation.duration = 0.25
|
||||
layer.add(pathAnimation, forKey: pathAnimation.keyPath)
|
||||
}
|
||||
layer.shadowPath = newPath
|
||||
let newColor = configuration.color.cgColor
|
||||
if configuration.isAnimated {
|
||||
let colorAnimation = CABasicAnimation(keyPath: "shadowColor")
|
||||
colorAnimation.fromValue = layer.shadowColor
|
||||
colorAnimation.toValue = newColor
|
||||
colorAnimation.duration = 0.25
|
||||
layer.add(colorAnimation, forKey: colorAnimation.keyPath)
|
||||
}
|
||||
layer.shadowColor = newColor
|
||||
layer.shadowOffset = configuration.offset
|
||||
layer.shadowOpacity = configuration.opacity
|
||||
layer.shadowRadius = configuration.radius
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
// Set up path stack view
|
||||
guard var mainPath = OnionRequestAPI.paths.first else {
|
||||
guard let mainPath = OnionRequestAPI.paths.first else {
|
||||
return close() // TODO: Show path establishing UI
|
||||
}
|
||||
let rows: [UIStackView]
|
||||
switch mainPath.count {
|
||||
case 1: return // TODO: Do we want to handle this case?
|
||||
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
|
||||
// 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(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)
|
||||
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)
|
||||
}
|
||||
if let colonIndex = snodeDescription.lastIndex(of: ":") {
|
||||
snodeDescription = String(snodeDescription[snodeDescription.startIndex..<colonIndex])
|
||||
}
|
||||
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: Interaction
|
||||
@objc private func close() {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@objc private func rebuildPath() {
|
||||
// TODO: Implement
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Line View
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2834,4 +2834,8 @@
|
|||
"Authorizing Device Link" = "Authorizing Device Link";
|
||||
"Please wait while the device link is created. This can take up to a minute." = "Please wait while the device link is created. This can take up to a minute.";
|
||||
"Path" = "Path";
|
||||
"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." = "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.";
|
||||
"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." = "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.";
|
||||
"Service Node" = "Service Node";
|
||||
"You" = "You";
|
||||
"Destination" = "Destination";
|
||||
"Rebuild Path" = "Rebuild Path";
|
||||
|
|
|
@ -48,8 +48,9 @@ public final class Values : NSObject {
|
|||
@objc public static let progressBarThickness: CGFloat = 2
|
||||
@objc public static let pnOptionCornerRadius = CGFloat(8)
|
||||
@objc public static let pathStatusViewSize = CGFloat(8)
|
||||
@objc public static let pathRowLineThickness = CGFloat(1)
|
||||
@objc public static var pathRowLineThickness: CGFloat { return 1 / UIScreen.main.scale }
|
||||
@objc public static let pathRowDotSize = CGFloat(8)
|
||||
@objc public static let pathRowExpandedDotSize = CGFloat(16)
|
||||
@objc public static let pathRowHeight = CGFloat(72)
|
||||
|
||||
// MARK: - Distances
|
||||
|
|
Loading…
Reference in New Issue