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
This commit is contained in:
James Graham
2022-10-26 18:23:17 +00:00
parent 0e42042fa4
commit 34e0e0205b
5 changed files with 312 additions and 12 deletions

View File

@@ -40,6 +40,7 @@ add_library(neochat STATIC
completionmodel.cpp completionmodel.cpp
completionproxymodel.cpp completionproxymodel.cpp
actionsmodel.cpp actionsmodel.cpp
serverlistmodel.cpp
) )
add_executable(neochat-app add_executable(neochat-app

View File

@@ -60,6 +60,7 @@
#include "publicroomlistmodel.h" #include "publicroomlistmodel.h"
#include "roomlistmodel.h" #include "roomlistmodel.h"
#include "roommanager.h" #include "roommanager.h"
#include "serverlistmodel.h"
#include "sortfilterroomlistmodel.h" #include "sortfilterroomlistmodel.h"
#include "sortfilterspacelistmodel.h" #include "sortfilterspacelistmodel.h"
#include "spacehierarchycache.h" #include "spacehierarchycache.h"
@@ -203,6 +204,7 @@ int main(int argc, char *argv[])
qmlRegisterType<MessageFilterModel>("org.kde.neochat", 1, 0, "MessageFilterModel"); qmlRegisterType<MessageFilterModel>("org.kde.neochat", 1, 0, "MessageFilterModel");
qmlRegisterType<PublicRoomListModel>("org.kde.neochat", 1, 0, "PublicRoomListModel"); qmlRegisterType<PublicRoomListModel>("org.kde.neochat", 1, 0, "PublicRoomListModel");
qmlRegisterType<UserDirectoryListModel>("org.kde.neochat", 1, 0, "UserDirectoryListModel"); qmlRegisterType<UserDirectoryListModel>("org.kde.neochat", 1, 0, "UserDirectoryListModel");
qmlRegisterType<ServerListModel>("org.kde.neochat", 1, 0, "ServerListModel");
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");

View File

@@ -5,6 +5,8 @@
import QtQuick 2.15 import QtQuick 2.15
import QtQuick.Controls 2.15 import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15 import QtQuick.Layouts 1.15
import Qt.labs.qmlmodels 1.0
import org.kde.kirigami 2.15 as Kirigami import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0 import org.kde.neochat 1.0
@@ -52,27 +54,122 @@ Kirigami.ScrollablePage {
} }
ComboBox { ComboBox {
Layout.maximumWidth: 120
id: serverField 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: { textRole: "url"
if (currentIndex == 0) { valueRole: "url"
server = "" model: ServerListModel {
} else if (currentIndex == 2) { id: serverListModel
server = "matrix.org" }
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: { onActivated: {
if (currentIndex == 1) { if (currentIndex !== count - 1) {
server = editText 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();
}
}
}
}
} }
} }
} }

153
src/serverlistmodel.cpp Normal file
View File

@@ -0,0 +1,153 @@
// SPDX-FileCopyrightText: 2022 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "serverlistmodel.h"
#include "controller.h"
#include <connection.h>
#include <QDebug>
#include <KConfig>
#include <KConfigGroup>
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<Quotient::QueryPublicRoomsJob>(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<int, QByteArray> ServerListModel::roleNames() const
{
return {
{UrlRole, QByteArrayLiteral("url")},
{IsHomeServerRole, QByteArrayLiteral("isHomeServer")},
{IsAddServerDelegateRole, QByteArrayLiteral("isAddServerDelegate")},
{IsDeletableRole, QByteArrayLiteral("isDeletable")},
};
}

47
src/serverlistmodel.h Normal file
View File

@@ -0,0 +1,47 @@
// SPDX-FileCopyrightText: 2022 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <csapi/list_public_rooms.h>
#include <QAbstractListModel>
#include <QPointer>
#include <QUrl>
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<int, QByteArray> 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<Server> m_servers;
QPointer<Quotient::QueryPublicRoomsJob> m_checkServerJob = nullptr;
};