diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 7494bbfb4..85572ccba 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -1570,9 +1570,11 @@ Tap the + to start adding people."; "device_verification_self_verify_wait_new_sign_in_title" = "Verify this login"; "device_verification_self_verify_wait_information" = "Verify this session from one of your other sessions, granting it access to encrypted messages.\n\nUse the latest %@ on your other devices:"; "device_verification_self_verify_wait_additional_information" = "This works with %@ and other cross-signing capable Matrix clients."; +"device_verification_self_verify_open_on_other_device_title" = "Open %@ on your other device"; +"device_verification_self_verify_open_on_other_device_information" = "You need to verify this session in order to read your secure message history.\n\nOpen Element on one of your other devices and follow the instructions."; "device_verification_self_verify_wait_recover_secrets_without_passphrase" = "Use Security Key"; "device_verification_self_verify_wait_recover_secrets_with_passphrase" = "Use Security Phrase or Key"; -"device_verification_self_verify_wait_recover_secrets_additional_information" = "If you can't access an existing session"; +"device_verification_self_verify_wait_recover_secrets_additional_help" = "Can't access an existing %@ session?"; "device_verification_self_verify_wait_recover_secrets_checking_availability" = "Checking for other verification capabilities ..."; // MARK: Verify @@ -1748,6 +1750,12 @@ Tap the + to start adding people."; "key_verification_verify_qr_code_scan_other_code_success_title" = "Code validated!"; "key_verification_verify_qr_code_scan_other_code_success_message" = "QR code has been successfully validated."; +"key_verification_scan_qr_code_title" = "Scan QR code"; +"key_verification_scan_qr_code_information_other_user" = "Point your camera at the QR code displayed on their device to verify their session"; +"key_verification_scan_qr_code_information_other_device" = "Point your camera at the QR code displayed on your other device to verify this session"; +"key_verification_scan_qr_code_information_other_session" = "Point your camera at the QR code displayed on your other device to verify your session"; +"key_verification_scan_qr_code_information_new_session" = "Point your camera at the QR code displayed on your other device to verify your new session"; + // MARK: Scan confirmation // Scanning diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 96f5ba8fb..4fcd1c6da 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -1871,6 +1871,14 @@ public class VectorL10n: NSObject { public static var deviceVerificationSelfVerifyAlertValidateAction: String { return VectorL10n.tr("Vector", "device_verification_self_verify_alert_validate_action") } + /// You need to verify this session in order to read your secure message history.\n\nOpen Element on one of your other devices and follow the instructions. + public static var deviceVerificationSelfVerifyOpenOnOtherDeviceInformation: String { + return VectorL10n.tr("Vector", "device_verification_self_verify_open_on_other_device_information") + } + /// Open %@ on your other device + public static func deviceVerificationSelfVerifyOpenOnOtherDeviceTitle(_ p1: String) -> String { + return VectorL10n.tr("Vector", "device_verification_self_verify_open_on_other_device_title", p1) + } /// Use this session to verify your new one, granting it access to encrypted messages. public static var deviceVerificationSelfVerifyStartInformation: String { return VectorL10n.tr("Vector", "device_verification_self_verify_start_information") @@ -1895,9 +1903,9 @@ public class VectorL10n: NSObject { public static var deviceVerificationSelfVerifyWaitNewSignInTitle: String { return VectorL10n.tr("Vector", "device_verification_self_verify_wait_new_sign_in_title") } - /// If you can't access an existing session - public static var deviceVerificationSelfVerifyWaitRecoverSecretsAdditionalInformation: String { - return VectorL10n.tr("Vector", "device_verification_self_verify_wait_recover_secrets_additional_information") + /// Can't access an existing %@ session? + public static func deviceVerificationSelfVerifyWaitRecoverSecretsAdditionalHelp(_ p1: String) -> String { + return VectorL10n.tr("Vector", "device_verification_self_verify_wait_recover_secrets_additional_help", p1) } /// Checking for other verification capabilities ... public static var deviceVerificationSelfVerifyWaitRecoverSecretsCheckingAvailability: String { @@ -2987,6 +2995,26 @@ public class VectorL10n: NSObject { public static func keyVerificationScanConfirmationScanningUserWaitingOther(_ p1: String) -> String { return VectorL10n.tr("Vector", "key_verification_scan_confirmation_scanning_user_waiting_other", p1) } + /// Point your camera at the QR code displayed on your other device to verify your new session + public static var keyVerificationScanQrCodeInformationNewSession: String { + return VectorL10n.tr("Vector", "key_verification_scan_qr_code_information_new_session") + } + /// Point your camera at the QR code displayed on your other device to verify this session + public static var keyVerificationScanQrCodeInformationOtherDevice: String { + return VectorL10n.tr("Vector", "key_verification_scan_qr_code_information_other_device") + } + /// Point your camera at the QR code displayed on your other device to verify your session + public static var keyVerificationScanQrCodeInformationOtherSession: String { + return VectorL10n.tr("Vector", "key_verification_scan_qr_code_information_other_session") + } + /// Point your camera at the QR code displayed on their device to verify their session + public static var keyVerificationScanQrCodeInformationOtherUser: String { + return VectorL10n.tr("Vector", "key_verification_scan_qr_code_information_other_user") + } + /// Scan QR code + public static var keyVerificationScanQrCodeTitle: String { + return VectorL10n.tr("Vector", "key_verification_scan_qr_code_title") + } /// Other users may not trust it. public static var keyVerificationSelfVerifyCurrentSessionAlertMessage: String { return VectorL10n.tr("Vector", "key_verification_self_verify_current_session_alert_message") diff --git a/Riot/Modules/KeyVerification/Common/ScanConfirmation/KeyVerificationScanConfirmationViewController.storyboard b/Riot/Modules/KeyVerification/Common/ScanConfirmation/KeyVerificationScanConfirmationViewController.storyboard index b6b9edafa..f353aaa83 100644 --- a/Riot/Modules/KeyVerification/Common/ScanConfirmation/KeyVerificationScanConfirmationViewController.storyboard +++ b/Riot/Modules/KeyVerification/Common/ScanConfirmation/KeyVerificationScanConfirmationViewController.storyboard @@ -1,11 +1,9 @@ - - - - + + - + @@ -13,38 +11,38 @@ - + - + - + - + - + - + @@ -178,6 +178,7 @@ Use the latest Riot on your other devices: + @@ -185,10 +186,8 @@ Use the latest Riot on your other devices: - - @@ -198,6 +197,7 @@ Use the latest Riot on your other devices: + diff --git a/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitViewController.swift b/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitViewController.swift index f7969bf22..91d3920ba 100644 --- a/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitViewController.swift +++ b/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitViewController.swift @@ -30,13 +30,12 @@ final class KeyVerificationSelfVerifyWaitViewController: UIViewController { // MARK: Outlets + @IBOutlet private weak var titleLabel: UILabel! @IBOutlet private weak var informationLabel: UILabel! @IBOutlet private weak var desktopClientImageView: UIImageView! @IBOutlet private weak var mobileClientImageView: UIImageView! - - @IBOutlet private weak var additionalInformationLabel: UILabel! - + @IBOutlet private weak var recoverSecretsAvailabilityLoadingContainerView: UIView! @IBOutlet private weak var recoverSecretsAvailabilityLoadingLabel: UILabel! @IBOutlet private weak var recoverSecretsAvailabilityActivityIndicatorView: UIActivityIndicatorView! @@ -70,7 +69,6 @@ final class KeyVerificationSelfVerifyWaitViewController: UIViewController { super.viewDidLoad() // Do any additional setup after loading the view. - self.setupViews() self.activityPresenter = ActivityIndicatorPresenter() self.errorPresenter = MXKErrorAlertPresentation() @@ -96,11 +94,11 @@ final class KeyVerificationSelfVerifyWaitViewController: UIViewController { if let navigationBar = self.navigationController?.navigationBar { theme.applyStyle(onNavigationBar: navigationBar) } - - self.informationLabel.textColor = theme.textPrimaryColor + + self.titleLabel.textColor = theme.textPrimaryColor + self.informationLabel.textColor = theme.textSecondaryColor self.desktopClientImageView.tintColor = theme.tintColor self.mobileClientImageView.tintColor = theme.tintColor - self.additionalInformationLabel.textColor = theme.textPrimaryColor self.recoverSecretsAvailabilityLoadingLabel.textColor = theme.textSecondaryColor self.recoverSecretsAvailabilityActivityIndicatorView.color = theme.tintColor } @@ -125,16 +123,13 @@ final class KeyVerificationSelfVerifyWaitViewController: UIViewController { self.cancelBarButtonItem = cancelBarButtonItem } - self.title = VectorL10n.deviceVerificationSelfVerifyWaitTitle - - self.informationLabel.text = VectorL10n.deviceVerificationSelfVerifyWaitInformation(AppInfo.current.displayName) + self.titleLabel.text = VectorL10n.deviceVerificationSelfVerifyOpenOnOtherDeviceTitle(AppInfo.current.displayName) + self.informationLabel.text = VectorL10n.deviceVerificationSelfVerifyOpenOnOtherDeviceInformation self.desktopClientImageView.image = Asset.Images.monitor.image.withRenderingMode(.alwaysTemplate) self.mobileClientImageView.image = Asset.Images.smartphone.image.withRenderingMode(.alwaysTemplate) - - self.additionalInformationLabel.text = VectorL10n.deviceVerificationSelfVerifyWaitAdditionalInformation(AppInfo.current.displayName) - - self.recoverSecretsAdditionalInformationLabel.text = VectorL10n.deviceVerificationSelfVerifyWaitRecoverSecretsAdditionalInformation + + self.recoverSecretsAdditionalInformationLabel.text = VectorL10n.deviceVerificationSelfVerifyWaitRecoverSecretsAdditionalHelp(AppInfo.current.displayName) } private func render(viewState: KeyVerificationSelfVerifyWaitViewState) { @@ -168,7 +163,6 @@ final class KeyVerificationSelfVerifyWaitViewController: UIViewController { private func renderLoaded(viewData: KeyVerificationSelfVerifyWaitViewData) { self.activityPresenter.removeCurrentActivityIndicator(animated: true) - self.title = viewData.isNewSignIn ? VectorL10n.deviceVerificationSelfVerifyWaitNewSignInTitle : VectorL10n.deviceVerificationSelfVerifyWaitTitle self.cancelBarButtonItem?.title = viewData.isNewSignIn ? VectorL10n.skip : VectorL10n.cancel let hideRecoverSecrets: Bool diff --git a/Riot/Modules/QRCode/Reader/QRCodeReaderView.swift b/Riot/Modules/QRCode/Reader/QRCodeReaderView.swift new file mode 100644 index 000000000..a860bba67 --- /dev/null +++ b/Riot/Modules/QRCode/Reader/QRCodeReaderView.swift @@ -0,0 +1,215 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit +import ZXingObjC +import Combine + +final class QRCodeReaderView: UIView { + + // MARK: Public + + var didFoundData: (Data) -> Void = { _ in } + + // MARK: Private + + private lazy var zxCapture: ZXCapture = ZXCapture() + private var captureSizeTransform: CGAffineTransform? + private var isScanning: Bool = false + private var isFirstApplyOrientation: Bool = false + + private var rotationObserver: AnyCancellable? + + init() { + super.init(frame: .zero) + setup() + } + + override init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setup() + } + + deinit { + self.zxCapture.layer.removeFromSuperlayer() + self.zxCapture.hard_stop() + } + + override func didMoveToSuperview() { + super.didMoveToSuperview() + + if superview == nil { + stopScanning() + } + } + + // MARK: - Public + + func startScanning() { + self.zxCapture.start() + isScanning = true + } + + func stopScanning() { + self.zxCapture.stop() + isScanning = false + } + + // MARK: - Private + + override func layoutSubviews() { + super.layoutSubviews() + + guard isFirstApplyOrientation == false else { + return + } + + isFirstApplyOrientation = true + applyOrientation() + } + + private func setup() { + isUserInteractionEnabled = true + clipsToBounds = true + self.setupQRCodeReaderView() + + rotationObserver = NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification) + .sink(receiveValue: { [weak self] _ in + self?.applyOrientation() + }) + } + + private func setupQRCodeReaderView() { + zxCapture.delegate = self + zxCapture.camera = zxCapture.back() + + zxCapture.layer.frame = self.bounds + self.layer.addSublayer(zxCapture.layer) + } + + private func applyOrientation() { + + let orientation = UIApplication.shared.statusBarOrientation + let captureRotation: Double + let scanRectRotation: Double + + switch orientation { + case .portrait: + captureRotation = 0 + scanRectRotation = 90 + case .landscapeLeft: + captureRotation = 90 + scanRectRotation = 180 + case .landscapeRight: + captureRotation = 270 + scanRectRotation = 0 + case .portraitUpsideDown: + captureRotation = 180 + scanRectRotation = 270 + default: + captureRotation = 0 + scanRectRotation = 90 + } + + applyRectOfInterest(orientation: orientation) + + let angleRadius = captureRotation / 180.0 * Double.pi + let captureTranform = CGAffineTransform(rotationAngle: CGFloat(angleRadius)) + + zxCapture.transform = captureTranform + zxCapture.rotation = CGFloat(scanRectRotation) + zxCapture.layer.frame = self.bounds + } + + private func applyRectOfInterest(orientation: UIInterfaceOrientation) { + var transformedVideoRect = self.frame + let cameraSessionPreset = zxCapture.sessionPreset + + var scaleVideoX, scaleVideoY: CGFloat + var videoHeight, videoWidth: CGFloat + + // Currently support only for 1920x1080 || 1280x720 + if cameraSessionPreset == AVCaptureSession.Preset.hd1920x1080.rawValue { + videoHeight = 1080.0 + videoWidth = 1920.0 + } else { + videoHeight = 720.0 + videoWidth = 1280.0 + } + + if orientation == UIInterfaceOrientation.portrait { + scaleVideoX = self.frame.width / videoHeight + scaleVideoY = self.frame.height / videoWidth + + // Convert CGPoint under portrait mode to map with orientation of image + // because the image will be cropped before rotate + // reference: https://github.com/TheLevelUp/ZXingObjC/issues/222 + let realX = transformedVideoRect.origin.y + let realY = self.frame.size.width - transformedVideoRect.size.width - transformedVideoRect.origin.x + let realWidth = transformedVideoRect.size.height + let realHeight = transformedVideoRect.size.width + transformedVideoRect = CGRect(x: realX, y: realY, width: realWidth, height: realHeight) + + } else { + scaleVideoX = self.frame.width / videoWidth + scaleVideoY = self.frame.height / videoHeight + } + + captureSizeTransform = CGAffineTransform(scaleX: 1.0/scaleVideoX, y: 1.0/scaleVideoY) + + guard let _captureSizeTransform = captureSizeTransform else { + return + } + + let transformRect = transformedVideoRect.applying(_captureSizeTransform) + zxCapture.scanRect = transformRect + } + +} + + +// MARK: - ZXCaptureDelegate +extension QRCodeReaderView: ZXCaptureDelegate { + + func captureCameraIsReady(_ capture: ZXCapture!) { + isScanning = true + } + + func captureResult(_ capture: ZXCapture!, result: ZXResult!) { + guard let zxResult = result, isScanning == true else { + return + } + + guard zxResult.barcodeFormat == kBarcodeFormatQRCode else { + return + } + + self.stopScanning() + + if let bytes = result.resultMetadata.object(forKey: kResultMetadataTypeByteSegments.rawValue) as? NSArray, + let byteArray = bytes.firstObject as? ZXByteArray { + + let data = Data(bytes: UnsafeRawPointer(byteArray.array), count: Int(byteArray.length)) + + self.didFoundData(data) + } + } +} diff --git a/Riot/Modules/QRCode/Reader/QRCodeReaderViewController.swift b/Riot/Modules/QRCode/Reader/QRCodeReaderViewController.swift index 4d35af705..764744613 100644 --- a/Riot/Modules/QRCode/Reader/QRCodeReaderViewController.swift +++ b/Riot/Modules/QRCode/Reader/QRCodeReaderViewController.swift @@ -15,7 +15,6 @@ */ import UIKit -import ZXingObjC protocol QRCodeReaderViewControllerDelegate: AnyObject { func qrCodeReaderViewController(_ viewController: QRCodeReaderViewController, didFound payloadData: Data) @@ -40,10 +39,7 @@ final class QRCodeReaderViewController: UIViewController { private var theme: Theme! private var errorPresenter: MXKErrorPresentation! - private lazy var zxCapture: ZXCapture = ZXCapture() - private var captureSizeTransform: CGAffineTransform? - private var isScanning: Bool = false - private var isFirstApplyOrientation: Bool = false + private var qrCodeReaderView: QRCodeReaderView! // MARK: Public @@ -56,12 +52,7 @@ final class QRCodeReaderViewController: UIViewController { viewController.theme = ThemeService.shared().theme return viewController } - - deinit { - self.zxCapture.layer.removeFromSuperlayer() - self.zxCapture.hard_stop() - } - + // MARK: - Life cycle override func viewDidLoad() { @@ -92,40 +83,14 @@ final class QRCodeReaderViewController: UIViewController { return self.theme.statusBarStyle } - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - guard isFirstApplyOrientation == false else { - return - } - - isFirstApplyOrientation = true - applyOrientation() - } - - override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { - super.viewWillTransition(to: size, with: coordinator) - - coordinator.animate(alongsideTransition: { (context) in - // do nothing - }, completion: { [weak self] (context) in - guard let self = self else { - return - } - self.applyOrientation() - }) - } - // MARK: - Public func startScanning() { - self.zxCapture.start() - isScanning = true + qrCodeReaderView.startScanning() } func stopScanning() { - self.zxCapture.stop() - isScanning = false + qrCodeReaderView.stopScanning() } // MARK: - Private @@ -145,94 +110,15 @@ final class QRCodeReaderViewController: UIViewController { } private func setupViews() { - self.setupQRCodeReaderView() + let qrCodeReaderView = QRCodeReaderView() + qrCodeReaderView.didFoundData = qrCodeReader(didFound:) + self.qrCodeReaderView = qrCodeReaderView + + self.codeReaderContainerView.vc_addSubViewMatchingParent(qrCodeReaderView) } - private func setupQRCodeReaderView() { - zxCapture.delegate = self - zxCapture.camera = zxCapture.back() - - zxCapture.layer.frame = codeReaderContainerView.bounds - codeReaderContainerView.layer.addSublayer(zxCapture.layer) - } - - private func applyOrientation() { - - let orientation = UIApplication.shared.statusBarOrientation - let captureRotation: Double - let scanRectRotation: Double - - switch orientation { - case .portrait: - captureRotation = 0 - scanRectRotation = 90 - case .landscapeLeft: - captureRotation = 90 - scanRectRotation = 180 - case .landscapeRight: - captureRotation = 270 - scanRectRotation = 0 - case .portraitUpsideDown: - captureRotation = 180 - scanRectRotation = 270 - default: - captureRotation = 0 - scanRectRotation = 90 - } - - applyRectOfInterest(orientation: orientation) - - let angleRadius = captureRotation / 180.0 * Double.pi - let captureTranform = CGAffineTransform(rotationAngle: CGFloat(angleRadius)) - - zxCapture.transform = captureTranform - zxCapture.rotation = CGFloat(scanRectRotation) - zxCapture.layer.frame = codeReaderContainerView.frame - } - - private func applyRectOfInterest(orientation: UIInterfaceOrientation) { - guard var transformedVideoRect = codeReaderContainerView?.frame, - let cameraSessionPreset = zxCapture.sessionPreset - else { return } - - var scaleVideoX, scaleVideoY: CGFloat - var videoHeight, videoWidth: CGFloat - - // Currently support only for 1920x1080 || 1280x720 - if cameraSessionPreset == AVCaptureSession.Preset.hd1920x1080.rawValue { - videoHeight = 1080.0 - videoWidth = 1920.0 - } else { - videoHeight = 720.0 - videoWidth = 1280.0 - } - - if orientation == UIInterfaceOrientation.portrait { - scaleVideoX = self.view.frame.width / videoHeight - scaleVideoY = self.view.frame.height / videoWidth - - // Convert CGPoint under portrait mode to map with orientation of image - // because the image will be cropped before rotate - // reference: https://github.com/TheLevelUp/ZXingObjC/issues/222 - let realX = transformedVideoRect.origin.y - let realY = self.view.frame.size.width - transformedVideoRect.size.width - transformedVideoRect.origin.x - let realWidth = transformedVideoRect.size.height - let realHeight = transformedVideoRect.size.width - transformedVideoRect = CGRect(x: realX, y: realY, width: realWidth, height: realHeight) - - } else { - scaleVideoX = self.view.frame.width / videoWidth - scaleVideoY = self.view.frame.height / videoHeight - } - - captureSizeTransform = CGAffineTransform(scaleX: 1.0/scaleVideoX, y: 1.0/scaleVideoY) - - guard let _captureSizeTransform = captureSizeTransform else { - return - } - - let transformRect = transformedVideoRect.applying(_captureSizeTransform) - zxCapture.scanRect = transformRect + private func qrCodeReader(didFound data: Data) { + self.delegate?.qrCodeReaderViewController(self, didFound: data) } // MARK: - Actions @@ -241,31 +127,3 @@ final class QRCodeReaderViewController: UIViewController { self.delegate?.qrCodeReaderViewControllerDidCancel(self) } } - -// MARK: - ZXCaptureDelegate -extension QRCodeReaderViewController: ZXCaptureDelegate { - - func captureCameraIsReady(_ capture: ZXCapture!) { - isScanning = true - } - - func captureResult(_ capture: ZXCapture!, result: ZXResult!) { - guard let zxResult = result, isScanning == true else { - return - } - - guard zxResult.barcodeFormat == kBarcodeFormatQRCode else { - return - } - - self.stopScanning() - - if let bytes = result.resultMetadata.object(forKey: kResultMetadataTypeByteSegments.rawValue) as? NSArray, - let byteArray = bytes.firstObject as? ZXByteArray { - - let data = Data(bytes: UnsafeRawPointer(byteArray.array), count: Int(byteArray.length)) - - self.delegate?.qrCodeReaderViewController(self, didFound: data) - } - } -} diff --git a/changelog.d/3115.change b/changelog.d/3115.change new file mode 100644 index 000000000..edc3085ab --- /dev/null +++ b/changelog.d/3115.change @@ -0,0 +1 @@ +Session verification: automatically starts scanning for a QR code if we do not have a QR code to display.