Improve DevicesPage and DevicesModel
- Split the list into sections for "this devices", "verified devices", "unverified devices", and "devices without encryption support" - Sort the lists by last activity
This commit is contained in:
@@ -64,6 +64,7 @@ add_library(neochat STATIC
|
|||||||
chatdocumenthandler.h
|
chatdocumenthandler.h
|
||||||
models/devicesmodel.cpp
|
models/devicesmodel.cpp
|
||||||
models/devicesmodel.h
|
models/devicesmodel.h
|
||||||
|
models/devicesproxymodel.cpp
|
||||||
filetypesingleton.cpp
|
filetypesingleton.cpp
|
||||||
filetypesingleton.h
|
filetypesingleton.h
|
||||||
login.cpp
|
login.cpp
|
||||||
|
|||||||
@@ -49,6 +49,7 @@
|
|||||||
#include "models/collapsestateproxymodel.h"
|
#include "models/collapsestateproxymodel.h"
|
||||||
#include "models/customemojimodel.h"
|
#include "models/customemojimodel.h"
|
||||||
#include "models/devicesmodel.h"
|
#include "models/devicesmodel.h"
|
||||||
|
#include "models/devicesproxymodel.h"
|
||||||
#include "models/emojimodel.h"
|
#include "models/emojimodel.h"
|
||||||
#include "models/emoticonfiltermodel.h"
|
#include "models/emoticonfiltermodel.h"
|
||||||
#include "models/imagepacksmodel.h"
|
#include "models/imagepacksmodel.h"
|
||||||
@@ -236,6 +237,7 @@ int main(int argc, char *argv[])
|
|||||||
qmlRegisterType<SortFilterRoomListModel>("org.kde.neochat", 1, 0, "SortFilterRoomListModel");
|
qmlRegisterType<SortFilterRoomListModel>("org.kde.neochat", 1, 0, "SortFilterRoomListModel");
|
||||||
qmlRegisterType<SortFilterSpaceListModel>("org.kde.neochat", 1, 0, "SortFilterSpaceListModel");
|
qmlRegisterType<SortFilterSpaceListModel>("org.kde.neochat", 1, 0, "SortFilterSpaceListModel");
|
||||||
qmlRegisterType<DevicesModel>("org.kde.neochat", 1, 0, "DevicesModel");
|
qmlRegisterType<DevicesModel>("org.kde.neochat", 1, 0, "DevicesModel");
|
||||||
|
qmlRegisterType<DevicesProxyModel>("org.kde.neochat", 1, 0, "DevicesProxyModel");
|
||||||
qmlRegisterType<LinkPreviewer>("org.kde.neochat", 1, 0, "LinkPreviewer");
|
qmlRegisterType<LinkPreviewer>("org.kde.neochat", 1, 0, "LinkPreviewer");
|
||||||
qmlRegisterType<CompletionModel>("org.kde.neochat", 1, 0, "CompletionModel");
|
qmlRegisterType<CompletionModel>("org.kde.neochat", 1, 0, "CompletionModel");
|
||||||
qmlRegisterType<StateModel>("org.kde.neochat", 1, 0, "StateModel");
|
qmlRegisterType<StateModel>("org.kde.neochat", 1, 0, "StateModel");
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <csapi/device_management.h>
|
#include <csapi/device_management.h>
|
||||||
|
|
||||||
#include "controller.h"
|
#include "controller.h"
|
||||||
|
#include <KLocalizedString>
|
||||||
#include <connection.h>
|
#include <connection.h>
|
||||||
#include <user.h>
|
#include <user.h>
|
||||||
|
|
||||||
@@ -14,12 +15,6 @@ using namespace Quotient;
|
|||||||
DevicesModel::DevicesModel(QObject *parent)
|
DevicesModel::DevicesModel(QObject *parent)
|
||||||
: QAbstractListModel(parent)
|
: QAbstractListModel(parent)
|
||||||
{
|
{
|
||||||
connect(&Controller::instance(), &Controller::activeConnectionChanged, this, [this]() {
|
|
||||||
DevicesModel::fetchDevices();
|
|
||||||
Q_EMIT connectionChanged();
|
|
||||||
});
|
|
||||||
|
|
||||||
fetchDevices();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void DevicesModel::fetchDevices()
|
void DevicesModel::fetchDevices()
|
||||||
@@ -30,6 +25,7 @@ void DevicesModel::fetchDevices()
|
|||||||
beginResetModel();
|
beginResetModel();
|
||||||
m_devices = job->devices();
|
m_devices = job->devices();
|
||||||
endResetModel();
|
endResetModel();
|
||||||
|
Q_EMIT countChanged();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -40,16 +36,33 @@ QVariant DevicesModel::data(const QModelIndex &index, int role) const
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const auto &device = m_devices[index.row()];
|
||||||
|
|
||||||
switch (role) {
|
switch (role) {
|
||||||
case Id:
|
case Id:
|
||||||
return m_devices[index.row()].deviceId;
|
return device.deviceId;
|
||||||
case DisplayName:
|
case DisplayName:
|
||||||
return m_devices[index.row()].displayName;
|
return device.displayName;
|
||||||
case LastIp:
|
case LastIp:
|
||||||
return m_devices[index.row()].lastSeenIp;
|
return device.lastSeenIp;
|
||||||
case LastTimestamp:
|
case LastTimestamp:
|
||||||
if (m_devices[index.row()].lastSeenTs)
|
if (device.lastSeenTs) {
|
||||||
return *m_devices[index.row()].lastSeenTs;
|
return *device.lastSeenTs;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
case Type:
|
||||||
|
if (device.deviceId == m_connection->deviceId()) {
|
||||||
|
return This;
|
||||||
|
}
|
||||||
|
if (!m_connection->isKnownE2eeCapableDevice(m_connection->userId(), device.deviceId)) {
|
||||||
|
return Unencrypted;
|
||||||
|
}
|
||||||
|
if (m_connection->isVerifiedDevice(m_connection->userId(), device.deviceId)) {
|
||||||
|
return Verified;
|
||||||
|
} else {
|
||||||
|
return Unverified;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
@@ -62,11 +75,21 @@ int DevicesModel::rowCount(const QModelIndex &parent) const
|
|||||||
|
|
||||||
QHash<int, QByteArray> DevicesModel::roleNames() const
|
QHash<int, QByteArray> DevicesModel::roleNames() const
|
||||||
{
|
{
|
||||||
return {{Id, "id"}, {DisplayName, "displayName"}, {LastIp, "lastIp"}, {LastTimestamp, "lastTimestamp"}};
|
return {
|
||||||
|
{Id, "id"},
|
||||||
|
{DisplayName, "displayName"},
|
||||||
|
{LastIp, "lastIp"},
|
||||||
|
{LastTimestamp, "lastTimestamp"},
|
||||||
|
{Type, "type"},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
void DevicesModel::logout(int index, const QString &password)
|
void DevicesModel::logout(const QString &deviceId, const QString &password)
|
||||||
{
|
{
|
||||||
|
int index;
|
||||||
|
for (index = 0; m_devices[index].deviceId != deviceId; index++)
|
||||||
|
;
|
||||||
|
|
||||||
auto job = Controller::instance().activeConnection()->callApi<NeochatDeleteDeviceJob>(m_devices[index].deviceId);
|
auto job = Controller::instance().activeConnection()->callApi<NeochatDeleteDeviceJob>(m_devices[index].deviceId);
|
||||||
|
|
||||||
connect(job, &BaseJob::result, this, [this, job, password, index] {
|
connect(job, &BaseJob::result, this, [this, job, password, index] {
|
||||||
@@ -74,6 +97,7 @@ void DevicesModel::logout(int index, const QString &password)
|
|||||||
beginRemoveRows(QModelIndex(), index, index);
|
beginRemoveRows(QModelIndex(), index, index);
|
||||||
m_devices.remove(index);
|
m_devices.remove(index);
|
||||||
endRemoveRows();
|
endRemoveRows();
|
||||||
|
Q_EMIT countChanged();
|
||||||
};
|
};
|
||||||
if (job->error() != BaseJob::Success) {
|
if (job->error() != BaseJob::Success) {
|
||||||
QJsonObject replyData = job->jsonData();
|
QJsonObject replyData = job->jsonData();
|
||||||
@@ -91,8 +115,11 @@ void DevicesModel::logout(int index, const QString &password)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void DevicesModel::setName(int index, const QString &name)
|
void DevicesModel::setName(const QString &deviceId, const QString &name)
|
||||||
{
|
{
|
||||||
|
int index;
|
||||||
|
for (index = 0; m_devices[index].deviceId != deviceId; index++);
|
||||||
|
|
||||||
auto job = Controller::instance().activeConnection()->callApi<UpdateDeviceJob>(m_devices[index].deviceId, name);
|
auto job = Controller::instance().activeConnection()->callApi<UpdateDeviceJob>(m_devices[index].deviceId, name);
|
||||||
QString oldName = m_devices[index].displayName;
|
QString oldName = m_devices[index].displayName;
|
||||||
beginResetModel();
|
beginResetModel();
|
||||||
@@ -107,7 +134,27 @@ void DevicesModel::setName(int index, const QString &name)
|
|||||||
|
|
||||||
Connection *DevicesModel::connection() const
|
Connection *DevicesModel::connection() const
|
||||||
{
|
{
|
||||||
return Controller::instance().activeConnection();
|
return m_connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DevicesModel::setConnection(Connection *connection)
|
||||||
|
{
|
||||||
|
if (m_connection) {
|
||||||
|
disconnect(m_connection, nullptr, this, nullptr);
|
||||||
|
}
|
||||||
|
m_connection = connection;
|
||||||
|
Q_EMIT connectionChanged();
|
||||||
|
fetchDevices();
|
||||||
|
|
||||||
|
connect(m_connection, &Connection::sessionVerified, this, [this](const QString &userId, const QString &deviceId) {
|
||||||
|
Q_UNUSED(deviceId);
|
||||||
|
if (userId == Controller::instance().activeConnection()->userId()) {
|
||||||
|
fetchDevices();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
connect(m_connection, &Connection::finishedQueryingKeys, this, [this]() {
|
||||||
|
fetchDevices();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#include "moc_devicesmodel.cpp"
|
#include "moc_devicesmodel.cpp"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
#include <QAbstractListModel>
|
#include <QAbstractListModel>
|
||||||
|
|
||||||
|
#include <QPointer>
|
||||||
#include <csapi/definitions/client_device.h>
|
#include <csapi/definitions/client_device.h>
|
||||||
|
|
||||||
namespace Quotient
|
namespace Quotient
|
||||||
@@ -28,7 +29,7 @@ class DevicesModel : public QAbstractListModel
|
|||||||
/**
|
/**
|
||||||
* @brief The current connection that the model is getting its devices from.
|
* @brief The current connection that the model is getting its devices from.
|
||||||
*/
|
*/
|
||||||
Q_PROPERTY(Quotient::Connection *connection READ connection NOTIFY connectionChanged)
|
Q_PROPERTY(Quotient::Connection *connection READ connection WRITE setConnection NOTIFY connectionChanged REQUIRED)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
/**
|
/**
|
||||||
@@ -39,10 +40,17 @@ public:
|
|||||||
DisplayName, /**< Display name set by the user for this device. */
|
DisplayName, /**< Display name set by the user for this device. */
|
||||||
LastIp, /**< The IP address where this device was last seen. */
|
LastIp, /**< The IP address where this device was last seen. */
|
||||||
LastTimestamp, /**< The timestamp when this devices was last seen. */
|
LastTimestamp, /**< The timestamp when this devices was last seen. */
|
||||||
|
Type, /**< The category to sort this device into. */
|
||||||
};
|
};
|
||||||
Q_ENUM(Roles)
|
Q_ENUM(Roles)
|
||||||
|
|
||||||
DevicesModel(QObject *parent = nullptr);
|
enum DeviceType {
|
||||||
|
This,
|
||||||
|
Verified,
|
||||||
|
Unverified,
|
||||||
|
Unencrypted,
|
||||||
|
};
|
||||||
|
Q_ENUM(DeviceType);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Get the given role value at the given index.
|
* @brief Get the given role value at the given index.
|
||||||
@@ -66,21 +74,27 @@ public:
|
|||||||
QHash<int, QByteArray> roleNames() const override;
|
QHash<int, QByteArray> roleNames() const override;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Logout the device at the given index.
|
* @brief Logout the device with the given id.
|
||||||
*/
|
*/
|
||||||
Q_INVOKABLE void logout(int index, const QString &password);
|
Q_INVOKABLE void logout(const QString &deviceId, const QString &password);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Set the display name of the device at the given index.
|
* @brief Set the display name of the device with the given id.
|
||||||
*/
|
*/
|
||||||
Q_INVOKABLE void setName(int index, const QString &name);
|
Q_INVOKABLE void setName(const QString &deviceId, const QString &name);
|
||||||
|
|
||||||
Quotient::Connection *connection() const;
|
explicit DevicesModel(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
|
||||||
|
[[nodiscard]] Quotient::Connection *connection() const;
|
||||||
|
void setConnection(Quotient::Connection *connection);
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void connectionChanged();
|
void connectionChanged();
|
||||||
|
void countChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void fetchDevices();
|
void fetchDevices();
|
||||||
QVector<Quotient::Device> m_devices;
|
QVector<Quotient::Device> m_devices;
|
||||||
|
QPointer<Quotient::Connection> m_connection;
|
||||||
};
|
};
|
||||||
|
|||||||
28
src/models/devicesproxymodel.cpp
Normal file
28
src/models/devicesproxymodel.cpp
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "devicesproxymodel.h"
|
||||||
|
#include "devicesmodel.h"
|
||||||
|
|
||||||
|
int DevicesProxyModel::type() const
|
||||||
|
{
|
||||||
|
return m_type;
|
||||||
|
}
|
||||||
|
void DevicesProxyModel::setType(int type)
|
||||||
|
{
|
||||||
|
m_type = type;
|
||||||
|
Q_EMIT typeChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DevicesProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
|
||||||
|
{
|
||||||
|
Q_UNUSED(source_parent)
|
||||||
|
return sourceModel()->data(sourceModel()->index(source_row, 0), DevicesModel::Type).toInt() == m_type;
|
||||||
|
}
|
||||||
|
DevicesProxyModel::DevicesProxyModel(QObject *parent)
|
||||||
|
: QSortFilterProxyModel(parent)
|
||||||
|
, m_type(0)
|
||||||
|
{
|
||||||
|
setSortRole(DevicesModel::LastTimestamp);
|
||||||
|
sort(0, Qt::DescendingOrder);
|
||||||
|
}
|
||||||
25
src/models/devicesproxymodel.h
Normal file
25
src/models/devicesproxymodel.h
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QSortFilterProxyModel>
|
||||||
|
|
||||||
|
class DevicesProxyModel : public QSortFilterProxyModel
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_PROPERTY(int type READ type WRITE setType NOTIFY typeChanged);
|
||||||
|
|
||||||
|
public:
|
||||||
|
DevicesProxyModel(QObject *parent = nullptr);
|
||||||
|
[[nodiscard]] bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
|
||||||
|
|
||||||
|
void setType(int type);
|
||||||
|
[[nodiscard]] int type() const;
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void typeChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
int m_type;
|
||||||
|
};
|
||||||
@@ -26,12 +26,22 @@ QQC2.Menu {
|
|||||||
QQC2.MenuItem {
|
QQC2.MenuItem {
|
||||||
text: i18n("Notification settings")
|
text: i18n("Notification settings")
|
||||||
icon.name: "notifications"
|
icon.name: "notifications"
|
||||||
onTriggered: pageStack.pushDialogLayer("qrc:/SettingsPage.qml", {defaultPage: "notifications"}, { title: i18n("Configure")})
|
onTriggered: pageStack.pushDialogLayer("qrc:/SettingsPage.qml", {
|
||||||
|
defaultPage: "notifications",
|
||||||
|
connection: Controller.activeConnection,
|
||||||
|
}, {
|
||||||
|
title: i18n("Configure")
|
||||||
|
});
|
||||||
}
|
}
|
||||||
QQC2.MenuItem {
|
QQC2.MenuItem {
|
||||||
text: i18n("Devices")
|
text: i18n("Devices")
|
||||||
icon.name: "computer-symbolic"
|
icon.name: "computer-symbolic"
|
||||||
onTriggered: pageStack.pushDialogLayer("qrc:/SettingsPage.qml", {defaultPage: "devices"}, { title: i18n("Configure")})
|
onTriggered: pageStack.pushDialogLayer("qrc:/SettingsPage.qml", {
|
||||||
|
defaultPage: "devices",
|
||||||
|
connection: Controller.activeConnection,
|
||||||
|
}, {
|
||||||
|
title: i18n("Configure")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
QQC2.MenuItem {
|
QQC2.MenuItem {
|
||||||
text: i18n("Logout")
|
text: i18n("Logout")
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ QQC2.ToolBar {
|
|||||||
}
|
}
|
||||||
QQC2.ToolButton {
|
QQC2.ToolButton {
|
||||||
icon.name: "settings-configure"
|
icon.name: "settings-configure"
|
||||||
onClicked: pageStack.pushDialogLayer("qrc:/SettingsPage.qml", {}, { title: i18n("Configure") })
|
onClicked: pageStack.pushDialogLayer("qrc:/SettingsPage.qml", {connection: Controller.activeConnection}, { title: i18n("Configure") })
|
||||||
text: i18n("Open Settings")
|
text: i18n("Open Settings")
|
||||||
display: QQC2.AbstractButton.IconOnly
|
display: QQC2.AbstractButton.IconOnly
|
||||||
Layout.minimumWidth: Layout.preferredWidth
|
Layout.minimumWidth: Layout.preferredWidth
|
||||||
|
|||||||
134
src/qml/Settings/DeviceDelegate.qml
Normal file
134
src/qml/Settings/DeviceDelegate.qml
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2020 - 2023 Tobias Fella <tobias.fella@kde.org>
|
||||||
|
// SPDX-FileCopyrightText: 2022 James Graham <james.h.graham@protonmail.com>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
import QtQuick 2.15
|
||||||
|
import QtQuick.Controls 2.15 as QQC2
|
||||||
|
import QtQuick.Layouts 1.15
|
||||||
|
|
||||||
|
import org.kde.kirigami 2.19 as Kirigami
|
||||||
|
import org.kde.kirigamiaddons.labs.mobileform 0.1 as MobileForm
|
||||||
|
|
||||||
|
import org.kde.neochat 1.0
|
||||||
|
|
||||||
|
MobileForm.AbstractFormDelegate {
|
||||||
|
id: deviceDelegate
|
||||||
|
|
||||||
|
required property string id
|
||||||
|
required property int lastTimestamp
|
||||||
|
required property string displayName
|
||||||
|
|
||||||
|
property bool editDeviceName: false
|
||||||
|
property bool showVerifyButton
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
onClicked: deviceDelegate.editDeviceName = true
|
||||||
|
|
||||||
|
contentItem: RowLayout {
|
||||||
|
spacing: Kirigami.Units.largeSpacing
|
||||||
|
|
||||||
|
Kirigami.Icon {
|
||||||
|
source: "network-connect"
|
||||||
|
implicitWidth: Kirigami.Units.iconSizes.medium
|
||||||
|
implicitHeight: Kirigami.Units.iconSizes.medium
|
||||||
|
}
|
||||||
|
ColumnLayout {
|
||||||
|
id: deviceLabel
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
visible: !deviceDelegate.editDeviceName
|
||||||
|
|
||||||
|
QQC2.Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: deviceDelegate.displayName
|
||||||
|
elide: Text.ElideRight
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
maximumLineCount: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: deviceDelegate.id + ", Last activity: " + (new Date(deviceDelegate.lastTimestamp)).toLocaleString(Qt.locale(), Locale.ShortFormat)
|
||||||
|
color: Kirigami.Theme.disabledTextColor
|
||||||
|
font: Kirigami.Theme.smallFont
|
||||||
|
elide: Text.ElideRight
|
||||||
|
visible: text.length > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Kirigami.ActionTextField {
|
||||||
|
id: nameField
|
||||||
|
Accessible.description: i18n("New device name")
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: deviceLabel.implicitHeight
|
||||||
|
visible: deviceDelegate.editDeviceName
|
||||||
|
|
||||||
|
text: deviceDelegate.displayName
|
||||||
|
|
||||||
|
rightActions: [
|
||||||
|
Kirigami.Action {
|
||||||
|
text: i18n("Cancel editing display name")
|
||||||
|
icon.name: "edit-delete-remove"
|
||||||
|
onTriggered: {
|
||||||
|
deviceDelegate.editDeviceName = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Kirigami.Action {
|
||||||
|
text: i18n("Confirm new display name")
|
||||||
|
icon.name: "checkmark"
|
||||||
|
visible: nameField.text !== deviceDelegate.displayName
|
||||||
|
onTriggered: {
|
||||||
|
devicesModel.setName(deviceDelegate.id, nameField.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
onAccepted: devicesModel.setName(deviceDelegate.id, nameField.text)
|
||||||
|
}
|
||||||
|
QQC2.ToolButton {
|
||||||
|
display: QQC2.AbstractButton.IconOnly
|
||||||
|
action: Kirigami.Action {
|
||||||
|
id: editDeviceAction
|
||||||
|
text: i18n("Edit device name")
|
||||||
|
icon.name: "document-edit"
|
||||||
|
onTriggered: deviceDelegate.editDeviceName = true
|
||||||
|
}
|
||||||
|
QQC2.ToolTip {
|
||||||
|
text: editDeviceAction.text
|
||||||
|
delay: Kirigami.Units.toolTipDelay
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QQC2.ToolButton {
|
||||||
|
display: QQC2.AbstractButton.IconOnly
|
||||||
|
visible: Controller.encryptionSupported && deviceDelegate.showVerifyButton
|
||||||
|
action: Kirigami.Action {
|
||||||
|
id: verifyDeviceAction
|
||||||
|
text: i18n("Verify device")
|
||||||
|
icon.name: "security-low-symbolic"
|
||||||
|
onTriggered: {
|
||||||
|
devicesModel.connection.startKeyVerificationSession(devicesModel.connection.localUserId, deviceDelegate.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QQC2.ToolTip {
|
||||||
|
text: verifyDeviceAction.text
|
||||||
|
delay: Kirigami.Units.toolTipDelay
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QQC2.ToolButton {
|
||||||
|
display: QQC2.AbstractButton.IconOnly
|
||||||
|
action: Kirigami.Action {
|
||||||
|
id: logoutDeviceAction
|
||||||
|
text: i18n("Logout device")
|
||||||
|
icon.name: "edit-delete-remove"
|
||||||
|
onTriggered: {
|
||||||
|
passwordSheet.deviceId = deviceDelegate.id
|
||||||
|
passwordSheet.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QQC2.ToolTip {
|
||||||
|
text: logoutDeviceAction.text
|
||||||
|
delay: Kirigami.Units.toolTipDelay
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
56
src/qml/Settings/DevicesCard.qml
Normal file
56
src/qml/Settings/DevicesCard.qml
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2020 - 2023 Tobias Fella <tobias.fella@kde.org>
|
||||||
|
// SPDX-FileCopyrightText: 2022 James Graham <james.h.graham@protonmail.com>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
import QtQuick 2.15
|
||||||
|
import QtQuick.Controls 2.15 as QQC2
|
||||||
|
import QtQuick.Layouts 1.15
|
||||||
|
|
||||||
|
import org.kde.kirigami 2.19 as Kirigami
|
||||||
|
import org.kde.kirigamiaddons.labs.mobileform 0.1 as MobileForm
|
||||||
|
|
||||||
|
import org.kde.neochat 1.0
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property string title
|
||||||
|
required property var type
|
||||||
|
required property bool showVerifyButton
|
||||||
|
|
||||||
|
visible: deviceRepeater.count > 0
|
||||||
|
MobileForm.FormHeader {
|
||||||
|
title: root.title
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
MobileForm.FormCard {
|
||||||
|
id: devicesCard
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
|
||||||
|
contentItem: ColumnLayout {
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
id: deviceRepeater
|
||||||
|
model: DevicesProxyModel {
|
||||||
|
sourceModel: devicesModel
|
||||||
|
type: root.type
|
||||||
|
}
|
||||||
|
|
||||||
|
Kirigami.LoadingPlaceholder {
|
||||||
|
visible: deviceModel.count === 0 // We can assume 0 means loading since there is at least one device
|
||||||
|
anchors.centerIn: parent
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: DeviceDelegate {
|
||||||
|
showVerifyButton: root.showVerifyButton
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
// SPDX-FileCopyrightText: Tobias Fella <tobias.fella@kde.org>
|
// SPDX-FileCopyrightText: 2020 - 2023 Tobias Fella <tobias.fella@kde.org>
|
||||||
|
// SPDX-FileCopyrightText: 2022 James Graham <james.h.graham@protonmail.com>
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
import QtQuick 2.15
|
import QtQuick 2.15
|
||||||
@@ -12,155 +13,44 @@ import org.kde.neochat 1.0
|
|||||||
|
|
||||||
Kirigami.ScrollablePage {
|
Kirigami.ScrollablePage {
|
||||||
title: i18n("Devices")
|
title: i18n("Devices")
|
||||||
topPadding: 0
|
|
||||||
|
property alias connection: devicesModel.connection
|
||||||
|
|
||||||
leftPadding: 0
|
leftPadding: 0
|
||||||
rightPadding: 0
|
rightPadding: 0
|
||||||
|
|
||||||
|
DevicesModel {
|
||||||
|
id: devicesModel
|
||||||
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
spacing: 0
|
DevicesCard {
|
||||||
MobileForm.FormHeader {
|
title: i18n("This Device")
|
||||||
Layout.fillWidth: true
|
type: DevicesModel.This
|
||||||
title: i18n("Devices")
|
showVerifyButton: false
|
||||||
}
|
}
|
||||||
MobileForm.FormCard {
|
DevicesCard {
|
||||||
Layout.fillWidth: true
|
title: i18n("Verified Devices")
|
||||||
|
type: DevicesModel.Verified
|
||||||
contentItem: ColumnLayout {
|
showVerifyButton: true
|
||||||
spacing: 0
|
|
||||||
MobileForm.AbstractFormDelegate {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
visible: Controller.activeConnection && deviceRepeater.count === 0 // We can assume 0 means loading since there is at least one device
|
|
||||||
contentItem: Kirigami.LoadingPlaceholder { }
|
|
||||||
}
|
|
||||||
Repeater {
|
|
||||||
id: deviceRepeater
|
|
||||||
model: DevicesModel {
|
|
||||||
id: devices
|
|
||||||
}
|
|
||||||
|
|
||||||
Kirigami.LoadingPlaceholder {
|
|
||||||
visible: parent.count === 0 // We can assume 0 means loading since there is at least one device
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate: MobileForm.AbstractFormDelegate {
|
|
||||||
id: deviceDelegate
|
|
||||||
|
|
||||||
property bool editDeviceName: false
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
onClicked: deviceDelegate.editDeviceName = true
|
|
||||||
|
|
||||||
contentItem: RowLayout {
|
|
||||||
spacing: Kirigami.Units.largeSpacing
|
|
||||||
|
|
||||||
Kirigami.Icon {
|
|
||||||
source: "network-connect"
|
|
||||||
implicitWidth: Kirigami.Units.iconSizes.medium
|
|
||||||
implicitHeight: Kirigami.Units.iconSizes.medium
|
|
||||||
}
|
|
||||||
ColumnLayout {
|
|
||||||
id: deviceLabel
|
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: Kirigami.Units.smallSpacing
|
|
||||||
visible: !deviceDelegate.editDeviceName
|
|
||||||
|
|
||||||
QQC2.Label {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
text: model.displayName
|
|
||||||
elide: Text.ElideRight
|
|
||||||
wrapMode: Text.Wrap
|
|
||||||
maximumLineCount: 2
|
|
||||||
}
|
|
||||||
|
|
||||||
QQC2.Label {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
text: model.id + ", Last activity: " + (new Date(model.lastTimestamp)).toLocaleString(Qt.locale(), Locale.ShortFormat)
|
|
||||||
color: Kirigami.Theme.disabledTextColor
|
|
||||||
font: Kirigami.Theme.smallFont
|
|
||||||
elide: Text.ElideRight
|
|
||||||
visible: text !== ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Kirigami.ActionTextField {
|
|
||||||
id: nameField
|
|
||||||
Accessible.description: i18n("New device name")
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.preferredHeight: deviceLabel.implicitHeight
|
|
||||||
visible: deviceDelegate.editDeviceName
|
|
||||||
|
|
||||||
text: model.displayName
|
|
||||||
|
|
||||||
rightActions: [
|
|
||||||
Kirigami.Action {
|
|
||||||
text: i18n("Cancel editing display name")
|
|
||||||
icon.name: "edit-delete-remove"
|
|
||||||
onTriggered: {
|
|
||||||
deviceDelegate.editDeviceName = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Kirigami.Action {
|
|
||||||
text: i18n("Confirm new display name")
|
|
||||||
icon.name: "checkmark"
|
|
||||||
visible: nameField.text != model.displayName
|
|
||||||
onTriggered: {
|
|
||||||
devices.setName(model.index, nameField.text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
onAccepted: devices.setName(model.index, nameField.text)
|
|
||||||
}
|
|
||||||
QQC2.ToolButton {
|
|
||||||
display: QQC2.AbstractButton.IconOnly
|
|
||||||
action: Kirigami.Action {
|
|
||||||
id: editDeviceAction
|
|
||||||
text: i18n("Edit device name")
|
|
||||||
icon.name: "document-edit"
|
|
||||||
onTriggered: deviceDelegate.editDeviceName = true
|
|
||||||
}
|
|
||||||
QQC2.ToolTip {
|
|
||||||
text: editDeviceAction.text
|
|
||||||
delay: Kirigami.Units.toolTipDelay
|
|
||||||
}
|
|
||||||
}
|
|
||||||
QQC2.ToolButton {
|
|
||||||
display: QQC2.AbstractButton.IconOnly
|
|
||||||
visible: Controller.encryptionSupported
|
|
||||||
action: Kirigami.Action {
|
|
||||||
id: verifyDeviceAction
|
|
||||||
text: i18n("Verify device")
|
|
||||||
icon.name: "security-low-symbolic"
|
|
||||||
onTriggered: {
|
|
||||||
devices.connection.startKeyVerificationSession(devices.connection.localUserId, model.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
QQC2.ToolTip {
|
|
||||||
text: verifyDeviceAction.text
|
|
||||||
delay: Kirigami.Units.toolTipDelay
|
|
||||||
}
|
|
||||||
}
|
|
||||||
QQC2.ToolButton {
|
|
||||||
display: QQC2.AbstractButton.IconOnly
|
|
||||||
action: Kirigami.Action {
|
|
||||||
id: logoutDeviceAction
|
|
||||||
text: i18n("Logout device")
|
|
||||||
icon.name: "edit-delete-remove"
|
|
||||||
onTriggered: {
|
|
||||||
passwordSheet.index = index
|
|
||||||
passwordSheet.open()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
QQC2.ToolTip {
|
|
||||||
text: logoutDeviceAction.text
|
|
||||||
delay: Kirigami.Units.toolTipDelay
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
DevicesCard {
|
||||||
|
title: i18n("Unverified Devices")
|
||||||
|
type: DevicesModel.Unverified
|
||||||
|
showVerifyButton: true
|
||||||
|
}
|
||||||
|
DevicesCard {
|
||||||
|
title: i18n("Devices without Encryption Support")
|
||||||
|
type: DevicesModel.Unencrypted
|
||||||
|
showVerifyButton: false
|
||||||
|
}
|
||||||
|
|
||||||
|
MobileForm.AbstractFormDelegate {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
visible: Controller.activeConnection && devicesModel.count === 0 // We can assume 0 means loading since there is at least one device
|
||||||
|
contentItem: Kirigami.LoadingPlaceholder { }
|
||||||
|
}
|
||||||
|
|
||||||
Kirigami.InlineMessage {
|
Kirigami.InlineMessage {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.maximumWidth: Kirigami.Units.gridUnit * 30
|
Layout.maximumWidth: Kirigami.Units.gridUnit * 30
|
||||||
@@ -174,7 +64,7 @@ Kirigami.ScrollablePage {
|
|||||||
Kirigami.OverlaySheet {
|
Kirigami.OverlaySheet {
|
||||||
id: passwordSheet
|
id: passwordSheet
|
||||||
|
|
||||||
property var index
|
property string deviceId
|
||||||
|
|
||||||
title: i18n("Remove device")
|
title: i18n("Remove device")
|
||||||
Kirigami.FormLayout {
|
Kirigami.FormLayout {
|
||||||
@@ -186,7 +76,7 @@ Kirigami.ScrollablePage {
|
|||||||
QQC2.Button {
|
QQC2.Button {
|
||||||
text: i18n("Confirm")
|
text: i18n("Confirm")
|
||||||
onClicked: {
|
onClicked: {
|
||||||
devices.logout(passwordSheet.index, passwordField.text)
|
devicesModel.logout(passwordSheet.deviceId, passwordField.text)
|
||||||
passwordField.text = ""
|
passwordField.text = ""
|
||||||
passwordSheet.close()
|
passwordSheet.close()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ import org.kde.kirigami 2.18 as Kirigami
|
|||||||
import QtQuick.Layouts 1.15
|
import QtQuick.Layouts 1.15
|
||||||
|
|
||||||
Kirigami.CategorizedSettings {
|
Kirigami.CategorizedSettings {
|
||||||
|
id: settingsPage
|
||||||
|
|
||||||
|
required property var connection
|
||||||
|
|
||||||
objectName: "settingsPage"
|
objectName: "settingsPage"
|
||||||
actions: [
|
actions: [
|
||||||
Kirigami.SettingAction {
|
Kirigami.SettingAction {
|
||||||
@@ -57,6 +61,11 @@ Kirigami.CategorizedSettings {
|
|||||||
text: i18n("Devices")
|
text: i18n("Devices")
|
||||||
icon.name: "computer"
|
icon.name: "computer"
|
||||||
page: Qt.resolvedUrl("DevicesPage.qml")
|
page: Qt.resolvedUrl("DevicesPage.qml")
|
||||||
|
initialProperties: {
|
||||||
|
return {
|
||||||
|
connection: settingsPage.connection
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Kirigami.SettingAction {
|
Kirigami.SettingAction {
|
||||||
actionName: "aboutNeochat"
|
actionName: "aboutNeochat"
|
||||||
|
|||||||
@@ -108,6 +108,8 @@
|
|||||||
<file alias="AccountsPage.qml">qml/Settings/AccountsPage.qml</file>
|
<file alias="AccountsPage.qml">qml/Settings/AccountsPage.qml</file>
|
||||||
<file alias="AccountEditorPage.qml">qml/Settings/AccountEditorPage.qml</file>
|
<file alias="AccountEditorPage.qml">qml/Settings/AccountEditorPage.qml</file>
|
||||||
<file alias="DevicesPage.qml">qml/Settings/DevicesPage.qml</file>
|
<file alias="DevicesPage.qml">qml/Settings/DevicesPage.qml</file>
|
||||||
|
<file alias="DeviceDelegate.qml">qml/Settings/DeviceDelegate.qml</file>
|
||||||
|
<file alias="DevicesCard.qml">qml/Settings/DevicesCard.qml</file>
|
||||||
<file alias="About.qml">qml/Settings/About.qml</file>
|
<file alias="About.qml">qml/Settings/About.qml</file>
|
||||||
<file alias="AboutKDE.qml">qml/Settings/AboutKDE.qml</file>
|
<file alias="AboutKDE.qml">qml/Settings/AboutKDE.qml</file>
|
||||||
<file alias="SonnetConfigPage.qml">qml/Settings/SonnetConfigPage.qml</file>
|
<file alias="SonnetConfigPage.qml">qml/Settings/SonnetConfigPage.qml</file>
|
||||||
|
|||||||
Reference in New Issue
Block a user