From 6f31336a7db897417843776b08f8cd0173141958 Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Tue, 14 Apr 2020 15:07:37 +1000 Subject: [PATCH] Implement preliminary push notification UI --- Signal.xcodeproj/project.pbxproj | 8 + .../src/Loki/Utilities/UIView+Wrapping.swift | 12 ++ .../src/Loki/View Controllers/PNModeVC.swift | 184 ++++++++++++++++++ .../Loki/Redesign/Style Guide/Colors.swift | 2 + .../Loki/Redesign/Style Guide/Values.swift | 1 + 5 files changed, 207 insertions(+) create mode 100644 Signal/src/Loki/Utilities/UIView+Wrapping.swift create mode 100644 Signal/src/Loki/View Controllers/PNModeVC.swift diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 7e91edaba..645856853 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -624,6 +624,8 @@ B9EB5ABD1884C002007CBB57 /* MessageUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B9EB5ABC1884C002007CBB57 /* MessageUI.framework */; }; BFF3FB9730634F37D25903F4 /* Pods_Signal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D17BB5C25D615AB49813100C /* Pods_Signal.framework */; }; C34C8F7423A7830B00D82669 /* SpaceMono-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = C34C8F7323A7830A00D82669 /* SpaceMono-Bold.ttf */; }; + C3548F0624456447009433A8 /* PNModeVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3548F0524456447009433A8 /* PNModeVC.swift */; }; + C3548F0824456AB6009433A8 /* UIView+Wrapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3548F0724456AB6009433A8 /* UIView+Wrapping.swift */; }; C354E75A23FE2A7600CE22E3 /* BaseVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C354E75923FE2A7600CE22E3 /* BaseVC.swift */; }; C36B8707243C50C60049991D /* SignalMessaging.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 453518921FC63DBF00210559 /* SignalMessaging.framework */; }; C3B781FF2411C18600C859D8 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = C3B781FE2411C18600C859D8 /* GoogleService-Info.plist */; }; @@ -1499,6 +1501,8 @@ 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; }; C34C8F7323A7830A00D82669 /* SpaceMono-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SpaceMono-Bold.ttf"; sourceTree = ""; }; + C3548F0524456447009433A8 /* PNModeVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PNModeVC.swift; sourceTree = ""; }; + C3548F0724456AB6009433A8 /* UIView+Wrapping.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Wrapping.swift"; sourceTree = ""; }; C354E75923FE2A7600CE22E3 /* BaseVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseVC.swift; sourceTree = ""; }; C3B781FE2411C18600C859D8 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Signal/GoogleService-Info.plist"; sourceTree = SOURCE_ROOT; }; C3DFFAC523E96F0D0058DAF8 /* Sheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sheet.swift; sourceTree = ""; }; @@ -2892,6 +2896,7 @@ B886B4A82398BA1500211ABE /* QRCode.swift */, B8783E9D23EB948D00404FB8 /* UILabel+Interaction.swift */, B83F2B87240CB75A000A54AB /* UIImage+Scaling.swift */, + C3548F0724456AB6009433A8 /* UIView+Wrapping.swift */, ); path = Utilities; sourceTree = ""; @@ -2917,6 +2922,7 @@ B8CCF63623961D6D0091D419 /* NewPrivateChatVC.swift */, B894D0742339EDCF00B4D94D /* NukeDataModal.swift */, C3DFFAC723E970080058DAF8 /* OpenGroupSuggestionSheet.swift */, + C3548F0524456447009433A8 /* PNModeVC.swift */, B886B4A62398B23E00211ABE /* QRCodeVC.swift */, B82B408B239A068800A248E7 /* RegisterVC.swift */, B82B408F239DD75000A248E7 /* RestoreVC.swift */, @@ -4043,6 +4049,7 @@ 34B6A907218B5241007C4606 /* TypingIndicatorCell.swift in Sources */, 4CFD151D22415AA400F2450F /* CallVideoHintView.swift in Sources */, 34D1F0AB1F867BFC0066283D /* OWSContactOffersCell.m in Sources */, + C3548F0824456AB6009433A8 /* UIView+Wrapping.swift in Sources */, B82B408A2399EC0600A248E7 /* FakeChatView.swift in Sources */, B8BB82B92394911B00BA5194 /* Separator.swift in Sources */, 343A65981FC4CFE7000477A1 /* ConversationScrollButton.m in Sources */, @@ -4096,6 +4103,7 @@ 349ED990221B0194008045B0 /* Onboarding2FAViewController.swift in Sources */, 45D231771DC7E8F10034FA89 /* SessionResetJob.swift in Sources */, 340FC8A9204DAC8D007AEB0F /* NotificationSettingsOptionsViewController.m in Sources */, + C3548F0624456447009433A8 /* PNModeVC.swift in Sources */, B80A579F23DFF1F300876683 /* NewClosedGroupVC.swift in Sources */, 452037D11EE84975004E4CDF /* DebugUISessionState.m in Sources */, D221A09A169C9E5E00537ABF /* main.m in Sources */, diff --git a/Signal/src/Loki/Utilities/UIView+Wrapping.swift b/Signal/src/Loki/Utilities/UIView+Wrapping.swift new file mode 100644 index 000000000..422a004b3 --- /dev/null +++ b/Signal/src/Loki/Utilities/UIView+Wrapping.swift @@ -0,0 +1,12 @@ + +extension UIView { + + convenience init(wrapping view: UIView, withInsets insets: UIEdgeInsets) { + self.init() + addSubview(view) + view.pin(.leading, to: .leading, of: self, withInset: insets.left) + view.pin(.top, to: .top, of: self, withInset: insets.top) + self.pin(.trailing, to: .trailing, of: view, withInset: insets.right) + self.pin(.bottom, to: .bottom, of: view, withInset: insets.bottom) + } +} diff --git a/Signal/src/Loki/View Controllers/PNModeVC.swift b/Signal/src/Loki/View Controllers/PNModeVC.swift new file mode 100644 index 000000000..8d0d1922c --- /dev/null +++ b/Signal/src/Loki/View Controllers/PNModeVC.swift @@ -0,0 +1,184 @@ + +final class PNModeVC : BaseVC, OptionViewDelegate { + + private var optionViews: [OptionView] { + [ apnsOptionView, backgroundPollingOptionView, noPNsOptionView ] + } + + private var selectedOptionView: OptionView? { + return optionViews.first { $0.isSelected } + } + + // MARK: Components + private lazy var apnsOptionView = OptionView(title: "Apple Push Notification Service", explanation: "The app will use the Apple Push Notification Service. You'll be notified of new messages immediately. This mode entails a slight privacy sacrifice as Apple will know your IP. The contents of your messages will still be fully encrypted, your data will still be stored in a decentralized manner and your messages will still be onion routed.", delegate: self) + private lazy var backgroundPollingOptionView = OptionView(title: "Background Polling", explanation: "The app will occassionally check for new messages when it's in the background. This provides full privacy but notifications may be significantly delayed.", delegate: self) + private lazy var noPNsOptionView = OptionView(title: "No Push Notifications", explanation: "You will not be notified of new messages when the app is closed. This provides full privacy.", delegate: self) + + // MARK: Lifecycle + override func viewDidLoad() { + super.viewDidLoad() + // Set gradient background + view.backgroundColor = .clear + let gradient = Gradients.defaultLokiBackground + view.setGradient(gradient) + // Set up navigation bar + let navigationBar = navigationController!.navigationBar + navigationBar.setBackgroundImage(UIImage(), for: UIBarMetrics.default) + navigationBar.shadowImage = UIImage() + navigationBar.isTranslucent = false + navigationBar.barTintColor = Colors.navigationBarBackground + // Set up logo image view + let logoImageView = UIImageView() + logoImageView.image = #imageLiteral(resourceName: "SessionGreen32") + logoImageView.contentMode = .scaleAspectFit + logoImageView.set(.width, to: 32) + logoImageView.set(.height, to: 32) + navigationItem.titleView = logoImageView + // Set up title label + let titleLabel = UILabel() + titleLabel.textColor = Colors.text + titleLabel.font = .boldSystemFont(ofSize: isSmallScreen ? Values.largeFontSize : Values.veryLargeFontSize) + titleLabel.text = "Push Notifications" + titleLabel.numberOfLines = 0 + titleLabel.lineBreakMode = .byWordWrapping + // Set up explanation label + let explanationLabel = UILabel() + explanationLabel.textColor = Colors.text + explanationLabel.font = .systemFont(ofSize: Values.smallFontSize) + explanationLabel.text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." + explanationLabel.numberOfLines = 0 + explanationLabel.lineBreakMode = .byWordWrapping + // Set up spacers + let topSpacer = UIView.vStretchingSpacer() + let bottomSpacer = UIView.vStretchingSpacer() + let registerButtonBottomOffsetSpacer = UIView() + registerButtonBottomOffsetSpacer.set(.height, to: Values.onboardingButtonBottomOffset) + // Set up register button + let registerButton = Button(style: .prominentFilled, size: .large) + registerButton.setTitle(NSLocalizedString("Continue", comment: ""), for: UIControl.State.normal) + registerButton.titleLabel!.font = .boldSystemFont(ofSize: Values.mediumFontSize) + registerButton.addTarget(self, action: #selector(register), for: UIControl.Event.touchUpInside) + // Set up register button container + let registerButtonContainer = UIView(wrapping: registerButton, withInsets: UIEdgeInsets(top: 0, leading: Values.massiveSpacing, bottom: 0, trailing: Values.massiveSpacing)) + // Set up options stack view + let optionsStackView = UIStackView(arrangedSubviews: optionViews) + optionsStackView.axis = .vertical + optionsStackView.spacing = Values.smallSpacing + optionsStackView.alignment = .fill + // Set up top stack view + let topStackView = UIStackView(arrangedSubviews: [ titleLabel, explanationLabel, optionsStackView ]) + topStackView.axis = .vertical + topStackView.spacing = isSmallScreen ? Values.smallSpacing : Values.veryLargeSpacing + topStackView.alignment = .fill + // Set up top stack view container + let topStackViewContainer = UIView(wrapping: topStackView, withInsets: UIEdgeInsets(top: 0, leading: Values.veryLargeSpacing, bottom: 0, trailing: Values.veryLargeSpacing)) + // Set up main stack view + let mainStackView = UIStackView(arrangedSubviews: [ topSpacer, topStackViewContainer, bottomSpacer, registerButtonContainer, registerButtonBottomOffsetSpacer ]) + mainStackView.axis = .vertical + mainStackView.alignment = .fill + view.addSubview(mainStackView) + mainStackView.pin(to: view) + topSpacer.heightAnchor.constraint(equalTo: bottomSpacer.heightAnchor, multiplier: 1).isActive = true + } + + // MARK: Interaction + fileprivate func optionViewDidActivate(_ optionView: OptionView) { + optionViews.filter { $0 != optionView }.forEach { $0.isSelected = false } + } + + @objc private func register() { + // TODO: Implement + } +} + +// MARK: Option View +private extension PNModeVC { + + final class OptionView : UIView { + private let title: String + private let explanation: String + private let delegate: OptionViewDelegate + var isSelected = false { didSet { handleIsSelectedChanged() } } + + init(title: String, explanation: String, delegate: OptionViewDelegate) { + self.title = title + self.explanation = explanation + self.delegate = delegate + super.init(frame: CGRect.zero) + setUpViewHierarchy() + } + + override init(frame: CGRect) { + preconditionFailure("Use init(string:explanation:) instead.") + } + + required init?(coder: NSCoder) { + preconditionFailure("Use init(string:explanation:) instead.") + } + + private func setUpViewHierarchy() { + backgroundColor = Colors.pnOptionBackground + // Round corners + layer.cornerRadius = Values.pnOptionCornerRadius + // Set up border + layer.borderWidth = Values.borderThickness + layer.borderColor = Colors.pnOptionBorder.cgColor + // Set up shadow + layer.shadowColor = UIColor.black.cgColor + layer.shadowOffset = CGSize(width: 0, height: 0.8) + layer.shadowOpacity = isLightMode ? 0.4 : 1 + layer.shadowRadius = isLightMode ? 4 : 6 + // Set up title label + let titleLabel = UILabel() + titleLabel.textColor = Colors.text + titleLabel.font = .boldSystemFont(ofSize: Values.mediumFontSize) + titleLabel.text = title + titleLabel.numberOfLines = 0 + titleLabel.lineBreakMode = .byWordWrapping + // Set up explanation label + let explanationLabel = UILabel() + explanationLabel.textColor = Colors.text + explanationLabel.font = .systemFont(ofSize: Values.verySmallFontSize) + explanationLabel.text = explanation + explanationLabel.numberOfLines = 0 + explanationLabel.lineBreakMode = .byWordWrapping + // Set up stack view + let stackView = UIStackView(arrangedSubviews: [ titleLabel, explanationLabel ]) + stackView.axis = .vertical + stackView.alignment = .fill + addSubview(stackView) + stackView.pin(.leading, to: .leading, of: self, withInset: 12) + stackView.pin(.top, to: .top, of: self, withInset: 12) + self.pin(.trailing, to: .trailing, of: stackView, withInset: 12) + self.pin(.bottom, to: .bottom, of: stackView, withInset: 12) + // Set up tap gesture recognizer + let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap)) + addGestureRecognizer(tapGestureRecognizer) + } + + @objc private func handleTap() { + isSelected = !isSelected + } + + private func handleIsSelectedChanged() { + let animationDuration: TimeInterval = 0.25 + UIView.animate(withDuration: animationDuration) { + self.backgroundColor = self.isSelected ? Colors.accent : Colors.buttonBackground + } + let newShadowColor = isSelected ? Colors.newConversationButtonShadow.cgColor : UIColor.black.cgColor + let shadowAnimation = CABasicAnimation(keyPath: "shadowColor") + shadowAnimation.fromValue = layer.shadowColor + shadowAnimation.toValue = newShadowColor + shadowAnimation.duration = animationDuration + layer.add(shadowAnimation, forKey: shadowAnimation.keyPath) + layer.shadowColor = newShadowColor + if isSelected { delegate.optionViewDidActivate(self) } + } + } +} + +// MARK: Option View Delegate +private protocol OptionViewDelegate { + + func optionViewDidActivate(_ optionView: PNModeVC.OptionView) +} diff --git a/SignalMessaging/Loki/Redesign/Style Guide/Colors.swift b/SignalMessaging/Loki/Redesign/Style Guide/Colors.swift index daff51918..a783b18fa 100644 --- a/SignalMessaging/Loki/Redesign/Style Guide/Colors.swift +++ b/SignalMessaging/Loki/Redesign/Style Guide/Colors.swift @@ -36,4 +36,6 @@ public final class Colors : NSObject { @objc public static var receivedMessageBackground = isLightMode ? UIColor(hex: 0xF5F5F5) : UIColor(hex: 0x222325) @objc public static var sentMessageBackground = isLightMode ? UIColor(hex: 0x00E97B) : UIColor(hex: 0x3F4146) @objc public static var newConversationButtonCollapsedBackground = isLightMode ? UIColor(hex: 0xF5F5F5) : UIColor(hex: 0x1F1F1F) + @objc public static var pnOptionBackground = isLightMode ? UIColor(hex: 0xFCFCFC) : UIColor(hex: 0x1B1B1B) + @objc public static var pnOptionBorder = UIColor(hex: 0x212121) } diff --git a/SignalMessaging/Loki/Redesign/Style Guide/Values.swift b/SignalMessaging/Loki/Redesign/Style Guide/Values.swift index 3f8798361..35f616454 100644 --- a/SignalMessaging/Loki/Redesign/Style Guide/Values.swift +++ b/SignalMessaging/Loki/Redesign/Style Guide/Values.swift @@ -46,6 +46,7 @@ public final class Values : NSObject { @objc public static let composeViewTextFieldBorderThickness = 1 / UIScreen.main.scale @objc public static let messageBubbleCornerRadius: CGFloat = 10 @objc public static let progressBarThickness: CGFloat = 2 + @objc public static let pnOptionCornerRadius = CGFloat(8) // MARK: - Distances @objc public static let verySmallSpacing = CGFloat(4)