mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
Updated the HomeVC, SettingsVC and GlobalSearch UI to use theming Removed the "fade view" gradients from the various screens Added a simple log to the PagedDatabaseObserver to make debugging easier Updated the FullConversationCell to also show the "read" state for messages Updated the read receipt icons to use SFSymbols directly Updated the PlaceholderIcon to use the PrimaryColour's as it's colour options
360 lines
17 KiB
Swift
360 lines
17 KiB
Swift
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
|
|
|
import UIKit
|
|
import PromiseKit
|
|
import SessionUIKit
|
|
import SessionUtilitiesKit
|
|
import SessionSnodeKit
|
|
|
|
final class LinkDeviceVC: BaseVC, UIPageViewControllerDataSource, UIPageViewControllerDelegate, OWSQRScannerDelegate {
|
|
private let pageVC = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
|
|
private var pages: [UIViewController] = []
|
|
private var targetVCIndex: Int?
|
|
private var tabBarTopConstraint: NSLayoutConstraint!
|
|
private var activityIndicatorModal: ModalActivityIndicatorViewController?
|
|
|
|
// MARK: - Components
|
|
|
|
private lazy var tabBar: TabBar = {
|
|
let tabs = [
|
|
TabBar.Tab(title: "vc_link_device_recovery_phrase_tab_title".localized()) { [weak self] in
|
|
guard let self = self else { return }
|
|
self.pageVC.setViewControllers([ self.pages[0] ], direction: .forward, animated: false, completion: nil)
|
|
},
|
|
TabBar.Tab(title: "vc_link_device_scan_qr_code_tab_title".localized()) { [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 recoveryPhraseVC: RecoveryPhraseVC = {
|
|
let result = RecoveryPhraseVC()
|
|
result.linkDeviceVC = self
|
|
return result
|
|
}()
|
|
|
|
private lazy var scanQRCodePlaceholderVC: ScanQRCodePlaceholderVC = {
|
|
let result = ScanQRCodePlaceholderVC()
|
|
result.linkDeviceVC = self
|
|
return result
|
|
}()
|
|
|
|
private lazy var scanQRCodeWrapperVC: ScanQRCodeWrapperVC = {
|
|
let message = NSLocalizedString("vc_link_device_scan_qr_code_explanation", comment: "")
|
|
let result = ScanQRCodeWrapperVC(message: message)
|
|
result.delegate = self
|
|
return result
|
|
}()
|
|
|
|
// MARK: Lifecycle
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
setNavBarTitle("vc_link_device_title".localized())
|
|
let navigationBar = navigationController!.navigationBar
|
|
// Page VC
|
|
let hasCameraAccess = (AVCaptureDevice.authorizationStatus(for: .video) == .authorized)
|
|
pages = [ recoveryPhraseVC, (hasCameraAccess ? scanQRCodeWrapperVC : scanQRCodePlaceholderVC) ]
|
|
pageVC.dataSource = self
|
|
pageVC.delegate = self
|
|
pageVC.setViewControllers([ recoveryPhraseVC ], direction: .forward, animated: false, completion: nil)
|
|
// Tab bar
|
|
view.addSubview(tabBar)
|
|
tabBar.pin(.leading, to: .leading, of: view)
|
|
tabBarTopConstraint = tabBar.autoPinEdge(toSuperviewSafeArea: .top)
|
|
view.pin(.trailing, to: .trailing, of: tabBar)
|
|
// 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 statusBarHeight = UIApplication.shared.statusBarFrame.height
|
|
let height = navigationController!.view.bounds.height - navigationBar.height() - TabBar.snHeight - statusBarHeight
|
|
pageVCView.set(.height, to: height)
|
|
recoveryPhraseVC.constrainHeight(to: height)
|
|
scanQRCodePlaceholderVC.constrainHeight(to: height)
|
|
}
|
|
|
|
override func viewDidAppear(_ animated: Bool) {
|
|
super.viewDidAppear(animated)
|
|
tabBarTopConstraint.constant = navigationController!.navigationBar.height()
|
|
}
|
|
|
|
deinit {
|
|
NotificationCenter.default.removeObserver(self)
|
|
}
|
|
|
|
// 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 seed = Data(hex: string)
|
|
continueWithSeed(seed)
|
|
}
|
|
|
|
func continueWithSeed(_ seed: Data) {
|
|
if (seed.count != 16) {
|
|
let alert = UIAlertController(
|
|
title: "invalid_recovery_phrase".localized(),
|
|
message: "INVALID_RECOVERY_PHRASE_MESSAGE".localized(),
|
|
preferredStyle: .alert
|
|
)
|
|
alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: { _ in
|
|
self.scanQRCodeWrapperVC.startCapture()
|
|
}))
|
|
presentAlert(alert)
|
|
return
|
|
}
|
|
let (ed25519KeyPair, x25519KeyPair) = try! Identity.generate(from: seed)
|
|
Onboarding.Flow.link.preregister(with: seed, ed25519KeyPair: ed25519KeyPair, x25519KeyPair: x25519KeyPair)
|
|
|
|
Identity.didRegister()
|
|
|
|
// Now that we have registered get the Snode pool
|
|
GetSnodePoolJob.run()
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(handleInitialConfigurationMessageReceived), name: .initialConfigurationMessageReceived, object: nil)
|
|
ModalActivityIndicatorViewController.present(fromViewController: navigationController!) { [weak self] modal in
|
|
self?.activityIndicatorModal = modal
|
|
}
|
|
}
|
|
|
|
@objc private func handleInitialConfigurationMessageReceived(_ notification: Notification) {
|
|
DispatchQueue.main.async {
|
|
self.navigationController!.dismiss(animated: true) {
|
|
let pnModeVC = PNModeVC()
|
|
self.navigationController!.setViewControllers([ pnModeVC ], animated: true)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private final class RecoveryPhraseVC : UIViewController {
|
|
weak var linkDeviceVC: LinkDeviceVC!
|
|
private var spacer1HeightConstraint: NSLayoutConstraint!
|
|
private var spacer2HeightConstraint: NSLayoutConstraint!
|
|
private var restoreButtonBottomOffsetConstraint: NSLayoutConstraint!
|
|
private var bottomConstraint: NSLayoutConstraint!
|
|
|
|
private lazy var mnemonicTextView: TextView = {
|
|
let result = TextView(placeholder: NSLocalizedString("vc_restore_seed_text_field_hint", comment: ""))
|
|
result.layer.borderColor = Colors.text.cgColor
|
|
result.accessibilityLabel = "Recovery phrase text view"
|
|
return result
|
|
}()
|
|
|
|
// MARK: Lifecycle
|
|
override func viewDidLoad() {
|
|
view.backgroundColor = .clear
|
|
|
|
// Title label
|
|
let titleLabel = UILabel()
|
|
titleLabel.font = .boldSystemFont(ofSize: isIPhone5OrSmaller ? Values.largeFontSize : Values.veryLargeFontSize)
|
|
titleLabel.text = "vc_enter_recovery_phrase_title".localized()
|
|
titleLabel.textColor = Colors.text
|
|
titleLabel.lineBreakMode = .byWordWrapping
|
|
titleLabel.numberOfLines = 0
|
|
|
|
// Explanation label
|
|
let explanationLabel = UILabel()
|
|
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
|
|
explanationLabel.text = "vc_enter_recovery_phrase_explanation".localized()
|
|
explanationLabel.textColor = Colors.text
|
|
explanationLabel.lineBreakMode = .byWordWrapping
|
|
explanationLabel.numberOfLines = 0
|
|
|
|
// Spacers
|
|
let topSpacer = UIView.vStretchingSpacer()
|
|
let spacer1 = UIView()
|
|
spacer1HeightConstraint = spacer1.set(.height, to: isIPhone5OrSmaller ? Values.smallSpacing : Values.veryLargeSpacing)
|
|
let spacer2 = UIView()
|
|
spacer2HeightConstraint = spacer2.set(.height, to: isIPhone5OrSmaller ? Values.smallSpacing : Values.veryLargeSpacing)
|
|
let bottomSpacer = UIView.vStretchingSpacer()
|
|
let restoreButtonBottomOffsetSpacer = UIView()
|
|
restoreButtonBottomOffsetConstraint = restoreButtonBottomOffsetSpacer.set(.height, to: Values.onboardingButtonBottomOffset)
|
|
|
|
// Continue button
|
|
let continueButton = OutlineButton(style: .filled, size: .large)
|
|
continueButton.setTitle("continue_2".localized(), for: UIControl.State.normal)
|
|
continueButton.addTarget(self, action: #selector(handleContinueButtonTapped), for: UIControl.Event.touchUpInside)
|
|
|
|
// Continue button container
|
|
let continueButtonContainer = UIView(wrapping: continueButton, withInsets: UIEdgeInsets(top: 0, leading: Values.massiveSpacing, bottom: 0, trailing: Values.massiveSpacing), shouldAdaptForIPadWithWidth: Values.iPadButtonWidth)
|
|
|
|
// Top stack view
|
|
let topStackView = UIStackView(arrangedSubviews: [ titleLabel, spacer1, explanationLabel, spacer2, mnemonicTextView ])
|
|
topStackView.axis = .vertical
|
|
topStackView.alignment = .fill
|
|
|
|
// Top stack view container
|
|
let topStackViewContainer = UIView()
|
|
topStackViewContainer.addSubview(topStackView)
|
|
topStackView.pin(.leading, to: .leading, of: topStackViewContainer, withInset: Values.veryLargeSpacing)
|
|
topStackView.pin(.top, to: .top, of: topStackViewContainer)
|
|
topStackViewContainer.pin(.trailing, to: .trailing, of: topStackView, withInset: Values.veryLargeSpacing)
|
|
topStackViewContainer.pin(.bottom, to: .bottom, of: topStackView)
|
|
|
|
// Main stack view
|
|
let mainStackView = UIStackView(arrangedSubviews: [ topSpacer, topStackViewContainer, bottomSpacer, continueButtonContainer, restoreButtonBottomOffsetSpacer ])
|
|
mainStackView.axis = .vertical
|
|
mainStackView.alignment = .fill
|
|
view.addSubview(mainStackView)
|
|
mainStackView.pin(.leading, to: .leading, of: view)
|
|
mainStackView.pin(.top, to: .top, of: view)
|
|
mainStackView.pin(.trailing, to: .trailing, of: view)
|
|
bottomConstraint = mainStackView.pin(.bottom, to: .bottom, of: view)
|
|
topSpacer.heightAnchor.constraint(equalTo: bottomSpacer.heightAnchor, multiplier: 1).isActive = true
|
|
|
|
// Dismiss keyboard on tap
|
|
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
|
|
view.addGestureRecognizer(tapGestureRecognizer)
|
|
|
|
// Listen to keyboard notifications
|
|
let notificationCenter = NotificationCenter.default
|
|
notificationCenter.addObserver(self, selector: #selector(handleKeyboardWillChangeFrameNotification(_:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
|
|
notificationCenter.addObserver(self, selector: #selector(handleKeyboardWillHideNotification(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
|
|
|
|
// Set up width constraint
|
|
view.set(.width, to: UIScreen.main.bounds.width)
|
|
}
|
|
|
|
deinit {
|
|
NotificationCenter.default.removeObserver(self)
|
|
}
|
|
|
|
// MARK: - General
|
|
|
|
func constrainHeight(to height: CGFloat) {
|
|
view.set(.height, to: height)
|
|
}
|
|
|
|
@objc private func dismissKeyboard() {
|
|
mnemonicTextView.resignFirstResponder()
|
|
}
|
|
|
|
// MARK: Updating
|
|
@objc private func handleKeyboardWillChangeFrameNotification(_ notification: Notification) {
|
|
guard let newHeight = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.size.height else { return }
|
|
bottomConstraint.constant = -newHeight // Negative due to how the constraint is set up
|
|
restoreButtonBottomOffsetConstraint.constant = isIPhone6OrSmaller ? Values.smallSpacing : Values.largeSpacing
|
|
spacer1HeightConstraint.constant = isIPhone6OrSmaller ? Values.smallSpacing : Values.mediumSpacing
|
|
spacer2HeightConstraint.constant = isIPhone6OrSmaller ? Values.smallSpacing : Values.mediumSpacing
|
|
UIView.animate(withDuration: 0.25) {
|
|
self.view.layoutIfNeeded()
|
|
}
|
|
}
|
|
|
|
@objc private func handleKeyboardWillHideNotification(_ notification: Notification) {
|
|
bottomConstraint.constant = 0
|
|
restoreButtonBottomOffsetConstraint.constant = Values.onboardingButtonBottomOffset
|
|
spacer1HeightConstraint.constant = isIPhone5OrSmaller ? Values.smallSpacing : Values.veryLargeSpacing
|
|
spacer2HeightConstraint.constant = isIPhone5OrSmaller ? Values.smallSpacing : Values.veryLargeSpacing
|
|
UIView.animate(withDuration: 0.25) {
|
|
self.view.layoutIfNeeded()
|
|
}
|
|
}
|
|
|
|
// MARK: Interaction
|
|
@objc private func handleContinueButtonTapped() {
|
|
func showError(title: String, message: String = "") {
|
|
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
|
alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil))
|
|
presentAlert(alert)
|
|
}
|
|
let mnemonic = mnemonicTextView.text!.lowercased()
|
|
do {
|
|
let hexEncodedSeed = try Mnemonic.decode(mnemonic: mnemonic)
|
|
let seed = Data(hex: hexEncodedSeed)
|
|
mnemonicTextView.resignFirstResponder()
|
|
linkDeviceVC.continueWithSeed(seed)
|
|
} catch let error {
|
|
let error = error as? Mnemonic.DecodingError ?? Mnemonic.DecodingError.generic
|
|
showError(title: error.errorDescription!)
|
|
}
|
|
}
|
|
}
|
|
|
|
private final class ScanQRCodePlaceholderVC : UIViewController {
|
|
weak var linkDeviceVC: LinkDeviceVC!
|
|
|
|
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("vc_scan_qr_code_camera_access_explanation", 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("vc_scan_qr_code_grant_camera_access_button_title", 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?.linkDeviceVC.handleCameraAccessGranted()
|
|
} else {
|
|
// Do nothing
|
|
}
|
|
})
|
|
}
|
|
}
|