From 8e2cdc8f08a156dd04d72bc3433139dc782b8325 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Thu, 10 Dec 2020 13:33:07 +0100 Subject: [PATCH] Implement a device management page --- imports/NeoChat/Page/DevicesPage.qml | 103 +++++++++++++++++++++++++++ qml/main.qml | 6 ++ res.qrc | 1 + src/CMakeLists.txt | 1 + src/controller.cpp | 9 +++ src/controller.h | 5 ++ src/devicesmodel.cpp | 87 ++++++++++++++++++++++ src/devicesmodel.h | 39 ++++++++++ src/main.cpp | 2 + 9 files changed, 253 insertions(+) create mode 100644 imports/NeoChat/Page/DevicesPage.qml create mode 100644 src/devicesmodel.cpp create mode 100644 src/devicesmodel.h diff --git a/imports/NeoChat/Page/DevicesPage.qml b/imports/NeoChat/Page/DevicesPage.qml new file mode 100644 index 000000000..7f7701b63 --- /dev/null +++ b/imports/NeoChat/Page/DevicesPage.qml @@ -0,0 +1,103 @@ +/** + * SPDX-FileCopyrightText: Tobias Fella + * + * SPDX-LicenseIdentifier: 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 + +Kirigami.ScrollablePage { + title: i18n("Devices") + + ListView { + model: DevicesModel { + id: devices + } + delegate: Kirigami.SwipeListItem { + leftPadding: 0 + rightPadding: 0 + Kirigami.BasicListItem { + anchors.top: parent.top + anchors.bottom: parent.bottom + + text: model.displayName + subtitle: model.id + icon: "network-connect" + } + actions: [ + Kirigami.Action { + text: i18n("Edit device name") + iconName: "document-edit" + onTriggered: { + renameSheet.index = model.index + renameSheet.name = model.displayName + renameSheet.open() + } + }, + Kirigami.Action { + text: i18n("Logout device") + iconName: "edit-delete-remove" + onTriggered: { + passwordSheet.index = index + passwordSheet.open() + } + } + ] + } + } + + Kirigami.OverlaySheet { + id: passwordSheet + + property var index + + header: Kirigami.Heading { + text: i18n("Remove device") + } + Kirigami.FormLayout { + Controls.TextField { + id: passwordField + Kirigami.FormData.label: i18n("Password:") + echoMode: TextInput.Password + } + Controls.Button { + text: i18n("Confirm") + onClicked: { + devices.logout(passwordSheet.index, passwordField.text) + passwordField.text = "" + passwordSheet.close() + } + } + } + } + + Kirigami.OverlaySheet { + id: renameSheet + property int index + property string name + + header: Kirigami.Heading { + text: i18n("Edit device") + } + Kirigami.FormLayout { + Controls.TextField { + id: nameField + Kirigami.FormData.label: i18n("Name:") + text: renameSheet.name + } + Controls.Button { + text: i18n("Save") + onClicked: { + devices.setName(renameSheet.index, nameField.text) + renameSheet.close() + } + } + } + } +} diff --git a/qml/main.qml b/qml/main.qml index 8bdf33e99..deb2b53dc 100644 --- a/qml/main.qml +++ b/qml/main.qml @@ -160,6 +160,12 @@ Kirigami.ApplicationWindow { onTriggered: pushReplaceLayer("qrc:/imports/NeoChat/Page/AccountsPage.qml") enabled: pageStack.layers.currentItem.title !== i18n("Accounts") }, + Kirigami.Action { + text: i18n("Devices") + iconName: "network-connect" + onTriggered: pageStack.layers.push("qrc:/imports/NeoChat/Page/DevicesPage.qml") + enabled: pageStack.layers.currentItem.title !== i18n("Devices") + }, Kirigami.Action { text: i18n("Settings") icon.name: "settings-configure" diff --git a/res.qrc b/res.qrc index 5863c31fb..05f8cdc6e 100644 --- a/res.qrc +++ b/res.qrc @@ -13,6 +13,7 @@ imports/NeoChat/Page/InvitationPage.qml imports/NeoChat/Page/StartChatPage.qml imports/NeoChat/Page/ImageEditorPage.qml + imports/NeoChat/Page/DevicesPage.qml imports/NeoChat/Component/qmldir imports/NeoChat/Component/ChatTextInput.qml imports/NeoChat/Component/AutoMouseArea.qml diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c96cebfad..22b75f77f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -16,6 +16,7 @@ add_executable(neochat notificationsmanager.cpp sortfilterroomlistmodel.cpp chatdocumenthandler.cpp + devicesmodel.cpp ../res.qrc ) diff --git a/src/controller.cpp b/src/controller.cpp index 731cf9171..507326298 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -563,3 +563,12 @@ QList Controller::preferencesShortcuts() const { return KStandardShortcut::preferences(); } + +NeochatDeleteDeviceJob::NeochatDeleteDeviceJob(const QString& deviceId, const Omittable &auth) + : Quotient::BaseJob(HttpVerb::Delete, QStringLiteral("DeleteDeviceJob"), + QStringLiteral("/_matrix/client/r0/devices/%1").arg(deviceId)) +{ + QJsonObject _data; + addParam(_data, QStringLiteral("auth"), auth); + setRequestData(std::move(_data)); +} diff --git a/src/controller.h b/src/controller.h index 6412af4be..6b6ffa1ba 100644 --- a/src/controller.h +++ b/src/controller.h @@ -130,4 +130,9 @@ public: explicit NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const Omittable &auth = none); }; +class NeochatDeleteDeviceJob : public BaseJob { +public: + explicit NeochatDeleteDeviceJob(const QString& deviceId, const Omittable &auth = none); +}; + #endif // CONTROLLER_H diff --git a/src/devicesmodel.cpp b/src/devicesmodel.cpp new file mode 100644 index 000000000..526fa307e --- /dev/null +++ b/src/devicesmodel.cpp @@ -0,0 +1,87 @@ +/** + * SPDX-FileCopyrightText: Tobias Fella + * + * SPDX-LicenseIdentifier: GPL-2.0-or-later + */ + +#include "devicesmodel.h" + +#include + +#include "controller.h" + +DevicesModel::DevicesModel(QObject *parent) + : QAbstractListModel(parent) +{ + GetDevicesJob *job = Controller::instance().activeConnection()->callApi(); + connect(job, &BaseJob::success, this, [this, job]() { + beginResetModel(); + m_devices = job->devices(); + endResetModel(); + }); +} + +QVariant DevicesModel::data(const QModelIndex& index, int role) const +{ + if(index.row() < 0 || index.row() >= rowCount(QModelIndex())) + return QVariant(); + switch(role) { + case Id: + return m_devices[index.row()].deviceId; + case DisplayName: + return m_devices[index.row()].displayName; + case LastIp: + return m_devices[index.row()].lastSeenIp; + case LastTimestamp: + if(m_devices[index.row()].lastSeenTs) + return *m_devices[index.row()].lastSeenTs; + } + return QVariant(); +} + +int DevicesModel::rowCount(const QModelIndex& parent) const +{ + return m_devices.size(); +} + +QHash DevicesModel::roleNames() const +{ + return {{Id, "id"}, {DisplayName, "displayName"}, {LastIp, "lastIp"}, {LastTimestamp, "lastTimestamp"}}; +} + +void DevicesModel::logout(int index, const QString &password) +{ + auto job = Controller::instance().activeConnection()->callApi(m_devices[index].deviceId); + + connect(job, &BaseJob::result, this, [this, job, password, index] { + if (job->error() != 0) { + QJsonObject replyData = job->jsonData(); + QJsonObject authData; + authData["session"] = replyData["session"]; + authData["password"] = password; + authData["type"] = "m.login.password"; + QJsonObject identifier = {{"type", "m.id.user"}, {"user", Controller::instance().activeConnection()->user()->id()}}; + authData["identifier"] = identifier; + auto *innerJob = Controller::instance().activeConnection()->callApi(m_devices[index].deviceId, authData); + connect(innerJob, &BaseJob::success, this, [this, index]() { + Q_EMIT beginRemoveRows(QModelIndex(), index, index); + m_devices.remove(index); + Q_EMIT endRemoveRows(); + }); + } + }); +} + +void DevicesModel::setName(int index, const QString &name) +{ + auto job = Controller::instance().activeConnection()->callApi(m_devices[index].deviceId, name); + QString oldName = m_devices[index].displayName; + Q_EMIT beginResetModel(); + m_devices[index].displayName = name; + Q_EMIT endResetModel(); + connect(job, &BaseJob::failure, this, [=]() { + Q_EMIT beginResetModel(); + m_devices[index].displayName = oldName; + Q_EMIT endResetModel(); + }); +} diff --git a/src/devicesmodel.h b/src/devicesmodel.h new file mode 100644 index 000000000..c516abe6d --- /dev/null +++ b/src/devicesmodel.h @@ -0,0 +1,39 @@ +/** + * SPDX-FileCopyrightText: Tobias Fella + * + * SPDX-LicenseIdentifier: GPL-2.0-or-later + */ + +#pragma once + +#include + +#include + +using namespace Quotient; + +class DevicesModel : public QAbstractListModel +{ + Q_OBJECT + +public: + enum Roles { + Id, + DisplayName, + LastIp, + LastTimestamp, + }; + Q_ENUM(Roles); + + DevicesModel(QObject *parent = nullptr); + + QVariant data(const QModelIndex & index, int role) const override; + QHash roleNames() const override; + int rowCount(const QModelIndex & parent) const override; + + Q_INVOKABLE void logout(int index, const QString &password); + Q_INVOKABLE void setName(int index, const QString &name); + +private: + QVector m_devices; +}; diff --git a/src/main.cpp b/src/main.cpp index 7c720c9e9..d4eba937c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -41,6 +41,7 @@ #include "sortfilterroomlistmodel.h" #include "userdirectorylistmodel.h" #include "userlistmodel.h" +#include "devicesmodel.h" using namespace Quotient; @@ -89,6 +90,7 @@ int main(int argc, char *argv[]) qmlRegisterType("org.kde.neochat", 1, 0, "UserDirectoryListModel"); qmlRegisterType("org.kde.neochat", 1, 0, "EmojiModel"); qmlRegisterType("org.kde.neochat", 1, 0, "SortFilterRoomListModel"); + qmlRegisterType("org.kde.neochat", 1, 0, "DevicesModel"); qmlRegisterUncreatableType("org.kde.neochat", 1, 0, "RoomMessageEvent", "ENUM"); qmlRegisterUncreatableType("org.kde.neochat", 1, 0, "RoomType", "ENUM"); qmlRegisterUncreatableType("org.kde.neochat", 1, 0, "UserType", "ENUM");