MESSENGER-4093 scan permalinks as qr

This commit is contained in:
Arnfried Griesert
2023-02-23 06:50:16 +00:00
committed by Frank Rotermund
parent 11ed494648
commit 00222176c7
15 changed files with 325 additions and 127 deletions

View File

@@ -108,7 +108,7 @@ class BWIBuildSettings: NSObject {
var bwiAllowRoomPermalink = false
var bwiAllowUserPermalink = false
var bwiCheckAppVersion = true
var bwiNotificationTimes = true
@@ -544,10 +544,16 @@ class BWIBuildSettings: NSObject {
// internal html page for netiquette in en and de
var netiquetteEnHTML = "netiquette_en"
var netiquetteDeHTML = "netiquette_de"
// MARK: - Scan server qr code
var scanServerQRCode = true
var allowScanServerQRCode = true
// MARK: - Login with qr code
var allowLoginWithQR = false // should be set by the server but we disable it with false also in the app
// MARK: - Scan permalink qr code
var clientPermalinkBaseUrl = ""
var allowScanPermalinkQRCode = false
// MARK: - Maintenance
var enableMaintenanceInfoOnWelcomeScreen = false

View File

@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "icon_qrcode.viewfinder.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

View File

@@ -0,0 +1,10 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_30_95)">
<path d="M0.78125 6.23047C1.29883 6.23047 1.57227 5.9375 1.57227 5.42969V3.125C1.57227 2.10938 2.10938 1.5918 3.08594 1.5918H5.44922C5.9668 1.5918 6.25 1.30859 6.25 0.800781C6.25 0.292969 5.9668 0.0195312 5.44922 0.0195312H3.06641C1.02539 0.0195312 0 1.02539 0 3.03711V5.42969C0 5.9375 0.283203 6.23047 0.78125 6.23047ZM18.3301 6.23047C18.8477 6.23047 19.1211 5.9375 19.1211 5.42969V3.03711C19.1211 1.02539 18.0957 0.0195312 16.0547 0.0195312H13.6621C13.1543 0.0195312 12.8711 0.292969 12.8711 0.800781C12.8711 1.30859 13.1543 1.5918 13.6621 1.5918H16.0254C16.9922 1.5918 17.5488 2.10938 17.5488 3.125V5.42969C17.5488 5.9375 17.832 6.23047 18.3301 6.23047ZM3.06641 19.1309H5.44922C5.9668 19.1309 6.25 18.8477 6.25 18.3496C6.25 17.8418 5.9668 17.5586 5.44922 17.5586H3.08594C2.10938 17.5586 1.57227 17.041 1.57227 16.0254V13.7207C1.57227 13.2031 1.28906 12.9199 0.78125 12.9199C0.273438 12.9199 0 13.2031 0 13.7207V16.1035C0 18.125 1.02539 19.1309 3.06641 19.1309ZM13.6621 19.1309H16.0547C18.0957 19.1309 19.1211 18.1152 19.1211 16.1035V13.7207C19.1211 13.2031 18.8379 12.9199 18.3301 12.9199C17.8223 12.9199 17.5488 13.2031 17.5488 13.7207V16.0254C17.5488 17.041 16.9922 17.5586 16.0254 17.5586H13.6621C13.1543 17.5586 12.8711 17.8418 12.8711 18.3496C12.8711 18.8477 13.1543 19.1309 13.6621 19.1309ZM5.30273 14.2578H8.70117C8.94531 14.2578 9.13086 14.0723 9.13086 13.8281V10.4297C9.13086 10.1855 8.94531 10 8.70117 10H5.30273C5.05859 10 4.87305 10.1855 4.87305 10.4297V13.8281C4.87305 14.0723 5.05859 14.2578 5.30273 14.2578ZM5.72266 13.4082V10.8496H8.28125V13.4082H5.72266ZM6.46484 12.6562H7.53906V11.5918H6.46484V12.6562ZM5.30273 9.15039H8.70117C8.94531 9.15039 9.13086 8.96484 9.13086 8.73047V5.32227C9.13086 5.08789 8.94531 4.89258 8.70117 4.89258H5.30273C5.05859 4.89258 4.87305 5.08789 4.87305 5.32227V8.73047C4.87305 8.96484 5.05859 9.15039 5.30273 9.15039ZM5.72266 8.30078V5.74219H8.28125V8.30078H5.72266ZM6.46484 7.55859H7.53906V6.48438H6.46484V7.55859ZM10.4004 9.15039H13.7988C14.043 9.15039 14.2285 8.96484 14.2285 8.73047V5.32227C14.2285 5.08789 14.043 4.89258 13.7988 4.89258H10.4004C10.1562 4.89258 9.9707 5.08789 9.9707 5.32227V8.73047C9.9707 8.96484 10.1562 9.15039 10.4004 9.15039ZM10.8203 8.30078V5.74219H13.3789V8.30078H10.8203ZM11.5723 7.55859H12.6367V6.48438H11.5723V7.55859ZM10.0977 14.1309H11.1621V13.0664H10.0977V14.1309ZM13.0371 14.1309H14.1016V13.0664H13.0371V14.1309ZM11.5625 12.6562H12.6367V11.5918H11.5625V12.6562ZM10.0977 11.1816H11.1621V10.1172H10.0977V11.1816ZM13.0371 11.1816H14.1016V10.1172H13.0371V11.1816Z" fill="black"/>
</g>
<defs>
<clipPath id="clip0_30_95">
<rect width="19.1211" height="19.1309" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -571,6 +571,11 @@
// MARK: - New Layout
"all_chats_edit_layout_show_personal_notes" = "Notizenraum anzeigen";
// MARK: - Permalink Scan
"room_recents_scan_qr_code" = "QR Code scannen";
"room_recents_scan_failed_title" = "Scan fehlgeschlagen";
"room_recents_scan_failed_message" = "Dieser QR Code entspricht keinem gültigen Permalink.";
// MARK: - Context Menu All Chats
"room_recents_create_empty_room" = "Neuer Raum";
"room_recents_start_chat_with" = "Neue Direktnachricht";

View File

@@ -450,6 +450,11 @@
// MARK: - New Layout
"all_chats_edit_layout_show_personal_notes" = "Show personal notes";
// MARK: - Permalink Scan
"room_recents_scan_qr_code" = "Scan QR Code";
"room_recents_scan_failed_title" = "Scan failed";
"room_recents_scan_failed_message" = "This qr code does not conform to a permlink.";
// MARK: Context Menu All Chats
"room_recents_create_empty_room" = "New room";
"room_recents_start_chat_with" = "New chat";

View File

@@ -48,6 +48,7 @@ internal class Asset: NSObject {
internal static let birthdayCake = ImageAsset(name: "birthday_cake")
internal static let fileAttachmentIcon = ImageAsset(name: "file_attachment_icon")
internal static let fileScanInfected = ImageAsset(name: "file_scan_infected")
internal static let qrcodeViewfinder = ImageAsset(name: "qrcode_viewfinder")
internal static let welcomeExperience1 = ImageAsset(name: "welcome_experience_1")
internal static let welcomeExperience2 = ImageAsset(name: "welcome_experience_2")
internal static let welcomeExperience3 = ImageAsset(name: "welcome_experience_3")

View File

@@ -162,6 +162,11 @@ FOUNDATION_EXPORT NSString *const RecentsViewControllerDataReadyNotification;
*/
- (void)startChat;
/**
Open the QR code scanner for scanning permalinks.
*/
- (void)scanPermalink;
/**
Open screen to create a new room.
*/

View File

@@ -1917,6 +1917,28 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro
[self performSegueWithIdentifier:@"presentStartChat" sender:self];
}
- (void)scanPermalink {
AVAuthorizationStatus authStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
if(authStatus == AVAuthorizationStatusDenied) {
UIAlertController* alert = [UIAlertController alertControllerWithTitle:BWIL10n.authenticationServerSelectionQrMissingAuthorizationTitle
message:BWIL10n.authenticationServerSelectionQrMissingAuthorizationMessage
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction* primaryAction = [UIAlertAction actionWithTitle:VectorL10n.settingsTitle style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
[[UIApplication sharedApplication] openURL: [NSURL URLWithString:UIApplicationOpenSettingsURLString] options:@{} completionHandler:nil];
}];
UIAlertAction* secondaryAction = [UIAlertAction actionWithTitle:VectorL10n.cancel style:UIAlertActionStyleCancel
handler:^(UIAlertAction * action) {}];
[alert addAction:primaryAction];
[alert addAction:secondaryAction];
[self presentViewController:alert animated:YES completion:nil];
} else {
UIViewController* viewController = [PermalinkQRCodeScannerController createFromSwiftUIView];
[self presentViewController:viewController animated: YES completion: nil];
}
}
- (void)createNewRoom
{
// Sanity check

View File

@@ -22,6 +22,7 @@ enum AllChatsEditActionProviderOption {
case createRoom
case startChat
case createSpace
case scanPermalink
}
protocol AllChatsEditActionProviderDelegate: AnyObject {
@@ -50,29 +51,57 @@ class AllChatsEditActionProvider {
// MARK: - RoomActionProviderProtocol
var menu: UIMenu {
guard parentSpace != nil else {
var createActions = [
self.createRoomAction,
self.startChatAction
]
if rootSpaceCount > 0 && BWIBuildSettings.shared.enableSpaces {
createActions.insert(self.createSpaceAction, at: 0)
if BWIBuildSettings.shared.allowScanPermalinkQRCode {
guard parentSpace != nil else {
var createActions = [
self.exploreRoomsAction,
self.createRoomAction,
self.startChatAction
]
if rootSpaceCount > 0 && BWIBuildSettings.shared.enableSpaces {
createActions.insert(self.createSpaceAction, at: 0)
}
return UIMenu(title: "", children: [
self.scanPermalinkAction,
UIMenu(title: "", options: .displayInline, children: createActions)
])
}
return UIMenu(title: "", children: [
self.exploreRoomsAction,
UIMenu(title: "", options: .displayInline, children: createActions)
UIMenu(title: "", options: .displayInline, children: [
self.scanPermalinkAction
]),
UIMenu(title: "", options: .displayInline, children: [
self.exploreRoomsAction,
self.createSpaceAction,
self.createRoomAction
])
])
} else {
guard parentSpace != nil else {
var createActions = [
self.createRoomAction,
self.startChatAction
]
if rootSpaceCount > 0 && BWIBuildSettings.shared.enableSpaces {
createActions.insert(self.createSpaceAction, at: 0)
}
return UIMenu(title: "", children: [
self.exploreRoomsAction,
UIMenu(title: "", options: .displayInline, children: createActions)
])
}
return UIMenu(title: "", children: [
UIMenu(title: "", options: .displayInline, children: [
self.exploreRoomsAction
]),
UIMenu(title: "", options: .displayInline, children: [
self.createSpaceAction,
self.createRoomAction
])
])
}
return UIMenu(title: "", children: [
UIMenu(title: "", options: .displayInline, children: [
self.exploreRoomsAction
]),
UIMenu(title: "", options: .displayInline, children: [
self.createSpaceAction,
self.createRoomAction
])
])
}
// MARK: - Public
@@ -167,4 +196,14 @@ class AllChatsEditActionProvider {
self.delegate?.allChatsEditActionProvider(self, didSelect: .createSpace)
}
}
private var scanPermalinkAction: UIAction {
UIAction(title: BWIL10n.roomRecentsScanQrCode,
image: Asset.Images.qrcodeViewfinder.image) { [weak self] action in
guard let self = self else { return }
self.delegate?.allChatsEditActionProvider(self, didSelect: .scanPermalink)
}
}
}

View File

@@ -770,6 +770,8 @@ extension AllChatsViewController: AllChatsEditActionProviderDelegate {
startChat()
case .createSpace:
showCreateSpace(parentSpaceId: dataSource?.currentSpace?.spaceId)
case .scanPermalink:
scanPermalink()
}
}

View File

@@ -48,7 +48,7 @@ struct AuthenticationServerSelectionScreen: View {
.padding(.top, OnboardingMetrics.topPaddingToNavigationBar)
.padding(.bottom, 16)
if BWIBuildSettings.shared.scanServerQRCode {
if BWIBuildSettings.shared.allowScanServerQRCode {
scanButton
.alert(isPresented: $showAlertForMissingCameraAuthorization) {
Alert(

View File

@@ -158,10 +158,8 @@ extension UserDefaults
return "https://" + url
} else if let url = ServerURLHelper.shared.httpsPermalink() {
return url
} else if let url = BuildSettings.clientPermalinkBaseUrl {
return url
} else {
return nil
return BWIBuildSettings.shared.clientPermalinkBaseUrl
}
}

View File

@@ -16,8 +16,6 @@
*/
import SwiftUI
import UIKit
import AVFoundation
struct AuthenticationServerSelectionQRCodeScanner: View {
@Environment(\.presentationMode) var presentationMode
@@ -44,102 +42,3 @@ struct AuthenticationServerSelectionQRCodeScanner: View {
}
}
}
struct ScannerView: UIViewControllerRepresentable {
@Binding var qrCode: String
@Binding var scanCompleted: Bool
class Coordinator: NSObject, AVCaptureMetadataOutputObjectsDelegate {
var parent: ScannerView
init(_ parent: ScannerView) {
self.parent = parent
}
func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
if let metadataObject = metadataObjects.first {
guard let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject else { return }
guard let stringValue = readableObject.stringValue else { return }
AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))
DispatchQueue.main.async {
self.parent.qrCode = stringValue
self.parent.scanCompleted = true
}
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> some ScannerViewController {
let controller = ScannerViewController()
controller.delegate = context.coordinator
return controller
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
}
}
class ScannerViewController: UIViewController {
var captureSession: AVCaptureSession?
var previewLayer: AVCaptureVideoPreviewLayer!
let metadataOutput = AVCaptureMetadataOutput()
weak var delegate: AVCaptureMetadataOutputObjectsDelegate?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.black
captureSession = AVCaptureSession()
guard let videoCaptureDevice = AVCaptureDevice.default(for: .video), let videoInput = try? AVCaptureDeviceInput(device: videoCaptureDevice) else {
captureSession = nil
return
}
if let captureSession = captureSession {
guard captureSession.canAddInput(videoInput) && captureSession.canAddOutput(metadataOutput) else {
self.captureSession = nil
return
}
captureSession.addInput(videoInput)
captureSession.addOutput(metadataOutput)
metadataOutput.setMetadataObjectsDelegate(delegate, queue: .main)
metadataOutput.metadataObjectTypes = [.qr]
previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
previewLayer.frame = view.layer.bounds
previewLayer.videoGravity = .resizeAspectFill
view.layer.addSublayer(previewLayer)
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let dispatchQueue = DispatchQueue(label: "AVCapturesession.startRunning", qos: .background)
dispatchQueue.async{
if (self.captureSession?.isRunning == false) {
self.captureSession?.startRunning()
}
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
let dispatchQueue = DispatchQueue(label: "AVCapturesession.stopRunning", qos: .background)
dispatchQueue.async{
if (self.captureSession?.isRunning == true) {
self.captureSession?.stopRunning()
}
}
}
}

View File

@@ -0,0 +1,71 @@
//
/*
* Copyright (c) 2022 BWI GmbH
*
* 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 SwiftUI
import UIKit
class PermalinkQRCodeScannerController: NSObject {
@objc static func createFromSwiftUIView() -> UIViewController {
return UIHostingController(rootView: PermalinkQRCodeScanner())
}
}
struct PermalinkQRCodeScanner: View {
@Environment(\.presentationMode) var presentationMode
@State var qrCode: String = ""
@State var scanCompleted = false
@State var showInvalidCodeAlert = false
var body: some View {
NavigationView {
ScannerView(qrCode: $qrCode, scanCompleted: $scanCompleted)
.navigationTitle(BWIL10n.roomRecentsScanQrCode)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(VectorL10n.close) {
presentationMode.wrappedValue.dismiss()
}
}
}
.alert(isPresented: $showInvalidCodeAlert) {
Alert(
title: Text(BWIL10n.roomRecentsScanFailedTitle),
message: Text(BWIL10n.roomRecentsScanFailedMessage),
dismissButton: .default(Text(VectorL10n.ok)))
}
}
.onChange(of: scanCompleted) { newValue in
if newValue {
if !BWIBuildSettings.shared.clientPermalinkBaseUrl.isEmpty && qrCode.hasPrefix(BWIBuildSettings.shared.clientPermalinkBaseUrl) {
presentationMode.wrappedValue.dismiss()
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
if let url = URL(string: qrCode) {
UIApplication.shared.open(url)
}
}
} else {
showInvalidCodeAlert = true
}
}
}
}
}

View File

@@ -0,0 +1,120 @@
//
/*
* Copyright (c) 2022 BWI GmbH
*
* 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 SwiftUI
import UIKit
import AVFoundation
struct ScannerView: UIViewControllerRepresentable {
@Binding var qrCode: String
@Binding var scanCompleted: Bool
class Coordinator: NSObject, AVCaptureMetadataOutputObjectsDelegate {
var parent: ScannerView
init(_ parent: ScannerView) {
self.parent = parent
}
func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
if let metadataObject = metadataObjects.first {
guard let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject else { return }
guard let stringValue = readableObject.stringValue else { return }
AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))
DispatchQueue.main.async {
self.parent.qrCode = stringValue
self.parent.scanCompleted = true
}
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> some ScannerViewController {
let controller = ScannerViewController()
controller.delegate = context.coordinator
return controller
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
}
}
class ScannerViewController: UIViewController {
var captureSession: AVCaptureSession?
var previewLayer: AVCaptureVideoPreviewLayer!
let metadataOutput = AVCaptureMetadataOutput()
weak var delegate: AVCaptureMetadataOutputObjectsDelegate?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.black
captureSession = AVCaptureSession()
guard let videoCaptureDevice = AVCaptureDevice.default(for: .video), let videoInput = try? AVCaptureDeviceInput(device: videoCaptureDevice) else {
captureSession = nil
return
}
if let captureSession = captureSession {
guard captureSession.canAddInput(videoInput) && captureSession.canAddOutput(metadataOutput) else {
self.captureSession = nil
return
}
captureSession.addInput(videoInput)
captureSession.addOutput(metadataOutput)
metadataOutput.setMetadataObjectsDelegate(delegate, queue: .main)
metadataOutput.metadataObjectTypes = [.qr]
previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
previewLayer.frame = view.layer.bounds
previewLayer.videoGravity = .resizeAspectFill
view.layer.addSublayer(previewLayer)
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let dispatchQueue = DispatchQueue(label: "AVCapturesession.startRunning", qos: .background)
dispatchQueue.async{
if (self.captureSession?.isRunning == false) {
self.captureSession?.startRunning()
}
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
let dispatchQueue = DispatchQueue(label: "AVCapturesession.stopRunning", qos: .background)
dispatchQueue.async{
if (self.captureSession?.isRunning == true) {
self.captureSession?.stopRunning()
}
}
}
}