From 112ff20c73a52bfac0b2760baa8c430f4c389c0f Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Thu, 28 May 2020 09:52:13 +1000 Subject: [PATCH] Prettify onion request UI --- Signal.xcodeproj/project.pbxproj | 8 + .../Components/NewConversationButtonSet.swift | 48 +---- .../src/Loki/Components/PathStatusView.swift | 27 +-- .../src/Loki/Utilities/CGRect+Utilities.swift | 10 + Signal/src/Loki/Utilities/UIView+Glow.swift | 46 +++++ Signal/src/Loki/View Controllers/PathVC.swift | 179 +++++++++++++----- .../translations/en.lproj/Localizable.strings | 6 +- .../Loki/Redesign/Style Guide/Values.swift | 3 +- 8 files changed, 214 insertions(+), 113 deletions(-) create mode 100644 Signal/src/Loki/Utilities/CGRect+Utilities.swift create mode 100644 Signal/src/Loki/Utilities/UIView+Glow.swift diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 4eaf6a13a..4acdde737 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -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 = ""; }; B97940261832BD2400BD66CB /* UIUtil.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIUtil.m; sourceTree = ""; }; 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 = ""; }; + C31A6C5B247F2CF3001123EF /* CGRect+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGRect+Utilities.swift"; sourceTree = ""; }; C34C8F7323A7830A00D82669 /* SpaceMono-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SpaceMono-Bold.ttf"; sourceTree = ""; }; C353F8F6244808E90011121A /* PNModeSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PNModeSheet.swift; sourceTree = ""; }; C353F8F8244809150011121A /* PNOptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PNOptionView.swift; sourceTree = ""; }; @@ -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 = ""; @@ -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 */, diff --git a/Signal/src/Loki/Components/NewConversationButtonSet.swift b/Signal/src/Loki/Components/NewConversationButtonSet.swift index 32591f69e..c53fc2f1b 100644 --- a/Signal/src/Loki/Components/NewConversationButtonSet.swift +++ b/Signal/src/Loki/Components/NewConversationButtonSet.swift @@ -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) - } -} diff --git a/Signal/src/Loki/Components/PathStatusView.swift b/Signal/src/Loki/Components/PathStatusView.swift index 9499864b1..4282c70e0 100644 --- a/Signal/src/Loki/Components/PathStatusView.swift +++ b/Signal/src/Loki/Components/PathStatusView.swift @@ -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 - } } diff --git a/Signal/src/Loki/Utilities/CGRect+Utilities.swift b/Signal/src/Loki/Utilities/CGRect+Utilities.swift new file mode 100644 index 000000000..68f8338da --- /dev/null +++ b/Signal/src/Loki/Utilities/CGRect+Utilities.swift @@ -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) + } +} diff --git a/Signal/src/Loki/Utilities/UIView+Glow.swift b/Signal/src/Loki/Utilities/UIView+Glow.swift new file mode 100644 index 000000000..393026497 --- /dev/null +++ b/Signal/src/Loki/Utilities/UIView+Glow.swift @@ -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 + } +} diff --git a/Signal/src/Loki/View Controllers/PathVC.swift b/Signal/src/Loki/View Controllers/PathVC.swift index 16c3fc924..708a655b4 100644 --- a/Signal/src/Loki/View Controllers/PathVC.swift +++ b/Signal/src/Loki/View Controllers/PathVC.swift @@ -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.. UIStackView { + var snodeIP = snode.description + if snodeIP.hasPrefix("https://") { snodeIP.removeFirst(8) } + if let colonIndex = snodeIP.lastIndex(of: ":") { + snodeIP = String(snodeIP[snodeIP.startIndex..