Improve first-run UX

- Replace LoginPage with step-by-step approach to support different login flows
- Implement login using SSO
This commit is contained in:
Tobias Fella
2021-02-07 21:23:31 +00:00
parent e7bada4cde
commit 464c48540e
21 changed files with 779 additions and 159 deletions

View File

@@ -0,0 +1,64 @@
/**
* SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import QtQuick 2.14
import QtQuick.Controls 2.14 as QQC2
import QtQuick.Layouts 1.14
import org.kde.kirigami 2.12 as Kirigami
import org.kde.neochat 1.0
LoginStep {
id: root
readonly property var homeserver: customHomeserver.visible ? customHomeserver.text : serverCombo.currentText
property bool loading: false
title: i18n("@title", "Select a Homeserver")
action: Kirigami.Action {
enabled: LoginHelper.homeserverReachable && !customHomeserver.visible || customHomeserver.acceptableInput
onTriggered: {
// TODO
console.log("register todo")
}
}
onHomeserverChanged: {
LoginHelper.testHomeserver("@user:" + homeserver)
}
Kirigami.FormLayout {
Component.onCompleted: Controller.testHomeserver(homeserver)
QQC2.ComboBox {
id: serverCombo
Kirigami.FormData.label: i18n("Homeserver:")
model: ["matrix.org", "kde.org", "tchncs.de", i18n("Other...")]
}
QQC2.TextField {
id: customHomeserver
Kirigami.FormData.label: i18n("Url:")
visible: serverCombo.currentIndex === 3
onTextChanged: {
Controller.testHomeserver(text)
}
validator: RegularExpressionValidator {
regularExpression: /([a-zA-Z0-9\-]+\.)*[a-zA-Z0-9]+(:[0-9]+)?/
}
}
QQC2.Button {
id: continueButton
text: i18nc("@action:button", "Continue")
action: root.action
}
}
}

View File

@@ -0,0 +1,23 @@
/**
* SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
*
* SPDX-License-Identifier: GPL-3.0-only
*/
import QtQuick 2.15
import QtQuick.Controls 2.12 as QQC2
import QtQuick.Layouts 1.12
import org.kde.neochat 1.0
import NeoChat.Component 1.0
import org.kde.kirigami 2.12 as Kirigami
QQC2.BusyIndicator {
property var showContinueButton: false
property var showBackButton: false
property string title: i18n("Loading")
anchors.centerIn: parent
}

View File

@@ -0,0 +1,68 @@
/**
* SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
* SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import QtQuick 2.15
import QtQuick.Controls 2.12 as QQC2
import QtQuick.Layouts 1.12
import org.kde.kirigami 2.12 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Component 1.0
LoginStep {
id: login
showContinueButton: true
showBackButton: false
title: i18nc("@title", "Login")
message: i18n("Enter your Matrix ID")
Component.onCompleted: {
LoginHelper.matrixId = ""
}
Kirigami.FormLayout {
QQC2.TextField {
id: matrixIdField
Kirigami.FormData.label: i18n("Matrix ID:")
placeholderText: "@user:matrix.org"
onTextChanged: {
if(acceptableInput) {
LoginHelper.matrixId = text
}
}
Component.onCompleted: {
matrixIdField.forceActiveFocus()
}
Keys.onReturnPressed: {
login.action.trigger()
}
validator: RegularExpressionValidator {
regularExpression: /^\@?[a-zA-Z0-9\._=\-/]+\:[a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]+)*\.[a-zA-Z]+(:[0-9]+)?$/
}
}
}
action: Kirigami.Action {
text: LoginHelper.testing && matrixIdField.acceptableInput ? i18n("Loading") : i18nc("@action:button", "Continue")
onTriggered: {
if (LoginHelper.supportsSso && LoginHelper.supportsPassword) {
processed("qrc:/imports/NeoChat/Component/Login/LoginMethod.qml");
} else if (LoginHelper.supportsPassword) {
processed("qrc:/imports/NeoChat/Component/Login/Password.qml");
} else {
processed("qrc:/imports/NeoChat/Component/Login/Sso.qml");
}
}
enabled: LoginHelper.homeserverReachable
}
}

View File

@@ -0,0 +1,35 @@
/**
* SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import QtQuick 2.14
import QtQuick.Controls 2.14 as Controls
import QtQuick.Layouts 1.14
import org.kde.kirigami 2.12 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Component.Login 1.0
LoginStep {
id: loginMethod
title: i18n("Login Methods")
Layout.alignment: Qt.AlignHCenter
Controls.Button {
Layout.alignment: Qt.AlignHCenter
text: i18n("Login with password")
Layout.preferredWidth: Kirigami.Units.gridUnit * 12
onClicked: processed("qrc:/imports/NeoChat/Component/Login/Password.qml")
}
Controls.Button {
Layout.alignment: Qt.AlignHCenter
text: i18n("Login with single sign-on")
Layout.preferredWidth: Kirigami.Units.gridUnit * 12
onClicked: processed("qrc:/imports/NeoChat/Component/Login/Sso.qml")
}
}

View File

@@ -0,0 +1,35 @@
/**
* SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import QtQuick 2.14
import QtQuick.Controls 2.14 as Controls
import QtQuick.Layouts 1.14
import org.kde.kirigami 2.12 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Component.Login 1.0
LoginStep {
id: loginRegister
Layout.alignment: Qt.AlignHCenter
Controls.Button {
Layout.alignment: Qt.AlignHCenter
text: i18n("Login")
Layout.preferredWidth: Kirigami.Units.gridUnit * 12
onClicked: processed("qrc:/imports/NeoChat/Component/Login/Login.qml")
}
Controls.Button {
Layout.alignment: Qt.AlignHCenter
text: i18n("Register")
Layout.preferredWidth: Kirigami.Units.gridUnit * 12
onClicked: processed("qrc:/imports/NeoChat/Component/Login/Homeserver.qml")
}
}

View File

@@ -0,0 +1,28 @@
// SPDX-FileCopyrightText: 2020 Carl Schwan <carlschwan@kde.org>
//
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14
/// Step for the login/registration flow
ColumnLayout {
property string title: i18n("Welcome")
property string message: i18n("Welcome")
property bool showContinueButton: false
property bool showBackButton: false
property bool acceptable: false
property string previousUrl: ""
/// Process this module, this is called by the continue button.
/// Should call \sa processed when it finish successfully.
property Action action: null
/// Called when switching to the next step.
signal processed(url nextUrl)
signal showMessage(string message)
}

View File

@@ -0,0 +1,56 @@
/**
* SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
*
* SPDX-License-Identifier: GPL-3.0-only
*/
import QtQuick 2.15
import QtQuick.Controls 2.12 as QQC2
import QtQuick.Layouts 1.12
import org.kde.kirigami 2.12 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Component 1.0
LoginStep {
id: password
title: i18nc("@title", "Password")
message: i18n("Enter your password")
showContinueButton: true
showBackButton: true
previousUrl: LoginHelper.isLoggingIn ? "" : LoginHelper.supportsSso ? "qrc:/imports/NeoChat/Component/Login/LoginMethod.qml" : "qrc:/imports/NeoChat/Component/Login/Login.qml"
action: Kirigami.Action {
text: i18nc("@action:button", "Login")
enabled: passwordField.text.length > 0 && !LoginHelper.isLoggingIn
onTriggered: {
LoginHelper.login();
}
}
Connections {
target: LoginHelper
function onConnected() {
processed("qrc:/imports/NeoChat/Component/Login/Loading.qml")
}
}
Kirigami.FormLayout {
Kirigami.PasswordField {
id: passwordField
onTextChanged: LoginHelper.password = text
enabled: !LoginHelper.isLoggingIn
Component.onCompleted: {
passwordField.forceActiveFocus()
}
Keys.onReturnPressed: {
password.action.trigger()
}
}
}
}

View File

@@ -0,0 +1,42 @@
/**
* SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
*
* SPDX-License-Identifier: GPL-3.0-only
*/
import QtQuick 2.15
import QtQuick.Controls 2.12 as QQC2
import QtQuick.Layouts 1.12
import org.kde.neochat 1.0
import NeoChat.Component 1.0
import org.kde.kirigami 2.12 as Kirigami
LoginStep {
id: root
title: i18nc("@title", "Login")
message: i18n("Login with single sign-on")
Kirigami.FormLayout {
Connections {
target: LoginHelper
onSsoUrlChanged: {
Qt.openUrlExternally(LoginHelper.ssoUrl)
}
onConnected: proccessed("qrc:/imports/NeoChat/Component/Login/Loading.qml")
}
QQC2.Button {
text: i18n("Login")
onClicked: {
LoginHelper.loginWithSso()
root.showMessage(i18n("Complete the authentification steps in your browser"))
}
Component.onCompleted: forceActiveFocus()
Keys.onReturnPressed: clicked()
}
}
}

View File

@@ -0,0 +1,7 @@
module NeoChat.Component.Login
Login 1.0 Login.qml
Password 1.0 Password.qml
LoginRegister 1.0 LoginRegister.qml
Loading 1.0 Loading.qml
LoginMethod 1.0 LoginMethod.qml
LoginStep 1.0 LoginStep.qml

View File

@@ -77,7 +77,7 @@ Kirigami.ScrollablePage {
actions.main: Kirigami.Action {
text: i18n("Add an account")
iconName: "list-add-user"
onTriggered: pageStack.layers.push("qrc:/imports/NeoChat/Page/LoginPage.qml")
onTriggered: pageStack.layers.push("qrc:/imports/NeoChat/Page/WelcomePage.qml")
}
Kirigami.OverlaySheet {

View File

@@ -1,97 +0,0 @@
/**
* SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
* SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
*
* SPDX-License-Identifier: GPL-3.0-only
*/
import QtQuick 2.12
import QtQuick.Controls 2.12 as QQC2
import QtQuick.Layouts 1.12
import org.kde.neochat 1.0
import NeoChat.Component 1.0
import org.kde.kirigami 2.12 as Kirigami
Kirigami.ScrollablePage {
id: root
title: i18n("Login")
header: QQC2.Control {
padding: Kirigami.Units.smallSpacing
contentItem: Kirigami.InlineMessage {
id: inlineMessage
visible: false
showCloseButton: true
}
}
Kirigami.FormLayout {
id: formLayout
QQC2.TextField {
id: serverField
Kirigami.FormData.label: i18n("Server Address")
text: "https://matrix.org"
onAccepted: usernameField.forceActiveFocus()
}
QQC2.TextField {
id: usernameField
Kirigami.FormData.label: i18n("Username")
onAccepted: passwordField.forceActiveFocus()
}
Kirigami.PasswordField {
id: passwordField
Kirigami.FormData.label: i18n("Password")
onAccepted: accessTokenField.forceActiveFocus()
}
QQC2.TextField {
id: accessTokenField
Kirigami.FormData.label: i18n("Access Token (Optional)")
onAccepted: deviceNameField.forceActiveFocus()
}
QQC2.TextField {
id: deviceNameField
Kirigami.FormData.label: i18n("Device Name (Optional)")
onAccepted: doLogin()
}
RowLayout {
QQC2.Button {
visible: Controller.accountCount > 0
text: i18n("Cancel")
onClicked: {
pageStack.layers.clear();
}
}
QQC2.Button {
text: i18n("Login")
onClicked: doLogin()
}
}
Connections {
target: Controller
function onErrorOccured(error, detail) {
inlineMessage.type = Kirigami.MessageType.Error;
if (detail && detail.length !== 0) {
inlineMessage.text = i18n("%1: %2", error, detail);
} else {
inlineMessage.text = error;
}
inlineMessage.visible = true;
}
}
}
function doLogin() {
inlineMessage.text = i18n("Loading, this might take up to 10 seconds.");
inlineMessage.type = Kirigami.MessageType.Information
inlineMessage.visible = true;
if (accessTokenField.text.length > 0) {
Controller.loginWithAccessToken(serverField.text.trim(), usernameField.text.trim(), accessTokenField.text, deviceNameField.text.trim());
} else {
Controller.loginWithCredentials(serverField.text.trim(), usernameField.text.trim(), passwordField.text, deviceNameField.text.trim());
}
}
}

View File

@@ -0,0 +1,101 @@
/**
* SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import QtQuick 2.14
import QtQuick.Controls 2.14 as Controls
import QtQuick.Layouts 1.14
import org.kde.kirigami 2.12 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Component.Login 1.0
Kirigami.ScrollablePage {
id: welcomePage
property alias currentStep: module.item
title: module.item.title ?? i18n("Welcome")
header: Controls.Control {
contentItem: Kirigami.InlineMessage {
id: headerMessage
type: Kirigami.MessageType.Error
showCloseButton: true
visible: false
}
}
Component.onCompleted: LoginHelper.init()
Connections {
target: LoginHelper
onErrorOccured: {
headerMessage.text = message;
headerMessage.visible = true;
headerMessage.type = Kirigami.MessageType.Error;
}
}
ColumnLayout {
Kirigami.Icon {
source: "org.kde.neochat"
Layout.fillWidth: true
Layout.preferredHeight: Kirigami.Units.gridUnit * 16
}
Controls.Label {
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
font.pixelSize: 25
text: module.item.message ?? module.item.title ?? i18n("Welcome to Matrix")
}
Loader {
id: module
Layout.alignment: Qt.AlignHCenter
source: "qrc:/imports/NeoChat/Component/Login/Login.qml"
onSourceChanged: {
headerMessage.visible = false
headerMessage.text = ""
}
}
RowLayout {
Layout.alignment: Qt.AlignHCenter
Controls.Button {
text: i18nc("@action:button", "Back")
enabled: welcomePage.currentStep.previousUrl !== ""
visible: welcomePage.currentStep.showBackButton
Layout.alignment: Qt.AlignHCenter
onClicked: {
module.source = welcomePage.currentStep.previousUrl
}
}
Controls.Button {
id: continueButton
enabled: welcomePage.currentStep.acceptable
visible: welcomePage.currentStep.showContinueButton
action: welcomePage.currentStep.action
}
}
Connections {
target: currentStep
function onProcessed(nextUrl) {
module.source = nextUrl;
}
function onShowMessage(message) {
headerMessage.text = message;
headerMessage.visible = true;
headerMessage.type = Kirigami.MessageType.Information;
}
}
}
}

View File

@@ -1,6 +1,5 @@
module NeoChat.Page
LoadingPage 1.0 LoadingPage.qml
LoginPage 1.0 LoginPage.qml
RoomListPage 1.0 RoomListPage.qml
RoomPage 1.0 RoomPage.qml
RoomWindow 1.0 RoomWindow.qml

View File

@@ -246,6 +246,12 @@ Kirigami.ApplicationWindow {
activeConnection: Controller.activeConnection
}
}
Connections {
target: LoginHelper
function onInitialSyncFinished() {
roomManager.roomList = pageStack.replace(roomListComponent);
}
}
Connections {
target: Controller
@@ -255,23 +261,13 @@ Kirigami.ApplicationWindow {
return;
}
if (Controller.accountCount === 0) {
pageStack.replace("qrc:/imports/NeoChat/Page/LoginPage.qml", {});
pageStack.replace("qrc:/imports/NeoChat/Page/WelcomePage.qml", {});
} else {
roomManager.roomList = pageStack.replace(roomListComponent, {'activeConnection': Controller.activeConnection});
roomManager.loadInitialRoom();
}
}
function onConnectionAdded() {
if (Controller.accountCount === 1) {
if (Controller.busy) {
pageStack.replace("qrc:/imports/NeoChat/Page/LoadingPage.qml");
} else {
roomManager.roomList = pageStack.replace(roomListComponent);
}
}
}
function onBusyChanged() {
if(!Controller.busy && roomManager.roomList === null) {
roomManager.roomList = pageStack.replace(roomListComponent);
@@ -286,7 +282,7 @@ Kirigami.ApplicationWindow {
function onConnectionDropped() {
if (Controller.accountCount === 0) {
pageStack.clear();
pageStack.replace("qrc:/imports/NeoChat/Page/LoginPage.qml");
pageStack.replace("qrc:/imports/NeoChat/Page/WelcomePage.qml");
}
}

11
res.qrc
View File

@@ -2,7 +2,6 @@
<qresource prefix="/">
<file>qml/main.qml</file>
<file>imports/NeoChat/Page/qmldir</file>
<file>imports/NeoChat/Page/LoginPage.qml</file>
<file>imports/NeoChat/Page/LoadingPage.qml</file>
<file>imports/NeoChat/Page/RoomListPage.qml</file>
<file>imports/NeoChat/Page/RoomPage.qml</file>
@@ -14,6 +13,7 @@
<file>imports/NeoChat/Page/StartChatPage.qml</file>
<file>imports/NeoChat/Page/ImageEditorPage.qml</file>
<file>imports/NeoChat/Page/DevicesPage.qml</file>
<file>imports/NeoChat/Page/WelcomePage.qml</file>
<file>imports/NeoChat/Component/qmldir</file>
<file>imports/NeoChat/Component/ChatTextInput.qml</file>
<file>imports/NeoChat/Component/AutoMouseArea.qml</file>
@@ -32,6 +32,15 @@
<file>imports/NeoChat/Component/Timeline/AudioDelegate.qml</file>
<file>imports/NeoChat/Component/Timeline/FileDelegate.qml</file>
<file>imports/NeoChat/Component/Timeline/ImageDelegate.qml</file>
<file>imports/NeoChat/Component/Login/qmldir</file>
<file>imports/NeoChat/Component/Login/LoginStep.qml</file>
<file>imports/NeoChat/Component/Login/Login.qml</file>
<file>imports/NeoChat/Component/Login/Password.qml</file>
<file>imports/NeoChat/Component/Login/LoginRegister.qml</file>
<file>imports/NeoChat/Component/Login/Loading.qml</file>
<file>imports/NeoChat/Component/Login/Homeserver.qml</file>
<file>imports/NeoChat/Component/Login/LoginMethod.qml</file>
<file>imports/NeoChat/Component/Login/Sso.qml</file>
<file>imports/NeoChat/Setting/Setting.qml</file>
<file>imports/NeoChat/Setting/qmldir</file>
<file>imports/NeoChat/Setting/Palette.qml</file>

View File

@@ -20,6 +20,7 @@ add_executable(neochat
chatdocumenthandler.cpp
devicesmodel.cpp
filetypesingleton.cpp
login.cpp
../res.qrc
)

View File

@@ -91,50 +91,6 @@ inline QString accessTokenFileName(const AccountSettings &account)
return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + '/' + fileName;
}
void Controller::loginWithCredentials(const QString &serverAddr, const QString &user, const QString &pass, QString deviceName)
{
if (user.isEmpty() || pass.isEmpty()) {
return;
}
if (deviceName.isEmpty()) {
deviceName = "NeoChat " + QSysInfo::machineHostName() + " " + QSysInfo::productType() + " " + QSysInfo::productVersion() + " " + QSysInfo::currentCpuArchitecture();
}
auto conn = new Connection(this);
const QUrl serverUrl = QUrl::fromUserInput(serverAddr);
// we are using a fake mixd since resolveServer just set the homeserver url :sigh:
conn->resolveServer("@username:" + serverUrl.host() + ":" + QString::number(serverUrl.port(443)));
connect(conn, &Connection::loginFlowsChanged, this, [this, user, conn, pass, deviceName]() {
conn->loginWithPassword(user, pass, deviceName, "");
connect(conn, &Connection::connected, this, [this, conn, deviceName] {
AccountSettings account(conn->userId());
account.setKeepLoggedIn(true);
account.clearAccessToken(); // Drop the legacy - just in case
account.setHomeserver(conn->homeserver());
account.setDeviceId(conn->deviceId());
account.setDeviceName(deviceName);
if (!saveAccessTokenToKeyChain(account, conn->accessToken())) {
qWarning() << "Couldn't save access token";
}
account.sync();
addConnection(conn);
setActiveConnection(conn);
});
connect(conn, &Connection::networkError, [=](QString error, const QString &, int, int) {
Q_EMIT globalErrorOccured(i18n("Network Error"), std::move(error));
});
connect(conn, &Connection::loginError, [=](QString error, const QString &) {
Q_EMIT errorOccured(i18n("Login Failed"), std::move(error));
});
});
connect(conn, &Connection::resolveError, this, [=](QString error) {
Q_EMIT globalErrorOccured(i18n("Network Error"), std::move(error));
});
}
void Controller::loginWithAccessToken(const QString &serverAddr, const QString &user, const QString &token, const QString &deviceName)
{
if (user.isEmpty() || token.isEmpty()) {
@@ -551,4 +507,5 @@ NeochatDeleteDeviceJob::NeochatDeleteDeviceJob(const QString &deviceId, const Om
QJsonObject _data;
addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth);
setRequestData(std::move(_data));
}

View File

@@ -45,7 +45,6 @@ public:
void addConnection(Connection *c);
void dropConnection(Connection *c);
Q_INVOKABLE void loginWithCredentials(const QString &, const QString &, const QString &, QString);
Q_INVOKABLE void loginWithAccessToken(const QString &, const QString &, const QString &, const QString &);
Q_INVOKABLE void changePassword(Quotient::Connection *connection, const QString &currentPassword, const QString &newPassword);
@@ -61,6 +60,9 @@ public:
void setAboutData(const KAboutData &aboutData);
[[nodiscard]] KAboutData aboutData() const;
bool saveAccessTokenToFile(const AccountSettings &account, const QByteArray &accessToken);
bool saveAccessTokenToKeyChain(const AccountSettings &account, const QByteArray &accessToken);
enum PasswordStatus {
Success,
Wrong,
@@ -79,8 +81,6 @@ private:
static QByteArray loadAccessTokenFromFile(const AccountSettings &account);
QByteArray loadAccessTokenFromKeyChain(const AccountSettings &account);
bool saveAccessTokenToFile(const AccountSettings &account, const QByteArray &accessToken);
bool saveAccessTokenToKeyChain(const AccountSettings &account, const QByteArray &accessToken);
void loadSettings();
void saveSettings() const;
@@ -110,6 +110,7 @@ Q_SIGNALS:
void showWindow();
void openRoom(NeoChatRoom *room);
void userConsentRequired(QUrl url);
void testConnectionResult(const QString &connection, bool usable);
public Q_SLOTS:
void logout(Quotient::Connection *conn, bool serverSideLogout);

206
src/login.cpp Normal file
View File

@@ -0,0 +1,206 @@
/**
* SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "login.h"
#include "connection.h"
#include "controller.h"
#include <QUrl>
#include <KLocalizedString>
Login::Login(QObject *parent)
: QObject(parent)
{
init();
}
void Login::init()
{
m_homeserverReachable = false;
m_connection = nullptr;
m_matrixId = QString();
m_password = QString();
m_deviceName = QString();
m_supportsSso = false;
m_supportsPassword = false;
m_ssoUrl = QUrl();
connect(this, &Login::matrixIdChanged, this, [=](){
setHomeserverReachable(false);
if (m_connection) {
delete m_connection;
m_connection = nullptr;
}
if(m_matrixId == "@") {
return;
}
m_testing = true;
Q_EMIT testingChanged();
m_connection = new Connection(this);
m_connection->resolveServer(m_matrixId);
connect(m_connection, &Connection::loginFlowsChanged, this, [=](){
setHomeserverReachable(true);
m_testing = false;
Q_EMIT testingChanged();
m_supportsSso = m_connection->supportsSso();
m_supportsPassword = m_connection->supportsPasswordAuth();
Q_EMIT loginFlowsChanged();
});
});
}
void Login::setHomeserverReachable(bool reachable)
{
m_homeserverReachable = reachable;
Q_EMIT homeserverReachableChanged();
}
bool Login::homeserverReachable() const
{
return m_homeserverReachable;
}
QString Login::matrixId() const
{
return m_matrixId;
}
void Login::setMatrixId(const QString &matrixId)
{
m_matrixId = matrixId;
if(!m_matrixId.startsWith('@')) {
m_matrixId.prepend('@');
}
Q_EMIT matrixIdChanged();
}
QString Login::password() const
{
return m_password;
}
void Login::setPassword(const QString &password)
{
m_password = password;
Q_EMIT passwordChanged();
}
QString Login::deviceName() const
{
return m_deviceName;
}
void Login::setDeviceName(const QString &deviceName)
{
m_deviceName = deviceName;
Q_EMIT deviceNameChanged();
}
void Login::login()
{
m_isLoggingIn = true;
Q_EMIT isLoggingInChanged();
setDeviceName("NeoChat " + QSysInfo::machineHostName() + " " + QSysInfo::productType() + " " + QSysInfo::productVersion() + " " + QSysInfo::currentCpuArchitecture());
m_connection = new Connection(this);
m_connection->resolveServer(m_matrixId);
connect(m_connection, &Connection::loginFlowsChanged, this, [=]() {
m_connection->loginWithPassword(m_matrixId, m_password, m_deviceName, QString());
connect(m_connection, &Connection::connected, this, [=] {
Q_EMIT connected();
m_isLoggingIn = false;
Q_EMIT isLoggingInChanged();
AccountSettings account(m_connection->userId());
account.setKeepLoggedIn(true);
account.clearAccessToken(); // Drop the legacy - just in case
account.setHomeserver(m_connection->homeserver());
account.setDeviceId(m_connection->deviceId());
account.setDeviceName(m_deviceName);
if (!Controller::instance().saveAccessTokenToKeyChain(account, m_connection->accessToken())) {
qWarning() << "Couldn't save access token";
}
account.sync();
Controller::instance().addConnection(m_connection);
Controller::instance().setActiveConnection(m_connection);
});
connect(m_connection, &Connection::networkError, [=](QString error, const QString &, int, int) {
Q_EMIT Controller::instance().globalErrorOccured(i18n("Network Error"), std::move(error));
m_isLoggingIn = false;
Q_EMIT isLoggingInChanged();
});
connect(m_connection, &Connection::loginError, [=](QString error, const QString &) {
Q_EMIT errorOccured(i18n("Login Failed: %1", error));
m_isLoggingIn = false;
Q_EMIT isLoggingInChanged();
});
});
connect(m_connection, &Connection::resolveError, this, [=](QString error) {
Q_EMIT Controller::instance().globalErrorOccured(i18n("Network Error"), std::move(error));
});
connect(m_connection, &Connection::syncDone, this, [=]() {
Q_EMIT initialSyncFinished();
disconnect(m_connection, &Connection::syncDone, this, nullptr);
});
}
bool Login::supportsPassword() const
{
return m_supportsPassword;
}
bool Login::supportsSso() const
{
return m_supportsSso;
}
QUrl Login::ssoUrl() const
{
return m_ssoUrl;
}
void Login::loginWithSso()
{
SsoSession *session = m_connection->prepareForSso("NeoChat " + QSysInfo::machineHostName() + " " + QSysInfo::productType() + " " + QSysInfo::productVersion() + " " + QSysInfo::currentCpuArchitecture());
m_ssoUrl = session->ssoUrl();
Q_EMIT ssoUrlChanged();
connect(m_connection, &Connection::connected, [=](){
Q_EMIT connected();
AccountSettings account(m_connection->userId());
account.setKeepLoggedIn(true);
account.clearAccessToken(); // Drop the legacy - just in case
account.setHomeserver(m_connection->homeserver());
account.setDeviceId(m_connection->deviceId());
account.setDeviceName(m_deviceName);
if (!Controller::instance().saveAccessTokenToKeyChain(account, m_connection->accessToken())) {
qWarning() << "Couldn't save access token";
}
account.sync();
Controller::instance().addConnection(m_connection);
Controller::instance().setActiveConnection(m_connection);
});
connect(m_connection, &Connection::syncDone, this, [=]() {
Q_EMIT initialSyncFinished();
disconnect(m_connection, &Connection::syncDone, this, nullptr);
});
}
bool Login::testing() const
{
return m_testing;
}
bool Login::isLoggingIn() const
{
return m_isLoggingIn;
}

85
src/login.h Normal file
View File

@@ -0,0 +1,85 @@
/**
* SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
#include <QObject>
#include "csapi/wellknown.h"
#include "connection.h"
using namespace Quotient;
class Login : public QObject
{
Q_OBJECT
Q_PROPERTY(bool homeserverReachable READ homeserverReachable NOTIFY homeserverReachableChanged)
Q_PROPERTY(bool testing READ testing NOTIFY testingChanged)
Q_PROPERTY(QString matrixId READ matrixId WRITE setMatrixId NOTIFY matrixIdChanged)
Q_PROPERTY(QString password READ password WRITE setPassword NOTIFY passwordChanged)
Q_PROPERTY(QString deviceName READ deviceName WRITE setDeviceName NOTIFY deviceNameChanged)
Q_PROPERTY(bool supportsSso READ supportsSso NOTIFY loginFlowsChanged STORED false)
Q_PROPERTY(bool supportsPassword READ supportsPassword NOTIFY loginFlowsChanged STORED false)
Q_PROPERTY(QUrl ssoUrl READ ssoUrl NOTIFY ssoUrlChanged)
Q_PROPERTY(bool isLoggingIn READ isLoggingIn NOTIFY isLoggingInChanged)
public:
explicit Login(QObject *parent = nullptr);
Q_INVOKABLE void init();
bool homeserverReachable() const;
QString matrixId() const;
void setMatrixId(const QString &matrixId);
QString password() const;
void setPassword(const QString &password);
QString deviceName() const;
void setDeviceName(const QString &deviceName);
bool supportsPassword() const;
bool supportsSso() const;
bool testing() const;
QUrl ssoUrl() const;
bool isLoggingIn() const;
Q_INVOKABLE void login();
Q_INVOKABLE void loginWithSso();
Q_SIGNALS:
void homeserverReachableChanged();
void testHomeserverFinished();
void matrixIdChanged();
void passwordChanged();
void deviceNameChanged();
void initialSyncFinished();
void loginFlowsChanged();
void ssoUrlChanged();
void connected();
void errorOccured(QString message);
void testingChanged();
void isLoggingInChanged();
private:
void setHomeserverReachable(bool reachable);
bool m_homeserverReachable;
QString m_matrixId;
QString m_password;
QString m_deviceName;
bool m_supportsSso = false;
bool m_supportsPassword = false;
Connection *m_connection = nullptr;
QUrl m_ssoUrl;
bool m_testing;
bool m_isLoggingIn = false;
};

View File

@@ -32,6 +32,7 @@
#include "devicesmodel.h"
#include "emojimodel.h"
#include "filetypesingleton.h"
#include "login.h"
#include "matriximageprovider.h"
#include "messageeventmodel.h"
#include "messagefiltermodel.h"
@@ -98,10 +99,13 @@ int main(int argc, char *argv[])
auto config = NeoChatConfig::self();
FileTypeSingleton fileTypeSingleton;
Login *login = new Login();
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Controller", &Controller::instance());
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Clipboard", &clipboard);
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Config", config);
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "FileType", &fileTypeSingleton);
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "LoginHelper", login);
qmlRegisterType<AccountListModel>("org.kde.neochat", 1, 0, "AccountListModel");
qmlRegisterType<ActionsHandler>("org.kde.neochat", 1, 0, "ActionsHandler");
qmlRegisterType<ChatDocumentHandler>("org.kde.neochat", 1, 0, "ChatDocumentHandler");