From dbd71ab9a298b115189b6cdbc7cf9cfe181620f6 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Wed, 4 Dec 2019 13:44:00 +1100 Subject: [PATCH] Split new conversation screen into tabs --- Signal.xcodeproj/project.pbxproj | 12 +- Signal/src/Loki/HomeVC.swift | 4 +- .../Loki/Messaging/NewConversationVC.swift | 172 ---------- .../src/Loki/Messaging/NewPrivateChatVC.swift | 306 ++++++++++++++++++ .../Loki/Messaging/ScanQRCodeWrapperVC.swift | 26 +- Signal/src/Loki/TabBar.swift | 102 ++++++ .../Loki/Utilities/Style Guide/Values.swift | 2 + .../HomeView/HomeViewController.m | 6 +- .../translations/en.lproj/Localizable.strings | 4 +- 9 files changed, 440 insertions(+), 194 deletions(-) delete mode 100644 Signal/src/Loki/Messaging/NewConversationVC.swift create mode 100644 Signal/src/Loki/Messaging/NewPrivateChatVC.swift create mode 100644 Signal/src/Loki/TabBar.swift diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index c36666611..bd164c4ea 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -591,11 +591,12 @@ B8BB82AB238F669C00BA5194 /* ConversationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BB82AA238F669C00BA5194 /* ConversationCell.swift */; }; B8BB82AD238F734800BA5194 /* ProfilePictureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BB82AC238F734800BA5194 /* ProfilePictureView.swift */; }; B8BB82B12390C37000BA5194 /* SearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BB82B02390C37000BA5194 /* SearchBar.swift */; }; - B8BB82B323947E6B00BA5194 /* NewConversationVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BB82B223947E6B00BA5194 /* NewConversationVC.swift */; }; B8BB82B523947F2D00BA5194 /* TextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BB82B423947F2D00BA5194 /* TextField.swift */; }; B8BB82B92394911B00BA5194 /* Separator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BB82B82394911B00BA5194 /* Separator.swift */; }; B8BB82BE2394D4CE00BA5194 /* Fonts.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BB82BD2394D4CE00BA5194 /* Fonts.swift */; }; B8CCF6352396005F0091D419 /* SpaceMono-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = B8CCF6342396005F0091D419 /* SpaceMono-Regular.ttf */; }; + B8CCF63723961D6D0091D419 /* NewPrivateChatVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8CCF63623961D6D0091D419 /* NewPrivateChatVC.swift */; }; + B8CCF639239721E20091D419 /* TabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8CCF638239721E20091D419 /* TabBar.swift */; }; 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 */; }; @@ -1415,11 +1416,12 @@ B8BB82AA238F669C00BA5194 /* ConversationCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationCell.swift; sourceTree = ""; }; B8BB82AC238F734800BA5194 /* ProfilePictureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePictureView.swift; sourceTree = ""; }; B8BB82B02390C37000BA5194 /* SearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBar.swift; sourceTree = ""; }; - B8BB82B223947E6B00BA5194 /* NewConversationVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewConversationVC.swift; sourceTree = ""; }; B8BB82B423947F2D00BA5194 /* TextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextField.swift; sourceTree = ""; }; B8BB82B82394911B00BA5194 /* Separator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Separator.swift; sourceTree = ""; }; B8BB82BD2394D4CE00BA5194 /* Fonts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fonts.swift; sourceTree = ""; }; B8CCF6342396005F0091D419 /* SpaceMono-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SpaceMono-Regular.ttf"; sourceTree = ""; }; + B8CCF63623961D6D0091D419 /* NewPrivateChatVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewPrivateChatVC.swift; sourceTree = ""; }; + B8CCF638239721E20091D419 /* TabBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBar.swift; sourceTree = ""; }; B90418E4183E9DD40038554A /* DateUtil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DateUtil.h; sourceTree = ""; }; B90418E5183E9DD40038554A /* DateUtil.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DateUtil.m; sourceTree = ""; }; B97940251832BD2400BD66CB /* UIUtil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UIUtil.h; sourceTree = ""; }; @@ -2688,6 +2690,7 @@ B8BB82B423947F2D00BA5194 /* TextField.swift */, B8BB82B82394911B00BA5194 /* Separator.swift */, B8BB82AC238F734800BA5194 /* ProfilePictureView.swift */, + B8CCF638239721E20091D419 /* TabBar.swift */, ); path = Loki; sourceTree = ""; @@ -2766,7 +2769,7 @@ B825849F2315024B001B41CB /* LokiRSSFeedPoller.swift */, B8B26C8E234D629C004ED98C /* MentionCandidateSelectionView.swift */, B8B26C90234D8CBD004ED98C /* MentionCandidateSelectionViewDelegate.swift */, - B8BB82B223947E6B00BA5194 /* NewConversationVC.swift */, + B8CCF63623961D6D0091D419 /* NewPrivateChatVC.swift */, 24BD2608234DA2050008EB0A /* JoinPublicChatVC.swift */, B893063E2383961A005EAA8E /* ScanQRCodeWrapperVC.swift */, ); @@ -3775,9 +3778,9 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + B8CCF63723961D6D0091D419 /* NewPrivateChatVC.swift in Sources */, 4CC0B59C20EC5F2E00CF6EE0 /* ConversationConfigurationSyncOperation.swift in Sources */, 3461293E1FD1D72B00532771 /* ExperienceUpgradeFinder.swift in Sources */, - B8BB82B323947E6B00BA5194 /* NewConversationVC.swift in Sources */, 34C4E2582118957600BEA353 /* WebRTCProto.swift in Sources */, 34D1F0BD1F8D108C0066283D /* AttachmentUploadView.m in Sources */, 452EC6DF205E9E30000E787C /* MediaGalleryViewController.swift in Sources */, @@ -3909,6 +3912,7 @@ 3448E16022134C89004B052E /* OnboardingSplashViewController.swift in Sources */, 34B6A903218B3F63007C4606 /* TypingIndicatorView.swift in Sources */, B8BB82AD238F734800BA5194 /* ProfilePictureView.swift in Sources */, + B8CCF639239721E20091D419 /* TabBar.swift in Sources */, B8162F0322891AD600D46544 /* FriendRequestView.swift in Sources */, 458E38371D668EBF0094BD24 /* OWSDeviceProvisioningURLParser.m in Sources */, 34B6A905218B4C91007C4606 /* TypingIndicatorInteraction.swift in Sources */, diff --git a/Signal/src/Loki/HomeVC.swift b/Signal/src/Loki/HomeVC.swift index 5028e167e..3223f30e5 100644 --- a/Signal/src/Loki/HomeVC.swift +++ b/Signal/src/Loki/HomeVC.swift @@ -307,8 +307,8 @@ final class HomeVC : UIViewController, UITableViewDataSource, UITableViewDelegat } @objc func createPrivateChat() { - let newConversationVC = NewConversationVC() - let navigationController = OWSNavigationController(rootViewController: newConversationVC) + let newPrivateChatVC = NewPrivateChatVC() + let navigationController = OWSNavigationController(rootViewController: newPrivateChatVC) present(navigationController, animated: true, completion: nil) } diff --git a/Signal/src/Loki/Messaging/NewConversationVC.swift b/Signal/src/Loki/Messaging/NewConversationVC.swift deleted file mode 100644 index f19ec2c7e..000000000 --- a/Signal/src/Loki/Messaging/NewConversationVC.swift +++ /dev/null @@ -1,172 +0,0 @@ - -@objc(LKNewConversationVC) -final class NewConversationVC : OWSViewController, OWSQRScannerDelegate { - - private lazy var userHexEncodedPublicKey: String = { - let userDefaults = UserDefaults.standard - if let masterHexEncodedPublicKey = userDefaults.string(forKey: "masterDeviceHexEncodedPublicKey") { - return masterHexEncodedPublicKey - } else { - return OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey - } - }() - - // MARK: Components - private lazy var publicKeyTextField = TextField(placeholder: NSLocalizedString("Enter public key of recipient", comment: "")) - - private lazy var userPublicKeyLabel: UILabel = { - let result = UILabel() - result.textColor = Colors.text - result.font = Fonts.spaceMono(ofSize: Values.mediumFontSize) - result.numberOfLines = 0 - result.textAlignment = .center - result.lineBreakMode = .byCharWrapping - return result - }() - - private lazy var copyButton: Button = { - let result = Button(style: .unimportant) - result.setTitle(NSLocalizedString("Copy", comment: ""), for: UIControl.State.normal) - result.addTarget(self, action: #selector(copyPublicKey), for: UIControl.Event.touchUpInside) - return result - }() - - // MARK: Lifecycle - override func viewDidLoad() { - // Set gradient background - view.backgroundColor = .clear - let gradient = Gradients.defaultLokiBackground - view.setGradient(gradient) - // Set navigation bar background color - if let navigationBar = navigationController?.navigationBar { - navigationBar.setBackgroundImage(UIImage(), for: UIBarMetrics.default) - navigationBar.shadowImage = UIImage() - navigationBar.isTranslucent = false - navigationBar.barTintColor = Colors.navigationBarBackground - } - // Set up navigation bar buttons - let closeButton = UIBarButtonItem(image: #imageLiteral(resourceName: "X"), style: .plain, target: self, action: #selector(close)) - closeButton.tintColor = Colors.text - navigationItem.leftBarButtonItem = closeButton - // Customize title - let titleLabel = UILabel() - titleLabel.text = NSLocalizedString("New Conversation", comment: "") - titleLabel.textColor = Colors.text - titleLabel.font = UIFont.boldSystemFont(ofSize: Values.veryLargeFontSize) - navigationItem.titleView = titleLabel - // Set up explanation label - let explanationLabel = UILabel() - explanationLabel.textColor = Colors.text.withAlphaComponent(Values.unimportantElementOpacity) - explanationLabel.font = .systemFont(ofSize: Values.smallFontSize) - explanationLabel.text = NSLocalizedString("Users can share their public key by going into their account settings and tapping \"Share Public Key\", or by sharing their QR code.", comment: "") - explanationLabel.numberOfLines = 0 - explanationLabel.textAlignment = .center - explanationLabel.lineBreakMode = .byWordWrapping - // Set up separator - let separator = Separator(title: NSLocalizedString("Your Public Key", comment: "")) - // Set up user public key label - userPublicKeyLabel.text = userHexEncodedPublicKey - // Set up share button - let shareButton = Button(style: .unimportant) - shareButton.setTitle(NSLocalizedString("Share", comment: ""), for: UIControl.State.normal) - shareButton.addTarget(self, action: #selector(sharePublicKey), for: UIControl.Event.touchUpInside) - // Set up button container - let buttonContainer = UIStackView(arrangedSubviews: [ copyButton, shareButton ]) - buttonContainer.axis = .horizontal - buttonContainer.spacing = Values.mediumSpacing - buttonContainer.distribution = .fillEqually - // Next button - let nextButton = Button(style: .prominent) - nextButton.setTitle(NSLocalizedString("Next", comment: ""), for: UIControl.State.normal) - nextButton.addTarget(self, action: #selector(handleNextButtonTapped), for: UIControl.Event.touchUpInside) - let nextButtonContainer = UIView() - nextButtonContainer.addSubview(nextButton) - nextButton.pin(.leading, to: .leading, of: nextButtonContainer, withInset: 80) - nextButton.pin(.top, to: .top, of: nextButtonContainer) - nextButtonContainer.pin(.trailing, to: .trailing, of: nextButton, withInset: 80) - nextButtonContainer.pin(.bottom, to: .bottom, of: nextButton) - // Stack view - let stackView = UIStackView(arrangedSubviews: [ publicKeyTextField, UIView.spacer(withHeight: Values.smallSpacing), explanationLabel, UIView.spacer(withHeight: Values.veryLargeSpacing), separator, UIView.spacer(withHeight: Values.veryLargeSpacing), userPublicKeyLabel, UIView.spacer(withHeight: Values.veryLargeSpacing), buttonContainer, UIView.spacer(withHeight: Values.veryLargeSpacing), nextButtonContainer, UIView.vStretchingSpacer() ]) - stackView.axis = .vertical - stackView.alignment = .fill - stackView.layoutMargins = UIEdgeInsets(top: Values.mediumSpacing, left: Values.largeSpacing, bottom: Values.mediumSpacing, right: Values.largeSpacing) - stackView.isLayoutMarginsRelativeArrangement = true - view.addSubview(stackView) - stackView.pin(to: view) - // Dismiss keyboard on tap - let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) - view.addGestureRecognizer(tapGestureRecognizer) - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - publicKeyTextField.becomeFirstResponder() - } - - // MARK: General - @objc private func dismissKeyboard() { - publicKeyTextField.resignFirstResponder() - } - - @objc private func enableCopyButton() { - copyButton.isUserInteractionEnabled = true - UIView.transition(with: copyButton, duration: 0.25, options: .transitionCrossDissolve, animations: { - self.copyButton.setTitle(NSLocalizedString("Copy", comment: ""), for: UIControl.State.normal) - }, completion: nil) - } - - // MARK: Interaction - @objc private func close() { - dismiss(animated: true, completion: nil) - } - - @objc private func copyPublicKey() { - UIPasteboard.general.string = userHexEncodedPublicKey - copyButton.isUserInteractionEnabled = false - UIView.transition(with: copyButton, duration: 0.25, options: .transitionCrossDissolve, animations: { - self.copyButton.setTitle(NSLocalizedString("Copied", comment: ""), for: UIControl.State.normal) - }, completion: nil) - Timer.scheduledTimer(timeInterval: 4, target: self, selector: #selector(enableCopyButton), userInfo: nil, repeats: false) - } - - @objc private func sharePublicKey() { - let shareVC = UIActivityViewController(activityItems: [ userHexEncodedPublicKey ], applicationActivities: nil) - navigationController?.present(shareVC, animated: true, completion: nil) - } - -// @objc private func scanQRCode() { -// ows_ask(forCameraPermissions: { [weak self] hasCameraAccess in -// if hasCameraAccess { -// let message = NSLocalizedString("Scan the QR code of the person you'd like to securely message. They can find their QR code by going into Loki Messenger's in-app settings and clicking \"Show QR Code\".", comment: "") -// let scanQRCodeWrapperVC = ScanQRCodeWrapperVC(message: message) -// scanQRCodeWrapperVC.delegate = self -// self?.navigationController!.pushViewController(scanQRCodeWrapperVC, animated: true) -// } else { -// // Do nothing -// } -// }) -// } -// -// func controller(_ controller: OWSQRCodeScanningViewController, didDetectQRCodeWith string: String) { -// Analytics.shared.track("QR Code Scanned") -// let hexEncodedPublicKey = string -// startNewConversationIfPossible(with: hexEncodedPublicKey) -// } - - @objc private func handleNextButtonTapped() { - let hexEncodedPublicKey = publicKeyTextField.text?.trimmingCharacters(in: .whitespaces) ?? "" - startNewConversationIfPossible(with: hexEncodedPublicKey) - } - - private func startNewConversationIfPossible(with hexEncodedPublicKey: String) { - if !ECKeyPair.isValidHexEncodedPublicKey(candidate: hexEncodedPublicKey) { - let alert = UIAlertController(title: NSLocalizedString("Invalid Public Key", comment: ""), message: NSLocalizedString("Please check the public key you entered and try again.", comment: ""), preferredStyle: .alert) - alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil)) - presentAlert(alert) - } else { - let thread = TSContactThread.getOrCreateThread(contactId: hexEncodedPublicKey) - presentingViewController!.dismiss(animated: true, completion: nil) - SignalApp.shared().presentConversation(for: thread, action: .compose, animated: false) - } - } -} diff --git a/Signal/src/Loki/Messaging/NewPrivateChatVC.swift b/Signal/src/Loki/Messaging/NewPrivateChatVC.swift new file mode 100644 index 000000000..9d397b1ff --- /dev/null +++ b/Signal/src/Loki/Messaging/NewPrivateChatVC.swift @@ -0,0 +1,306 @@ + +final class NewPrivateChatVC : UIViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate, OWSQRScannerDelegate { + private let pageVC = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil) + private var pages: [UIViewController] = [] + private var targetVCIndex: Int? + + // MARK: Components + private lazy var tabBar: TabBar = { + let tabs = [ + TabBar.Tab(title: "Enter Public Key") { [weak self] in + guard let self = self else { return } + self.pageVC.setViewControllers([ self.pages[0] ], direction: .forward, animated: false, completion: nil) + }, + TabBar.Tab(title: "Scan QR Code") { [weak self] in + guard let self = self else { return } + self.pageVC.setViewControllers([ self.pages[1] ], direction: .forward, animated: false, completion: nil) + } + ] + return TabBar(tabs: tabs) + }() + + private lazy var enterPublicKeyVC: EnterPublicKeyVC = { + let result = EnterPublicKeyVC() + result.newPrivateChatVC = self + return result + }() + + private lazy var scanQRCodePlaceholderVC: ScanQRCodePlaceholderVC = { + let result = ScanQRCodePlaceholderVC() + result.newPrivateChatVC = self + return result + }() + + private lazy var scanQRCodeWrapperVC: ScanQRCodeWrapperVC = { + let message = NSLocalizedString("Scan the QR code of the person you'd like to securely message. They can find their QR code by going into Loki Messenger's in-app settings and tapping \"Show QR Code\".", comment: "") + let result = ScanQRCodeWrapperVC(message: message) + result.delegate = self + return result + }() + + // MARK: Lifecycle + override func viewDidLoad() { + // Set gradient background + view.backgroundColor = .clear + let gradient = Gradients.defaultLokiBackground + view.setGradient(gradient) + // Set navigation bar background color + let navigationBar = navigationController!.navigationBar + navigationBar.setBackgroundImage(UIImage(), for: UIBarMetrics.default) + navigationBar.shadowImage = UIImage() + navigationBar.isTranslucent = false + navigationBar.barTintColor = Colors.navigationBarBackground + // Set up navigation bar buttons + let closeButton = UIBarButtonItem(image: #imageLiteral(resourceName: "X"), style: .plain, target: self, action: #selector(close)) + closeButton.tintColor = Colors.text + navigationItem.leftBarButtonItem = closeButton + // Customize title + let titleLabel = UILabel() + titleLabel.text = NSLocalizedString("New Conversation", comment: "") + titleLabel.textColor = Colors.text + titleLabel.font = UIFont.boldSystemFont(ofSize: Values.veryLargeFontSize) + navigationItem.titleView = titleLabel + // Set up page VC + let hasCameraAccess = (AVCaptureDevice.authorizationStatus(for: .video) == .authorized) + pages = [ enterPublicKeyVC, (hasCameraAccess ? scanQRCodeWrapperVC : scanQRCodePlaceholderVC) ] + pageVC.dataSource = self + pageVC.delegate = self + pageVC.setViewControllers([ enterPublicKeyVC ], direction: .forward, animated: false, completion: nil) + // Set up tab bar + view.addSubview(tabBar) + tabBar.pin(.leading, to: .leading, of: view) + tabBar.pin(.top, to: .top, of: view, withInset: navigationBar.height()) + view.pin(.trailing, to: .trailing, of: tabBar) + // Set up page VC constraints + let pageVCView = pageVC.view! + view.addSubview(pageVCView) + pageVCView.pin(.leading, to: .leading, of: view) + pageVCView.pin(.top, to: .bottom, of: tabBar) + view.pin(.trailing, to: .trailing, of: pageVCView) + view.pin(.bottom, to: .bottom, of: pageVCView) + let screen = UIScreen.main.bounds + pageVCView.set(.width, to: screen.width) + let height = navigationController!.view.bounds.height - navigationBar.height() - Values.tabBarHeight + pageVCView.set(.height, to: height) + enterPublicKeyVC.constrainHeight(to: height) + scanQRCodePlaceholderVC.constrainHeight(to: height) + } + + // MARK: General + func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { + guard let index = pages.firstIndex(of: viewController), index != 0 else { return nil } + return pages[index - 1] + } + + func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { + guard let index = pages.firstIndex(of: viewController), index != (pages.count - 1) else { return nil } + return pages[index + 1] + } + + fileprivate func handleCameraAccessGranted() { + pages[1] = scanQRCodeWrapperVC + pageVC.setViewControllers([ scanQRCodeWrapperVC ], direction: .forward, animated: false, completion: nil) + } + + // MARK: Updating + func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) { + guard let targetVC = pendingViewControllers.first, let index = pages.firstIndex(of: targetVC) else { return } + targetVCIndex = index + } + + func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating isFinished: Bool, previousViewControllers: [UIViewController], transitionCompleted isCompleted: Bool) { + guard isCompleted, let index = targetVCIndex else { return } + tabBar.selectTab(at: index) + } + + // MARK: Interaction + @objc private func close() { + dismiss(animated: true, completion: nil) + } + + func controller(_ controller: OWSQRCodeScanningViewController, didDetectQRCodeWith string: String) { + let hexEncodedPublicKey = string + startNewPrivateChatIfPossible(with: hexEncodedPublicKey) + } + + fileprivate func startNewPrivateChatIfPossible(with hexEncodedPublicKey: String) { + if !ECKeyPair.isValidHexEncodedPublicKey(candidate: hexEncodedPublicKey) { + let alert = UIAlertController(title: NSLocalizedString("Invalid Public Key", comment: ""), message: NSLocalizedString("Please check the public key you entered and try again.", comment: ""), preferredStyle: .alert) + alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil)) + presentAlert(alert) + } else { + let thread = TSContactThread.getOrCreateThread(contactId: hexEncodedPublicKey) + presentingViewController?.dismiss(animated: true, completion: nil) + SignalApp.shared().presentConversation(for: thread, action: .compose, animated: false) + } + } +} + +private final class EnterPublicKeyVC : UIViewController { + var newPrivateChatVC: NewPrivateChatVC? + + private lazy var userHexEncodedPublicKey: String = { + let userDefaults = UserDefaults.standard + if let masterHexEncodedPublicKey = userDefaults.string(forKey: "masterDeviceHexEncodedPublicKey") { + return masterHexEncodedPublicKey + } else { + return OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey + } + }() + + // MARK: Components + private lazy var publicKeyTextField = TextField(placeholder: NSLocalizedString("Enter public key of recipient", comment: "")) + + private lazy var copyButton: Button = { + let result = Button(style: .unimportant) + result.setTitle(NSLocalizedString("Copy", comment: ""), for: UIControl.State.normal) + result.addTarget(self, action: #selector(copyPublicKey), for: UIControl.Event.touchUpInside) + return result + }() + + // MARK: Lifecycle + override func viewDidLoad() { + // Remove background color + view.backgroundColor = .clear + // Set up explanation label + let explanationLabel = UILabel() + explanationLabel.textColor = Colors.text.withAlphaComponent(Values.unimportantElementOpacity) + explanationLabel.font = .systemFont(ofSize: Values.smallFontSize) + explanationLabel.text = NSLocalizedString("Users can share their public key by going into their account settings and tapping \"Share Public Key\", or by sharing their QR code.", comment: "") + explanationLabel.numberOfLines = 0 + explanationLabel.textAlignment = .center + explanationLabel.lineBreakMode = .byWordWrapping + // Set up separator + let separator = Separator(title: NSLocalizedString("Your Public Key", comment: "")) + // Set up user public key label + let userPublicKeyLabel = UILabel() + userPublicKeyLabel.textColor = Colors.text + userPublicKeyLabel.font = Fonts.spaceMono(ofSize: Values.mediumFontSize) + userPublicKeyLabel.numberOfLines = 0 + userPublicKeyLabel.textAlignment = .center + userPublicKeyLabel.lineBreakMode = .byCharWrapping + userPublicKeyLabel.text = userHexEncodedPublicKey + // Set up share button + let shareButton = Button(style: .unimportant) + shareButton.setTitle(NSLocalizedString("Share", comment: ""), for: UIControl.State.normal) + shareButton.addTarget(self, action: #selector(sharePublicKey), for: UIControl.Event.touchUpInside) + // Set up button container + let buttonContainer = UIStackView(arrangedSubviews: [ copyButton, shareButton ]) + buttonContainer.axis = .horizontal + buttonContainer.spacing = Values.mediumSpacing + buttonContainer.distribution = .fillEqually + // Next button + let nextButton = Button(style: .prominent) + nextButton.setTitle(NSLocalizedString("Next", comment: ""), for: UIControl.State.normal) + nextButton.addTarget(self, action: #selector(startNewPrivateChatIfPossible), for: UIControl.Event.touchUpInside) + let nextButtonContainer = UIView() + nextButtonContainer.addSubview(nextButton) + nextButton.pin(.leading, to: .leading, of: nextButtonContainer, withInset: 80) + nextButton.pin(.top, to: .top, of: nextButtonContainer) + nextButtonContainer.pin(.trailing, to: .trailing, of: nextButton, withInset: 80) + nextButtonContainer.pin(.bottom, to: .bottom, of: nextButton) + // Stack view + let stackView = UIStackView(arrangedSubviews: [ publicKeyTextField, UIView.spacer(withHeight: Values.smallSpacing), explanationLabel, UIView.spacer(withHeight: Values.veryLargeSpacing), separator, UIView.spacer(withHeight: Values.veryLargeSpacing), userPublicKeyLabel, UIView.spacer(withHeight: Values.veryLargeSpacing), buttonContainer, UIView.spacer(withHeight: Values.veryLargeSpacing), nextButtonContainer, UIView.vStretchingSpacer() ]) + stackView.axis = .vertical + stackView.alignment = .fill + stackView.layoutMargins = UIEdgeInsets(top: Values.mediumSpacing, left: Values.largeSpacing, bottom: Values.mediumSpacing, right: Values.largeSpacing) + stackView.isLayoutMarginsRelativeArrangement = true + view.addSubview(stackView) + stackView.pin(to: view) + // Set up width constraint + view.set(.width, to: UIScreen.main.bounds.width) + // Dismiss keyboard on tap + let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) + view.addGestureRecognizer(tapGestureRecognizer) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + publicKeyTextField.becomeFirstResponder() + } + + // MARK: General + func constrainHeight(to height: CGFloat) { + view.set(.height, to: height) + } + + @objc private func dismissKeyboard() { + publicKeyTextField.resignFirstResponder() + } + + @objc private func enableCopyButton() { + copyButton.isUserInteractionEnabled = true + UIView.transition(with: copyButton, duration: 0.25, options: .transitionCrossDissolve, animations: { + self.copyButton.setTitle(NSLocalizedString("Copy", comment: ""), for: UIControl.State.normal) + }, completion: nil) + } + + // MARK: Interaction + @objc private func copyPublicKey() { + UIPasteboard.general.string = userHexEncodedPublicKey + copyButton.isUserInteractionEnabled = false + UIView.transition(with: copyButton, duration: 0.25, options: .transitionCrossDissolve, animations: { + self.copyButton.setTitle(NSLocalizedString("Copied", comment: ""), for: UIControl.State.normal) + }, completion: nil) + Timer.scheduledTimer(timeInterval: 4, target: self, selector: #selector(enableCopyButton), userInfo: nil, repeats: false) + } + + @objc private func sharePublicKey() { + let shareVC = UIActivityViewController(activityItems: [ userHexEncodedPublicKey ], applicationActivities: nil) + newPrivateChatVC!.navigationController!.present(shareVC, animated: true, completion: nil) + } + + @objc private func startNewPrivateChatIfPossible() { + let hexEncodedPublicKey = publicKeyTextField.text?.trimmingCharacters(in: .whitespaces) ?? "" + newPrivateChatVC!.startNewPrivateChatIfPossible(with: hexEncodedPublicKey) + } +} + +private final class ScanQRCodePlaceholderVC : UIViewController { + var newPrivateChatVC: NewPrivateChatVC? + + override func viewDidLoad() { + // Remove background color + view.backgroundColor = .clear + // Set up explanation label + let explanationLabel = UILabel() + explanationLabel.textColor = Colors.text + explanationLabel.font = .systemFont(ofSize: Values.smallFontSize) + explanationLabel.text = NSLocalizedString("Loki Messenger needs camera access to scan QR codes", comment: "") + explanationLabel.numberOfLines = 0 + explanationLabel.textAlignment = .center + explanationLabel.lineBreakMode = .byWordWrapping + // Set up call to action button + let callToActionButton = UIButton() + callToActionButton.titleLabel!.font = .boldSystemFont(ofSize: Values.mediumFontSize) + callToActionButton.setTitleColor(Colors.accent, for: UIControl.State.normal) + callToActionButton.setTitle(NSLocalizedString("Enable Camera Access", comment: ""), for: UIControl.State.normal) + callToActionButton.addTarget(self, action: #selector(requestCameraAccess), for: UIControl.Event.touchUpInside) + // Set up stack view + let stackView = UIStackView(arrangedSubviews: [ explanationLabel, callToActionButton ]) + stackView.axis = .vertical + stackView.spacing = Values.mediumSpacing + stackView.alignment = .center + // Set up constraints + view.set(.width, to: UIScreen.main.bounds.width) + view.addSubview(stackView) + stackView.pin(.leading, to: .leading, of: view, withInset: Values.massiveSpacing) + view.pin(.trailing, to: .trailing, of: stackView, withInset: Values.massiveSpacing) + let verticalCenteringConstraint = stackView.center(.vertical, in: view) + verticalCenteringConstraint.constant = -16 // Makes things appear centered visually + } + + func constrainHeight(to height: CGFloat) { + view.set(.height, to: height) + } + + @objc private func requestCameraAccess() { + ows_ask(forCameraPermissions: { [weak self] hasCameraAccess in + if hasCameraAccess { + self?.newPrivateChatVC!.handleCameraAccessGranted() + } else { + // Do nothing + } + }) + } +} diff --git a/Signal/src/Loki/Messaging/ScanQRCodeWrapperVC.swift b/Signal/src/Loki/Messaging/ScanQRCodeWrapperVC.swift index dede72043..849defece 100644 --- a/Signal/src/Loki/Messaging/ScanQRCodeWrapperVC.swift +++ b/Signal/src/Loki/Messaging/ScanQRCodeWrapperVC.swift @@ -15,21 +15,23 @@ final class ScanQRCodeWrapperVC : UIViewController { } required init?(coder: NSCoder) { - preconditionFailure("Use init(title:) instead.") + preconditionFailure("Use init(message:) instead.") } override init(nibName: String?, bundle: Bundle?) { - preconditionFailure("Use init(title:) instead.") + preconditionFailure("Use init(message:) instead.") } override func viewDidLoad() { - // Navigation bar + // Set up navigation bar if needed if isPresentedModally { - navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .stop, target: self, action: #selector(objc_dismiss)) + navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .stop, target: self, action: #selector(close)) } - // Background color - view.backgroundColor = Theme.backgroundColor - // Scan QR code VC + // Set gradient background + view.backgroundColor = .clear + let gradient = Gradients.defaultLokiBackground + view.setGradient(gradient) + // Set up scan QR code VC scanQRCodeVC.scanDelegate = delegate let scanQRCodeVCView = scanQRCodeVC.view! view.addSubview(scanQRCodeVCView) @@ -37,18 +39,18 @@ final class ScanQRCodeWrapperVC : UIViewController { scanQRCodeVCView.pin(.trailing, to: .trailing, of: view) scanQRCodeVCView.autoPin(toTopLayoutGuideOf: self, withInset: 0) scanQRCodeVCView.autoPinToSquareAspectRatio() - // Bottom view + // Set up bottom view let bottomView = UIView() view.addSubview(bottomView) bottomView.pin(.top, to: .bottom, of: scanQRCodeVCView) bottomView.pin(.leading, to: .leading, of: view) bottomView.pin(.trailing, to: .trailing, of: view) bottomView.pin(.bottom, to: .bottom, of: view) - // Explanation label + // Set up explanation label let explanationLabel = UILabel() explanationLabel.text = message - explanationLabel.textColor = Theme.primaryColor - explanationLabel.font = .ows_dynamicTypeSubheadlineClamped + explanationLabel.textColor = Colors.text + explanationLabel.font = .systemFont(ofSize: Values.smallFontSize) explanationLabel.numberOfLines = 0 explanationLabel.lineBreakMode = .byWordWrapping explanationLabel.textAlignment = .center @@ -68,7 +70,7 @@ final class ScanQRCodeWrapperVC : UIViewController { } // MARK: Interaction - @objc private func objc_dismiss() { + @objc private func close() { presentingViewController?.dismiss(animated: true, completion: nil) } } diff --git a/Signal/src/Loki/TabBar.swift b/Signal/src/Loki/TabBar.swift new file mode 100644 index 000000000..ce4aacf48 --- /dev/null +++ b/Signal/src/Loki/TabBar.swift @@ -0,0 +1,102 @@ + +final class TabBar : UIView { + private let tabs: [Tab] + private var accentLineViewHorizontalCenteringConstraint: NSLayoutConstraint! + private var accentLineViewWidthConstraint: NSLayoutConstraint! + + // MARK: Components + private lazy var tabLabels: [UILabel] = tabs.map { tab in + let result = UILabel() + result.font = .boldSystemFont(ofSize: Values.mediumFontSize) + result.textColor = Colors.text.withAlphaComponent(Values.unimportantElementOpacity) + result.textAlignment = .center + result.text = tab.title + result.set(.height, to: Values.tabBarHeight - Values.separatorThickness - Values.accentLineThickness) + return result + } + + private lazy var accentLineView: UIView = { + let result = UIView() + result.backgroundColor = Colors.accent + return result + }() + + // MARK: Types + struct Tab { + let title: String + let onTap: () -> Void + } + + // MARK: Lifecycle + init(tabs: [Tab]) { + self.tabs = tabs + super.init(frame: CGRect.zero) + setUpViewHierarchy() + } + + override init(frame: CGRect) { + preconditionFailure("Use init(tabs:) instead.") + } + + required init?(coder: NSCoder) { + preconditionFailure("Use init(tabs:) instead.") + } + + private func setUpViewHierarchy() { + set(.height, to: Values.tabBarHeight) + tabLabels.forEach { tabLabel in + let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTabLabelTapped(_:))) + tabLabel.addGestureRecognizer(tapGestureRecognizer) + } + let tabLabelStackView = UIStackView(arrangedSubviews: tabLabels) + tabLabelStackView.axis = .horizontal + tabLabelStackView.distribution = .fillEqually + tabLabelStackView.spacing = Values.mediumSpacing + let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTabLabelTapped(_:))) + tabLabelStackView.addGestureRecognizer(tapGestureRecognizer) + tabLabelStackView.set(.height, to: Values.tabBarHeight - Values.separatorThickness - Values.accentLineThickness) + addSubview(tabLabelStackView) + let separator = UIView() + separator.backgroundColor = Colors.separator + separator.set(.height, to: Values.separatorThickness) + addSubview(separator) + accentLineView.set(.height, to: Values.accentLineThickness) + addSubview(accentLineView) + tabLabelStackView.pin(.leading, to: .leading, of: self) + tabLabelStackView.pin(.top, to: .top, of: self) + pin(.trailing, to: .trailing, of: tabLabelStackView) + separator.pin(.leading, to: .leading, of: self) + separator.pin(.top, to: .bottom, of: tabLabelStackView) + pin(.trailing, to: .trailing, of: separator) + accentLineView.translatesAutoresizingMaskIntoConstraints = false + selectTab(at: 0, withAnimatedTransition: false) + accentLineView.pin(.top, to: .bottom, of: separator) + pin(.bottom, to: .bottom, of: accentLineView) + } + + // MARK: Updating + func selectTab(at index: Int, withAnimatedTransition isAnimated: Bool = true) { + let tabLabel = tabLabels[index] + accentLineViewHorizontalCenteringConstraint?.isActive = false + accentLineViewHorizontalCenteringConstraint = accentLineView.centerXAnchor.constraint(equalTo: tabLabel.centerXAnchor) + accentLineViewHorizontalCenteringConstraint.isActive = true + accentLineViewWidthConstraint?.isActive = false + accentLineViewWidthConstraint = accentLineView.widthAnchor.constraint(equalTo: tabLabel.widthAnchor) + accentLineViewWidthConstraint.isActive = true + var tabLabelsCopy = tabLabels + tabLabelsCopy.remove(at: index) + UIView.animate(withDuration: isAnimated ? 0.25 : 0) { + tabLabel.textColor = Colors.text + tabLabelsCopy.forEach { $0.textColor = Colors.text.withAlphaComponent(Values.unimportantElementOpacity) } + self.layoutIfNeeded() + } + } + + // MARK: Interaction + @objc private func handleTabLabelTapped(_ sender: UITapGestureRecognizer) { + guard let tabLabel = tabLabels.first(where: { $0.bounds.contains(sender.location(in: $0)) }), let index = tabLabels.firstIndex(of: tabLabel) else { return } + selectTab(at: index) + let tab = tabs[index] + tab.onTap() + } +} diff --git a/Signal/src/Loki/Utilities/Style Guide/Values.swift b/Signal/src/Loki/Utilities/Style Guide/Values.swift index 805d39c94..fa060f23a 100644 --- a/Signal/src/Loki/Utilities/Style Guide/Values.swift +++ b/Signal/src/Loki/Utilities/Style Guide/Values.swift @@ -30,11 +30,13 @@ final class Values : NSObject { @objc static let textFieldCornerRadius = CGFloat(8) @objc static let separatorLabelHeight = CGFloat(24) @objc static var separatorThickness: CGFloat { return 1 / UIScreen.main.scale } + @objc static let tabBarHeight = CGFloat(48) // MARK: - Distances @objc static let smallSpacing = CGFloat(8) @objc static let mediumSpacing = CGFloat(16) @objc static let largeSpacing = CGFloat(24) @objc static let veryLargeSpacing = CGFloat(35) + @objc static let massiveSpacing = CGFloat(64) @objc static let newConversationButtonBottomOffset = CGFloat(52) } diff --git a/Signal/src/ViewControllers/HomeView/HomeViewController.m b/Signal/src/ViewControllers/HomeView/HomeViewController.m index d5bfc9079..441959082 100644 --- a/Signal/src/ViewControllers/HomeView/HomeViewController.m +++ b/Signal/src/ViewControllers/HomeView/HomeViewController.m @@ -841,9 +841,9 @@ typedef NS_ENUM(NSInteger, HomeViewControllerSection) { - (void)showNewConversationVC { - LKNewConversationVC *newConversationVC = [LKNewConversationVC new]; - OWSNavigationController *navigationController = [[OWSNavigationController alloc] initWithRootViewController:newConversationVC]; - [self.navigationController presentViewController:navigationController animated:YES completion:nil]; +// LKNewConversationVC *newConversationVC = [LKNewConversationVC new]; +// OWSNavigationController *navigationController = [[OWSNavigationController alloc] initWithRootViewController:newConversationVC]; +// [self.navigationController presentViewController:navigationController animated:YES completion:nil]; /** OWSAssertIsOnMainThread(); diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index b21fc3992..ee8673722 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -2616,7 +2616,6 @@ "Scan a QR Code Instead" = "Scan a QR Code Instead"; "Loki Messenger needs camera access to scan QR codes." = "Loki Messenger needs camera access to scan QR codes."; "You can enable camera access in your device settings." = "You can enable camera access in your device settings."; -"Scan the QR code of the person you'd like to securely message. They can find their QR code by going into Loki Messenger's in-app settings and clicking \"Show QR Code\"." = "Scan the QR code of the person you'd like to securely message. They can find their QR code by going into Loki Messenger's in-app settings and clicking \"Show QR Code\"."; "Scan QR Code" = "Scan QR Code"; "Loki" = "Loki"; "Can't Start Conversation" = "Can't Start Conversation"; @@ -2687,3 +2686,6 @@ "Copied" = "Copied"; "Share" = "Share"; "Next" = "Next"; +"Loki Messenger needs camera access to scan QR codes" = "Loki Messenger needs camera access to scan QR codes"; +"Enable Camera Access" = "Enable Camera Access"; +"Scan the QR code of the person you'd like to securely message. They can find their QR code by going into Loki Messenger's in-app settings and tapping \"Show QR Code\"." = "Scan the QR code of the person you'd like to securely message. They can find their QR code by going into Loki Messenger's in-app settings and tapping \"Show QR Code\".";