From 34e0e0205bfb13bed0f8e7b2032e6634abe99292 Mon Sep 17 00:00:00 2001 From: James Graham Date: Wed, 26 Oct 2022 18:23:17 +0000 Subject: [PATCH] Improve JoinRoom Server List Initial work to create a model so that new servers can be added to the list. The model will also check if the server is valid before allowing it to be added. Implements network/neochat#11 ### TODO - [x] Add functionality to cache added servers --- src/CMakeLists.txt | 1 + src/main.cpp | 2 + src/qml/Page/JoinRoomPage.qml | 121 ++++++++++++++++++++++++--- src/serverlistmodel.cpp | 153 ++++++++++++++++++++++++++++++++++ src/serverlistmodel.h | 47 +++++++++++ 5 files changed, 312 insertions(+), 12 deletions(-) create mode 100644 src/serverlistmodel.cpp create mode 100644 src/serverlistmodel.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4c8588a6b..4b4ad05c5 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -40,6 +40,7 @@ add_library(neochat STATIC completionmodel.cpp completionproxymodel.cpp actionsmodel.cpp + serverlistmodel.cpp ) add_executable(neochat-app diff --git a/src/main.cpp b/src/main.cpp index cf1b5e68c..11f2715bd 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -60,6 +60,7 @@ #include "publicroomlistmodel.h" #include "roomlistmodel.h" #include "roommanager.h" +#include "serverlistmodel.h" #include "sortfilterroomlistmodel.h" #include "sortfilterspacelistmodel.h" #include "spacehierarchycache.h" @@ -203,6 +204,7 @@ int main(int argc, char *argv[]) qmlRegisterType("org.kde.neochat", 1, 0, "MessageFilterModel"); qmlRegisterType("org.kde.neochat", 1, 0, "PublicRoomListModel"); qmlRegisterType("org.kde.neochat", 1, 0, "UserDirectoryListModel"); + qmlRegisterType("org.kde.neochat", 1, 0, "ServerListModel"); qmlRegisterType("org.kde.neochat", 1, 0, "SortFilterRoomListModel"); qmlRegisterType("org.kde.neochat", 1, 0, "SortFilterSpaceListModel"); qmlRegisterType("org.kde.neochat", 1, 0, "DevicesModel"); diff --git a/src/qml/Page/JoinRoomPage.qml b/src/qml/Page/JoinRoomPage.qml index 6143cbd5a..df9a05881 100644 --- a/src/qml/Page/JoinRoomPage.qml +++ b/src/qml/Page/JoinRoomPage.qml @@ -5,6 +5,8 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 +import Qt.labs.qmlmodels 1.0 + import org.kde.kirigami 2.15 as Kirigami import org.kde.neochat 1.0 @@ -52,27 +54,122 @@ Kirigami.ScrollablePage { } ComboBox { - Layout.maximumWidth: 120 - id: serverField - editable: currentIndex == 1 + // TODO: in KF6 we should be able to switch to using implicitContentWidthPolicy + Layout.preferredWidth: Kirigami.Units.gridUnit * 10 - model: [i18n("Local"), i18n("Global"), "matrix.org"] + Component.onCompleted: currentIndex = 0 - onCurrentIndexChanged: { - if (currentIndex == 0) { - server = "" - } else if (currentIndex == 2) { - server = "matrix.org" + textRole: "url" + valueRole: "url" + model: ServerListModel { + id: serverListModel + } + + delegate: Kirigami.BasicListItem { + id: serverItem + + label: isAddServerDelegate ? i18n("Add New Server") : url + subtitle: isHomeServer ? i18n("Home Server") : "" + + onClicked: if (isAddServerDelegate) { + addServerSheet.open() + } + + trailing: ToolButton { + visible: isAddServerDelegate || isDeletable + icon.name: isAddServerDelegate ? "list-add" : "dialog-close" + text: i18n("Add new server") + Accessible.name: text + display: AbstractButton.IconOnly + + onClicked: { + if (serverField.currentIndex === index && isDeletable) { + serverField.currentIndex = 0 + server = serverField.currentValue + serverField.popup.close() + } + if (isAddServerDelegate) { + addServerSheet.open() + serverItem.clicked() + } else { + serverListModel.removeServerAtIndex(index) + } + } } } - Keys.onReturnPressed: { - if (currentIndex == 1) { - server = editText + onActivated: { + if (currentIndex !== count - 1) { + server = currentValue } } + + Kirigami.OverlaySheet { + id: addServerSheet + + parent: applicationWindow().overlay + + title: i18nc("@title:window", "Add server") + + onSheetOpenChanged: if (!serverUrlField.isValidServer && !sheetOpen) { + serverField.currentIndex = 0 + server = serverField.currentValue + } else if (sheetOpen) { + serverUrlField.forceActiveFocus() + } + + contentItem: Kirigami.FormLayout { + Label { + Layout.minimumWidth: Kirigami.Units.gridUnit * 20 + + text: serverUrlField.length > 0 ? (serverUrlField.acceptableInput ? (serverUrlField.isValidServer ? i18n("Valid server entered") : i18n("This server cannot be resolved or has already been added")) : i18n("The entered text is not a valid url")) : i18n("Enter server url e.g. kde.org") + color: serverUrlField.length > 0 ? (serverUrlField.acceptableInput ? (serverUrlField.isValidServer ? Kirigami.Theme.positiveTextColor : Kirigami.Theme.negativeTextColor) : Kirigami.Theme.negativeTextColor) : Kirigami.Theme.textColor + } + TextField { + id: serverUrlField + + property bool isValidServer: false + + Kirigami.FormData.label: i18n("Server URL") + onTextChanged: { + if(acceptableInput) { + serverListModel.checkServer(text) + } + } + + validator: RegularExpressionValidator { + regularExpression: /^[a-zA-Z0-9-]{1,61}\.([a-zA-Z]{2,}|[a-zA-Z0-9-]{2,}\.[a-zA-Z]{2,3})$/ + } + + Connections { + target: serverListModel + function onServerCheckComplete(url, valid) { + if (url == serverUrlField.text && valid) { + serverUrlField.isValidServer = true + } + } + } + } + + Button { + id: okButton + + text: i18nc("@action:button", "Ok") + enabled: serverUrlField.acceptableInput && serverUrlField.isValidServer + onClicked: { + serverListModel.addServer(serverUrlField.text) + serverField.currentIndex = serverField.indexOfValue(serverUrlField.text) + // console.log(serverField.delegate.label) + server = serverField.currentValue + serverUrlField.text = "" + addServerSheet.close(); + } + } + } + } + } } } diff --git a/src/serverlistmodel.cpp b/src/serverlistmodel.cpp new file mode 100644 index 000000000..8e3579e9f --- /dev/null +++ b/src/serverlistmodel.cpp @@ -0,0 +1,153 @@ +// SPDX-FileCopyrightText: 2022 James Graham +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +#include "serverlistmodel.h" + +#include "controller.h" + +#include + +#include + +#include +#include + +ServerListModel::ServerListModel(QObject *parent) + : QAbstractListModel(parent) +{ + KConfig dataResource("data", KConfig::SimpleConfig, QStandardPaths::AppDataLocation); + KConfigGroup serverGroup(&dataResource, "Servers"); + + QString domain = Controller::instance().activeConnection()->domain(); + + // Add the user's homeserver + m_servers.append(Server{ + domain, + true, + false, + false, + }); + // Add matrix.org + m_servers.append(Server{ + QStringLiteral("matrix.org"), + false, + false, + false, + }); + // Add each of the saved custom servers + for (const auto &i : serverGroup.keyList()) { + m_servers.append(Server{ + serverGroup.readEntry(i, QString()), + false, + false, + true, + }); + } + // Add add server delegate entry + m_servers.append(Server{ + QStringLiteral(""), + false, + true, + false, + }); +} + +QVariant ServerListModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return {}; + } + + if (index.row() >= m_servers.count()) { + qDebug() << "ServerListModel, something's wrong: index.row() >= m_notificationRules.count()"; + return {}; + } + + if (role == UrlRole) { + return m_servers.at(index.row()).url; + } + + if (role == IsHomeServerRole) { + return m_servers.at(index.row()).isHomeServer; + } + + if (role == IsAddServerDelegateRole) { + return m_servers.at(index.row()).isAddServerDelegate; + } + + if (role == IsDeletableRole) { + return m_servers.at(index.row()).isDeletable; + } + + return {}; +} + +int ServerListModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + + return m_servers.count(); +} + +void ServerListModel::checkServer(const QString &url) +{ + KConfig dataResource("data", KConfig::SimpleConfig, QStandardPaths::AppDataLocation); + KConfigGroup serverGroup(&dataResource, "Servers"); + + if (!serverGroup.hasKey(url)) { +#ifdef QUOTIENT_07 + if (Quotient::isJobPending(m_checkServerJob)) { +#else + if (Quotient::isJobRunning(m_checkServerJob)) { +#endif + m_checkServerJob->abandon(); + } + + m_checkServerJob = Controller::instance().activeConnection()->callApi(url, 1); + connect(m_checkServerJob, &Quotient::BaseJob::success, this, [this, url] { + Q_EMIT serverCheckComplete(url, true); + }); + } +} + +void ServerListModel::addServer(const QString &url) +{ + KConfig dataResource("data", KConfig::SimpleConfig, QStandardPaths::AppDataLocation); + KConfigGroup serverGroup(&dataResource, "Servers"); + + if (!serverGroup.hasKey(url)) { + Server newServer = Server{ + url, + false, + false, + true, + }; + + beginInsertRows(QModelIndex(), m_servers.count() - 1, m_servers.count() - 1); + m_servers.insert(rowCount() - 1, newServer); + endInsertRows(); + } + + serverGroup.writeEntry(url, url); +} + +void ServerListModel::removeServerAtIndex(int row) +{ + KConfig dataResource("data", KConfig::SimpleConfig, QStandardPaths::AppDataLocation); + KConfigGroup serverGroup(&dataResource, "Servers"); + serverGroup.deleteEntry(data(index(row), UrlRole).toString()); + + beginRemoveRows(QModelIndex(), row, row); + m_servers.removeAt(row); + endRemoveRows(); +} + +QHash ServerListModel::roleNames() const +{ + return { + {UrlRole, QByteArrayLiteral("url")}, + {IsHomeServerRole, QByteArrayLiteral("isHomeServer")}, + {IsAddServerDelegateRole, QByteArrayLiteral("isAddServerDelegate")}, + {IsDeletableRole, QByteArrayLiteral("isDeletable")}, + }; +} diff --git a/src/serverlistmodel.h b/src/serverlistmodel.h new file mode 100644 index 000000000..fd00c3b30 --- /dev/null +++ b/src/serverlistmodel.h @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2022 James Graham +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +#pragma once + +#include + +#include +#include +#include + +class ServerListModel : public QAbstractListModel +{ + Q_OBJECT + +public: + struct Server { + QString url; + bool isHomeServer; + bool isAddServerDelegate; + bool isDeletable; + }; + enum EventRoles { + UrlRole = Qt::UserRole + 1, + IsHomeServerRole, + IsAddServerDelegateRole, + IsDeletableRole, + }; + + ServerListModel(QObject *parent = nullptr); + + [[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override; + + [[nodiscard]] QHash roleNames() const override; + + Q_INVOKABLE void checkServer(const QString &url); + Q_INVOKABLE void addServer(const QString &url); + Q_INVOKABLE void removeServerAtIndex(int index); + +Q_SIGNALS: + void serverCheckComplete(QString url, bool valid); + +private: + QList m_servers; + QPointer m_checkServerJob = nullptr; +};