session-ios/Session/Settings/QRCodeVC.swift
Morgan Pretty 6f4bdcdccb Moved some logic outside of DBWrite closures to prevent hangs
Updated the SessionApp.presentConversation function from using the DBWrite thread if it didn't need to
Updated the PagedDatabaseObserver to process database commits async on a serial queue to avoid holding up the DBWrite thread
Moved another Atomic mutation outside of a DBWrite closure
Refactored the PagedDatabaseObserver 'databaseDidCommit' logic to be much more straightforward
Tweaked a couple of flaky unit tests
2023-07-07 13:20:32 +10:00

328 lines
14 KiB
Swift

// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import AVFoundation
import SessionUIKit
import SessionMessagingKit
import SessionUtilitiesKit
final class QRCodeVC : BaseVC, UIPageViewControllerDataSource, UIPageViewControllerDelegate, QRScannerDelegate {
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: "vc_qr_code_view_my_qr_code_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_qr_code_view_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 viewMyQRCodeVC: ViewMyQRCodeVC = {
let result = ViewMyQRCodeVC()
result.qrCodeVC = self
return result
}()
private lazy var scanQRCodePlaceholderVC: ScanQRCodePlaceholderVC = {
let result = ScanQRCodePlaceholderVC()
result.qrCodeVC = self
return result
}()
private lazy var scanQRCodeWrapperVC: ScanQRCodeWrapperVC = {
let message = "vc_qr_code_view_scan_qr_code_explanation".localized()
let result = ScanQRCodeWrapperVC(message: message)
result.delegate = self
return result
}()
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setNavBarTitle("vc_qr_code_title".localized())
// Set up tab bar
view.addSubview(tabBar)
tabBar.pin(.top, to: .top, of: view.safeAreaLayoutGuide)
tabBar.pin(.leading, to: .leading, of: view)
tabBar.pin(.trailing, to: .trailing, of: view)
// Set up page VC
let containerView: UIView = UIView()
view.addSubview(containerView)
containerView.pin(.top, to: .bottom, of: tabBar)
containerView.pin(.leading, to: .leading, of: view)
containerView.pin(.trailing, to: .trailing, of: view)
containerView.pin(.bottom, to: .bottom, of: view)
let hasCameraAccess = (AVCaptureDevice.authorizationStatus(for: .video) == .authorized)
pages = [ viewMyQRCodeVC, (hasCameraAccess ? scanQRCodeWrapperVC : scanQRCodePlaceholderVC) ]
pageVC.dataSource = self
pageVC.delegate = self
pageVC.setViewControllers([ viewMyQRCodeVC ], direction: .forward, animated: false, completion: nil)
addChild(pageVC)
containerView.addSubview(pageVC.view)
pageVC.view.pin(to: containerView)
pageVC.didMove(toParent: 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() {
DispatchQueue.main.async {
self.pages[1] = self.scanQRCodeWrapperVC
self.pageVC.setViewControllers([ self.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: QRCodeScanningViewController, didDetectQRCodeWith string: String) {
let hexEncodedPublicKey = string
startNewPrivateChatIfPossible(with: hexEncodedPublicKey)
}
fileprivate func startNewPrivateChatIfPossible(with hexEncodedPublicKey: String) {
if !KeyPair.isValidHexEncodedPublicKey(candidate: hexEncodedPublicKey) {
let modal: ConfirmationModal = ConfirmationModal(
targetView: self.view,
info: ConfirmationModal.Info(
title: "invalid_session_id".localized(),
body: .text("INVALID_SESSION_ID_MESSAGE".localized()),
cancelTitle: "BUTTON_OK".localized(),
cancelStyle: .alert_text
)
)
self.present(modal, animated: true)
}
else {
SessionApp.presentConversationCreatingIfNeeded(
for: hexEncodedPublicKey,
variant: .contact,
dismissing: presentingViewController,
animated: false
)
}
}
}
private final class ViewMyQRCodeVC : UIViewController {
weak var qrCodeVC: QRCodeVC!
// MARK: - Lifecycle
override func viewDidLoad() {
// Remove background color
view.themeBackgroundColor = .clear
// Set up title label
let titleLabel = UILabel()
titleLabel.font = .boldSystemFont(ofSize: isIPhone5OrSmaller ? CGFloat(40) : Values.massiveFontSize)
titleLabel.text = "Scan Me"
titleLabel.themeTextColor = .textPrimary
titleLabel.textAlignment = .center
titleLabel.lineBreakMode = .byWordWrapping
titleLabel.numberOfLines = 1
titleLabel.set(.height, to: isIPhone5OrSmaller ? CGFloat(40) : Values.massiveFontSize)
// Set up QR code image view
let qrCodeImageView = UIImageView(
image: QRCode.generate(for: getUserHexEncodedPublicKey(), hasBackground: false)
.withRenderingMode(.alwaysTemplate)
)
qrCodeImageView.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
qrCodeImageView.set(.width, to: .height, of: qrCodeImageView)
qrCodeImageView.heightAnchor
.constraint(lessThanOrEqualToConstant: (isIPhone5OrSmaller ? 180 : 240))
.isActive = true
#if targetEnvironment(simulator)
#else
// Note: For some reason setting this seems to stop the QRCode from rendering on the
// simulator so only doing it on device
qrCodeImageView.contentMode = .scaleAspectFit
#endif
let qrCodeImageViewBackgroundView = UIView()
qrCodeImageViewBackgroundView.layer.cornerRadius = 8
qrCodeImageViewBackgroundView.addSubview(qrCodeImageView)
qrCodeImageView.pin(
to: qrCodeImageViewBackgroundView,
withInset: 5 // The QRCode image has about 6pt of padding and we want 11 in total
)
ThemeManager.onThemeChange(observer: qrCodeImageView) { [weak qrCodeImageView, weak qrCodeImageViewBackgroundView] theme, _ in
switch theme.interfaceStyle {
case .light:
qrCodeImageView?.themeTintColorForced = .theme(theme, color: .textPrimary)
qrCodeImageViewBackgroundView?.themeBackgroundColorForced = nil
default:
qrCodeImageView?.themeTintColorForced = .theme(theme, color: .backgroundPrimary)
qrCodeImageViewBackgroundView?.themeBackgroundColorForced = .color(.white)
}
}
// Set up QR code image view container
let qrCodeImageViewContainer = UIView()
qrCodeImageViewContainer.accessibilityLabel = "Your QR code"
qrCodeImageViewContainer.isAccessibilityElement = true
qrCodeImageViewContainer.addSubview(qrCodeImageViewBackgroundView)
qrCodeImageViewBackgroundView.center(.horizontal, in: qrCodeImageViewContainer)
qrCodeImageViewBackgroundView.pin(.top, to: .top, of: qrCodeImageViewContainer)
qrCodeImageViewBackgroundView.pin(.bottom, to: .bottom, of: qrCodeImageViewContainer)
// Set up explanation label
let explanationLabel = UILabel()
explanationLabel.font = .systemFont(ofSize: Values.mediumFontSize)
explanationLabel.text = "vc_view_my_qr_code_explanation".localized()
explanationLabel.themeTextColor = .textPrimary
explanationLabel.textAlignment = .center
explanationLabel.lineBreakMode = .byWordWrapping
explanationLabel.numberOfLines = 0
// Set up share button
let shareButton = SessionButton(style: .bordered, size: .large)
shareButton.setTitle("share".localized(), for: .normal)
shareButton.addTarget(self, action: #selector(shareQRCode), for: .touchUpInside)
// Set up share button container
let shareButtonContainer = UIView()
shareButtonContainer.addSubview(shareButton)
shareButton.pin(.top, to: .top, of: shareButtonContainer)
shareButton.pin(.bottom, to: .bottom, of: shareButtonContainer)
if UIDevice.current.isIPad {
shareButton.center(in: shareButtonContainer)
shareButton.set(.width, to: Values.iPadButtonWidth)
} else {
shareButton.pin(.leading, to: .leading, of: shareButtonContainer, withInset: 80)
shareButton.pin(.trailing, to: .trailing, of: shareButtonContainer, withInset: -80)
}
// Set up stack view
let spacing = (isIPhone5OrSmaller ? Values.mediumSpacing : Values.largeSpacing)
let stackView = UIStackView(
arrangedSubviews: [
titleLabel,
UIView.spacer(withHeight: spacing),
qrCodeImageViewContainer,
UIView.spacer(withHeight: spacing),
explanationLabel,
UIView.vStretchingSpacer(),
shareButtonContainer
]
)
stackView.axis = .vertical
stackView.alignment = .fill
stackView.layoutMargins = UIEdgeInsets(
top: Values.largeSpacing,
left: Values.largeSpacing,
bottom: Values.smallSpacing,
right: Values.largeSpacing
)
stackView.isLayoutMarginsRelativeArrangement = true
view.addSubview(stackView)
stackView.pin(to: view)
}
// MARK: - Interaction
@objc private func shareQRCode() {
let qrCode = QRCode.generate(for: getUserHexEncodedPublicKey(), hasBackground: true)
let shareVC = UIActivityViewController(activityItems: [ qrCode ], applicationActivities: nil)
if UIDevice.current.isIPad {
shareVC.excludedActivityTypes = []
shareVC.popoverPresentationController?.permittedArrowDirections = []
shareVC.popoverPresentationController?.sourceView = self.view
shareVC.popoverPresentationController?.sourceRect = self.view.bounds
}
qrCodeVC.navigationController!.present(shareVC, animated: true, completion: nil)
}
}
private final class ScanQRCodePlaceholderVC : UIViewController {
weak var qrCodeVC: QRCodeVC!
override func viewDidLoad() {
// Remove background color
view.themeBackgroundColor = .clear
// Set up explanation label
let explanationLabel = UILabel()
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
explanationLabel.text = "vc_scan_qr_code_camera_access_explanation".localized()
explanationLabel.themeTextColor = .textPrimary
explanationLabel.textAlignment = .center
explanationLabel.lineBreakMode = .byWordWrapping
explanationLabel.numberOfLines = 0
// Set up call to action button
let callToActionButton = UIButton()
callToActionButton.titleLabel?.font = .boldSystemFont(ofSize: Values.mediumFontSize)
callToActionButton.setTitle("vc_scan_qr_code_grant_camera_access_button_title".localized(), for: UIControl.State.normal)
callToActionButton.setThemeTitleColor(.primary, for: .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.addSubview(stackView)
stackView.pin(.leading, to: .leading, of: view, withInset: Values.massiveSpacing)
stackView.pin(.trailing, to: .trailing, of: view, withInset: -Values.massiveSpacing)
stackView.center(.vertical, in: view, withInset: -16) // Makes things appear centered visually
}
@objc private func requestCameraAccess() {
Permissions.requestCameraPermissionIfNeeded { [weak self] in
self?.qrCodeVC.handleCameraAccessGranted()
}
}
}