Commit 79a1aba7 authored by Melvin Keskin's avatar Melvin Keskin Committed by Linus Jahn
Browse files

Add page for scanning and sharing QR codes



Currently, the page can only add contacts by scanning QR codes which
contain XMPP URIs with their JIDs.

The chat page is opened when the scanned contact is already in the roster.
A passive notification is shown when the URI is invalid.
Co-authored-by: Mathis Brüchert's avatarMathis Brüchert <mbblp@protonmail.ch>
parent 18a3dd7c
Pipeline #78623 passed with stage
in 2 minutes and 33 seconds
......@@ -17,6 +17,7 @@ Copyright: 2016-2021, Linus Jahn <lnj@kaidan.im>
2019, Robert Maerkisch <zatroxde@protonmail.ch>
2020, Andrea Scarpino <scarpino@kde.org>
2019, caca hueto <cacahueto@olomono.de>
2021, Mathis Brüchert <mbblp@protonmail.ch>
2021, Carl Schwan <carl@carlschwan.eu>
2021, Aurélien Couderc <coucouf@coucouf.fr>
2020, Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
......@@ -163,6 +164,11 @@ Copyright: 2020, Jonah Brüchert <jbb@kaidan.im>
2020, Mathis Brüchert <mbblp@protonmail.ch>
License: CC-BY-SA-4.0
Files: data/images/qr-code-scan-1.svg
data/images/qr-code-scan-2.svg
Copyright: 2021, Mathis Brüchert <mbblp@protonmail.ch>
License: CC-BY-SA-4.0
Files: utils/convert-prl-libs-to-cmake.pl
Copyright: 2016, Konstantin Tokarev <annulen@yandex.ru>
License: MIT-Apple
......
......@@ -26,5 +26,7 @@
<file>mic1.svg</file>
<file>mic2.svg</file>
<file>mic3.svg</file>
<file>qr-code-scan-1.svg</file>
<file>qr-code-scan-2.svg</file>
</qresource>
</RCC>
This diff is collapsed.
This diff is collapsed.
......@@ -273,7 +273,8 @@ void AccountManager::deleteSettings()
m_settings->remove({
KAIDAN_SETTINGS_AUTH_ONLINE,
KAIDAN_SETTINGS_NOTIFICATIONS_MUTED,
KAIDAN_SETTINGS_FAVORITE_EMOJIS
KAIDAN_SETTINGS_FAVORITE_EMOJIS,
KAIDAN_SETTINGS_HELP_VISIBILITY_QR_CODE_PAGE,
});
}
......
......@@ -55,7 +55,7 @@ set(KAIDAN_SOURCES
src/RegistrationDataFormModel.cpp
src/ServerListModel.cpp
src/ServerListItem.cpp
src/Settings.cpp
src/Settings.cpp
src/AccountManager.cpp
src/VCardCache.cpp
......
......@@ -44,6 +44,7 @@
#define KAIDAN_SETTINGS_NOTIFICATIONS_MUTED "muted/"
#define KAIDAN_SETTINGS_FAVORITE_EMOJIS "emojis/favorites"
#define KAIDAN_SETTINGS_WINDOW_SIZE "window/size"
#define KAIDAN_SETTINGS_HELP_VISIBILITY_QR_CODE_PAGE "helpVisibility/qrCodePage"
#define KAIDAN_JID_RESOURCE_DEFAULT_PREFIX APPLICATION_DISPLAY_NAME
......
......@@ -46,6 +46,7 @@
#include "MessageDb.h"
#include "Notifications.h"
#include "RosterDb.h"
#include "RosterManager.h"
#include "Settings.h"
Kaidan *Kaidan::s_instance;
......
......@@ -60,6 +60,7 @@ class Kaidan : public QObject
Q_PROPERTY(PresenceCache* presenceCache READ presenceCache CONSTANT)
Q_PROPERTY(TransferCache* transferCache READ transferCache CONSTANT)
Q_PROPERTY(ServerFeaturesCache* serverFeaturesCache READ serverFeaturesCache CONSTANT)
Q_PROPERTY(Settings* settings READ settings CONSTANT)
Q_PROPERTY(quint8 connectionState READ connectionState NOTIFY connectionStateChanged)
Q_PROPERTY(QString connectionStateText READ connectionStateText NOTIFY connectionStateChanged)
Q_PROPERTY(quint8 connectionError READ connectionError NOTIFY connectionErrorChanged)
......@@ -190,7 +191,9 @@ public:
void addOpenUri(const QString &uri);
/**
* Connects to the server by the parsed credentials (bare JID and password) from a given XMPP URI (e.g. from scanning a QR code) like "xmpp:user@example.org?login;password=abc"
* Connects to the server by the parsed credentials (bare JID and
* password) from a given XMPP URI (e.g. from scanning a QR code) such
* as "xmpp:user@example.org?login;password=abc"
*
* The URI is used in the following cases.
*
......
......@@ -58,8 +58,9 @@ signals:
*
* @param nick A simple nick name for the new contact, which should be
* used to display in the roster.
* @param msg message presented to the added contact
*/
void addContactRequested(const QString &jid, const QString &nick, const QString &msg);
void addContactRequested(const QString &jid, const QString &nick = {}, const QString &msg = {});
/**
* Remove a contact from your roster
......@@ -74,7 +75,7 @@ signals:
void renameContactRequested(const QString &jid, const QString &newContactName);
public slots:
void addContact(const QString &jid, const QString &name, const QString &msg);
void addContact(const QString &jid, const QString &name = {}, const QString &msg = {});
void removeContact(const QString &jid);
void renameContact(const QString &jid, const QString &newContactName);
......
......@@ -36,7 +36,9 @@
#include "MessageDb.h"
#include "MessageModel.h"
#include "RosterDb.h"
#include "RosterManager.h"
#include "qxmpp-exts/QXmppUri.h"
#include <QXmppUtils.h>
RosterModel *RosterModel::s_instance = nullptr;
......@@ -142,6 +144,11 @@ QVariant RosterModel::data(const QModelIndex &index, int role) const
return {};
}
bool RosterModel::hasItem(const QString &jid) const
{
return findItem(jid).has_value();
}
std::optional<const RosterItem> RosterModel::findItem(const QString &jid) const
{
for (const auto &item : qAsConst(m_items)) {
......@@ -159,6 +166,29 @@ QString RosterModel::itemName(const QString &, const QString &jid) const
return {};
}
RosterModel::AddContactByUriResult RosterModel::addContactByUri(const QString &uriString)
{
if (QXmppUri::isXmppUri(uriString)) {
auto uri = QXmppUri(uriString);
auto jid = uri.jid();
if (jid.isEmpty()) {
return AddContactByUriResult::InvalidUri;
}
if (RosterModel::instance()->hasItem(jid)) {
Kaidan::instance()->openChatPageRequested(AccountManager::instance()->jid(), jid);
return AddContactByUriResult::ContactExists;
}
emit Kaidan::instance()->client()->rosterManager()->addContactRequested(jid);
return AddContactByUriResult::AddingContact;
}
return AddContactByUriResult::InvalidUri;
}
void RosterModel::handleItemsFetched(const QVector<RosterItem> &items)
{
beginResetModel();
......
......@@ -56,6 +56,16 @@ public:
LastMessageRole,
};
/**
* Result for adding a contact by an XMPP URI specifying how the URI is used
*/
enum AddContactByUriResult {
AddingContact, ///< The contact is being added to the roster.
ContactExists, ///< The contact is already in the roster.
InvalidUri ///< The URI cannot be used for contact addition.
};
Q_ENUM(AddContactByUriResult)
static RosterModel *instance();
RosterModel(QObject *parent = nullptr);
......@@ -67,6 +77,15 @@ public:
Q_REQUIRED_RESULT QHash<int, QByteArray> roleNames() const override;
Q_REQUIRED_RESULT QVariant data(const QModelIndex &index, int role) const override;
/**
* Returns whether this model has a roster item with the passed properties.
*
* @param jid JID of the roster item
*
* @return true if a roster item with the passed properties exists, otherwise false
*/
Q_INVOKABLE bool hasItem(const QString &jid) const;
/**
* Retrieves the name of a roster item or its JID's local part.
*
......@@ -79,6 +98,14 @@ public:
*/
Q_INVOKABLE QString itemName(const QString &accountJid, const QString &jid) const;
/**
* Adds a contact (bare JID) by a given XMPP URI (e.g., from a scanned QR
* code) such as "xmpp:user@example.org".
*
* @param uriString XMPP URI string that contains only a JID
*/
Q_INVOKABLE AddContactByUriResult addContactByUri(const QString &uriString);
signals:
void addItemRequested(const RosterItem &item);
void removeItemRequested(const QString &jid);
......
......@@ -134,6 +134,17 @@ void Settings::setAuthPasswordVisibility(Kaidan::PasswordVisibility visibility)
emit authPasswordVisibilityChanged();
}
bool Settings::qrCodePageExplanationVisible() const
{
return m_settings.value(KAIDAN_SETTINGS_HELP_VISIBILITY_QR_CODE_PAGE, true).toBool();
}
void Settings::setQrCodePageExplanationVisible(bool isVisible)
{
m_settings.setValue(KAIDAN_SETTINGS_HELP_VISIBILITY_QR_CODE_PAGE, isVisible);
emit qrCodePageExplanationVisibleChanged();
}
bool Settings::notificationsMuted(const QString &bareJid) const
{
return m_settings.value(QStringLiteral(KAIDAN_SETTINGS_NOTIFICATIONS_MUTED) + bareJid, false).toBool();
......
......@@ -44,6 +44,8 @@ class Settings : public QObject
{
Q_OBJECT
Q_PROPERTY(bool qrCodePageExplanationVisible READ qrCodePageExplanationVisible WRITE setQrCodePageExplanationVisible NOTIFY qrCodePageExplanationVisibleChanged)
public:
explicit Settings(QObject *parent = nullptr);
......@@ -77,6 +79,20 @@ public:
Kaidan::PasswordVisibility authPasswordVisibility() const;
void setAuthPasswordVisibility(Kaidan::PasswordVisibility visibility);
/**
* Retrieves the visibility of the QrCodePage's explanation from the settings file.
*
* @return true if the explanation is set to be visible, otherwise false
*/
bool qrCodePageExplanationVisible() const;
/**
* Stores the visibility of the QrCodePage's explanation in the settings file.
*
* @param isVisible true if the explanation should be visible in the future, otherwise false
*/
void setQrCodePageExplanationVisible(bool isVisible);
bool notificationsMuted(const QString &bareJid) const;
void setNotificationsMuted(const QString &bareJid, bool muted);
......@@ -96,6 +112,7 @@ signals:
void authHostChanged();
void authPortChanged();
void authPasswordVisibilityChanged();
void qrCodePageExplanationVisibleChanged();
void notificationsMutedChanged(const QString &bareJid);
void favoriteEmojisChanged();
void windowSizeChanged();
......
......@@ -90,8 +90,7 @@ ExplainedContentPage {
}
content: Item {
Layout.fillHeight: true
Layout.fillWidth: true
anchors.fill: parent
QrCode {
id: qrCode
......
......@@ -85,6 +85,11 @@ Kirigami.GlobalDrawer {
]
actions: [
Kirigami.Action {
text: qsTr("Scan QR codes")
icon.name: "view-barcode-qr"
onTriggered: pageStack.layers.push(qrCodePage)
},
Kirigami.Action {
text: qsTr("Invite friends")
icon.name: "mail-invitation"
......
......@@ -75,8 +75,7 @@ ExplanationTogglePage {
content: QrCodeScanner {
id: scanner
Layout.fillWidth: true
Layout.fillHeight: true
anchors.fill: parent
Item {
anchors.centerIn: parent
......
/*
* Kaidan - A user-friendly XMPP client for every device!
*
* Copyright (C) 2016-2021 Kaidan developers and contributors
* (see the LICENSE file for a full list of copyright authors)
*
* Kaidan is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* In addition, as a special exception, the author of Kaidan gives
* permission to link the code of its release with the OpenSSL
* project's "OpenSSL" library (or with modified versions of it that
* use the same license as the "OpenSSL" library), and distribute the
* linked executables. You must obey the GNU General Public License in
* all respects for all of the code used other than "OpenSSL". If you
* modify this file, you may extend this exception to your version of
* the file, but you are not obligated to do so. If you do not wish to
* do so, delete this exception statement from your version.
*
* Kaidan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kaidan. If not, see <http://www.gnu.org/licenses/>.
*/
import QtQuick 2.12
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.14 as Controls
import org.kde.kirigami 2.12 as Kirigami
import im.kaidan.kaidan 1.0
import "elements"
/**
* This page is used for scanning QR codes of contacts and generating an own
* QR code which can be scanned by contacts.
*/
ExplanationTogglePage {
title: qsTr("Scan QR codes")
explanationArea.visible: Kaidan.settings.qrCodePageExplanationVisible
explanationToggleButton.text: explanationToggleButton.checked ? qsTr("Show explanation") : qsTr("Scan QR codes")
explanationToggleButton.checked: !Kaidan.settings.qrCodePageExplanationVisible
explanationToggleButton.onClicked: {
if (Kaidan.settings.qrCodePageExplanationVisible) {
// Hide the explanation when this page is opened again in the future.
Kaidan.settings.qrCodePageExplanationVisible = false
if (!scanner.cameraEnabled) {
scanner.camera.start()
scanner.cameraEnabled = true
}
}
}
secondaryButton.visible: false
explanation: GridLayout {
flow: applicationWindow().wideScreen ? GridLayout.LeftToRight : GridLayout.TopToBottom
ColumnLayout {
CenteredAdaptiveText {
text: qsTr("Step 1: Scan your <b>contact's</b> QR code")
scaleFactor: 1.5
}
Image {
source: Utils.getResourcePath("images/qr-code-scan-1.svg")
sourceSize: Qt.size(860, 860)
fillMode: Image.PreserveAspectFit
mipmap: true
Layout.fillHeight: true
Layout.fillWidth: true
}
}
Kirigami.Separator {
Layout.fillWidth: applicationWindow().wideScreen ? false : true
Layout.fillHeight: !Layout.fillWidth
Layout.topMargin: applicationWindow().wideScreen ? parent.height * 0.1 : parent.height * 0.01
Layout.bottomMargin: Layout.topMargin
Layout.leftMargin: applicationWindow().wideScreen ? parent.width * 0.01 : parent.width * 0.1
Layout.rightMargin: Layout.leftMargin
}
ColumnLayout {
CenteredAdaptiveText {
text: qsTr("Step 2: Let your contact scan <b>your</b> QR code")
scaleFactor: 1.5
}
Image {
source: Utils.getResourcePath("images/qr-code-scan-2.svg")
sourceSize: Qt.size(860, 860)
fillMode: Image.PreserveAspectFit
Layout.fillHeight: true
Layout.fillWidth: true
}
}
}
content: GridLayout {
anchors.centerIn: parent
flow: applicationWindow().wideScreen ? GridLayout.LeftToRight : GridLayout.TopToBottom
visible: !Kaidan.settings.qrCodePageExplanationVisible
width: applicationWindow().wideScreen ? parent.width : Math.min(largeButtonWidth, parent.width, parent.height * 0.48)
height: applicationWindow().wideScreen ? Math.min(parent.height, parent.width * 0.48) : parent.height
QrCodeScanner {
id: scanner
Layout.preferredWidth: applicationWindow().wideScreen ? parent.width * 0.48 : parent.width
Layout.preferredHeight: applicationWindow().wideScreen ? parent.height : Layout.preferredWidth
filter.onScanningSucceeded: {
if (isAcceptingResult) {
isBusy = true
// Try to add a contact by the data from the decoded QR code.
switch (RosterModel.addContactByUri(result)) {
case RosterModel.AddingContact:
showPassiveNotification(qsTr("Contact added - Continue with step 2!"), Kirigami.Units.veryLongDuration * 4)
break
case RosterModel.ContactExists:
break
case RosterModel.InvalidUri:
showPassiveNotification(qsTr("This QR code does not contain a contact."), Kirigami.Units.veryLongDuration * 4)
}
isBusy = false
isAcceptingResult = false
resetAcceptResultTimer.start()
}
}
property bool isAcceptingResult: true
property bool isBusy: false
// timer to accept the result again after an invalid URI was scanned
Timer {
id: resetAcceptResultTimer
interval: Kirigami.Units.veryLongDuration * 4
onTriggered: scanner.isAcceptingResult = true
}
Item {
anchors.centerIn: parent
// background of loadingArea
Rectangle {
anchors.fill: loadingArea
anchors.margins: -8
radius: roundedCornersRadius
color: Kirigami.Theme.backgroundColor
opacity: 0.9
visible: loadingArea.visible
}
ColumnLayout {
id: loadingArea
anchors.centerIn: parent
visible: scanner.isBusy
Controls.BusyIndicator {
Layout.alignment: Qt.AlignHCenter
}
Controls.Label {
text: "<i>" + qsTr("Adding contact…") + "</i>"
color: Kirigami.Theme.textColor
}
}
}
}
Kirigami.Separator {
Layout.fillWidth: !applicationWindow().wideScreen
Layout.fillHeight: !Layout.fillWidth
Layout.topMargin: applicationWindow().wideScreen ? parent.height * 0.1 : parent.height * 0.01
Layout.bottomMargin: Layout.topMargin
Layout.leftMargin: applicationWindow().wideScreen ? parent.width * 0.01 : parent.width * 0.1
Layout.rightMargin: Layout.leftMargin
}
QrCode {
jid: AccountManager.jid
Layout.preferredWidth: applicationWindow().wideScreen ? parent.width * 0.48 : parent.width
Layout.preferredHeight: applicationWindow().wideScreen ? parent.height : Layout.preferredWidth
}
}
Component.onCompleted: {
if (!Kaidan.settings.qrCodePageExplanationVisible) {
scanner.camera.start()
scanner.cameraEnabled = true
}
}
}
......@@ -150,6 +150,7 @@ Kirigami.ScrollablePage {
while (pageStack.depth > 1)
pageStack.pop()
popLayersAboveLowest()
pageStack.push(chatPage)
}
}
......@@ -76,53 +76,49 @@ Kirigami.Page {
property bool useMarginsForContent: true
Item {
id: contentArea
anchors.fill: parent
anchors.margins: useMarginsForContent ? 20 : 0
anchors.bottomMargin: useMarginsForContent ? parent.height - buttonArea.y : 0
}
GridLayout {
id: contentArea
anchors.fill: parent
anchors.margins: useMarginsForContent ? 20 : 0
anchors.bottomMargin: useMarginsForContent ? parent.height - buttonArea.y : 0
}
// background of overlay
Rectangle {
z: 1
anchors.fill: overlay
anchors.margins: -8
radius: roundedCornersRadius
color: Kirigami.Theme.backgroundColor
opacity: 0.90
visible: explanationArea.visible
}
ColumnLayout {
id: overlay
z: 2
anchors.margins: 18
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
// background of overlay
Rectangle {
z: 1
anchors.fill: overlay
anchors.margins: -8
radius: roundedCornersRadius
color: Kirigami.Theme.backgroundColor
opacity: 0.90
visible: explanationArea.visible
GridLayout {
id: explanationArea
Layout.fillWidth: true
Layout.fillHeight: true
}
ColumnLayout {
id: overlay
z: 2
anchors.margins: 18
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
GridLayout {
id: explanationArea
Layout.fillWidth: true
Layout.fillHeight: true
}
id: buttonArea
Layout.alignment: Qt.AlignBottom | Qt.AlignHCenter
Layout.maximumWidth: largeButtonWidth
ColumnLayout {
id: buttonArea
Layout.alignment: Qt.AlignBottom | Qt.AlignHCenter
Layout.maximumWidth: largeButtonWidth
CenteredAdaptiveHighlightedButton {
id: primaryButton
}
CenteredAdaptiveHighlightedButton {
id: primaryButton
}
CenteredAdaptiveButton {
id: secondaryButton
}
CenteredAdaptiveButton {
id: secondaryButton
}
}
}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment