Devtools Improvements

- Now has tabs setup as more features are added
- First extra tab has basic server info
- Use mobileform to make it look nicer
- For the room data tab allow the room to be changed from within devtools
- For the room data tab allow m.room.member events to be filtered out so other event types can be found easily
- For the room data tab allow viewing room account data

network/neochat#557
This commit is contained in:
James Graham
2023-04-29 15:20:51 +00:00
committed by Tobias Fella
parent ca805917de
commit 85b40ca536
14 changed files with 347 additions and 23 deletions

View File

@@ -44,6 +44,7 @@ add_library(neochat STATIC
models/actionsmodel.cpp
models/serverlistmodel.cpp
models/statemodel.cpp
models/statefiltermodel.cpp
filetransferpseudojob.cpp
models/searchmodel.cpp
texthandler.cpp

View File

@@ -769,3 +769,19 @@ QString Controller::activeAccountLabel() const
}
return m_connection->accountDataJson("org.kde.neochat.account_label")["account_label"].toString();
}
QVariantList Controller::getSupportedRoomVersions(Quotient::Connection *connection)
{
auto roomVersions = connection->availableRoomVersions();
QVariantList supportedRoomVersions;
for (const Quotient::Connection::SupportedRoomVersion &v : roomVersions) {
QVariantMap roomVersionMap;
roomVersionMap.insert("id", v.id);
roomVersionMap.insert("status", v.status);
roomVersionMap.insert("isStable", v.isStable());
supportedRoomVersions.append(roomVersionMap);
}
return supportedRoomVersions;
}

View File

@@ -214,6 +214,8 @@ public:
*/
Q_INVOKABLE void forceRefreshTextDocument(QQuickTextDocument *textDocument, QQuickItem *item);
Q_INVOKABLE QVariantList getSupportedRoomVersions(Quotient::Connection *connection);
private:
explicit Controller(QObject *parent = nullptr);

View File

@@ -71,6 +71,7 @@
#ifdef QUOTIENT_07
#include "pollhandler.h"
#endif
#include "models/statefiltermodel.h"
#include "roommanager.h"
#include "spacehierarchycache.h"
#include "urlhelper.h"
@@ -234,6 +235,7 @@ int main(int argc, char *argv[])
qmlRegisterType<LinkPreviewer>("org.kde.neochat", 1, 0, "LinkPreviewer");
qmlRegisterType<CompletionModel>("org.kde.neochat", 1, 0, "CompletionModel");
qmlRegisterType<StateModel>("org.kde.neochat", 1, 0, "StateModel");
qmlRegisterType<StateFilterModel>("org.kde.neochat", 1, 0, "StateFilterModel");
qmlRegisterType<SearchModel>("org.kde.neochat", 1, 0, "SearchModel");
#ifdef QUOTIENT_07
qmlRegisterType<PollHandler>("org.kde.neochat", 1, 0, "PollHandler");

View File

@@ -0,0 +1,32 @@
// 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 "statefiltermodel.h"
#include "statemodel.h"
bool StateFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
{
Q_UNUSED(sourceParent);
// No need to run the check if there are no items in m_stateEventTypesFiltered.
if (m_stateEventTypesFiltered.empty()) {
return true;
}
return !m_stateEventTypesFiltered.contains(sourceModel()->data(sourceModel()->index(sourceRow, 0), StateModel::TypeRole).toString());
}
void StateFilterModel::addStateEventTypeFiltered(const QString &stateEventType)
{
if (!m_stateEventTypesFiltered.contains(stateEventType)) {
m_stateEventTypesFiltered.append(stateEventType);
invalidateFilter();
}
}
void StateFilterModel::removeStateEventTypeFiltered(const QString &stateEventType)
{
if (m_stateEventTypesFiltered.contains(stateEventType)) {
m_stateEventTypesFiltered.removeAll(stateEventType);
invalidateFilter();
}
}

View File

@@ -0,0 +1,44 @@
// 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 <QSortFilterProxyModel>
/**
* @class StateFilterModel
*
* This class creates a custom QSortFilterProxyModel for filtering a list of state events.
* Event types can be filtered out by adding them to m_stateEventTypesFiltered.
*/
class StateFilterModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
/**
* @brief Custom filter function checking if an event type has been filtered out.
*
* The filter rejects a row if the state event type has been added to m_stateEventTypesFiltered.
*
* @sa m_stateEventTypesFiltered
*/
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
/**
* @brief Add an event type to m_stateEventTypesFiltered.
*
* @sa m_stateEventTypesFiltered
*/
Q_INVOKABLE void addStateEventTypeFiltered(const QString &stateEventType);
/**
* @brief Remove an event type from m_stateEventTypesFiltered.
*
* @sa m_stateEventTypesFiltered
*/
Q_INVOKABLE void removeStateEventTypeFiltered(const QString &stateEventType);
private:
QStringList m_stateEventTypesFiltered;
};

View File

@@ -10,7 +10,7 @@ StateModel::StateModel(QObject *parent)
QHash<int, QByteArray> StateModel::roleNames() const
{
return {{TypeRole, "type"}, {StateKeyRole, "stateKey"}, {SourceRole, "source"}};
return {{TypeRole, "type"}, {StateKeyRole, "stateKey"}};
}
QVariant StateModel::data(const QModelIndex &index, int role) const
{
@@ -18,11 +18,9 @@ QVariant StateModel::data(const QModelIndex &index, int role) const
auto row = index.row();
switch (role) {
case TypeRole:
return m_room->currentState().events().keys()[row].first;
return m_stateEvents[row].first;
case StateKeyRole:
return m_room->currentState().events().keys()[row].second;
case SourceRole:
return QJsonDocument(m_room->currentState().events()[m_room->currentState().events().keys()[row]]->fullJson()).toJson();
return m_stateEvents[row].second;
}
#endif
return {};
@@ -48,9 +46,27 @@ void StateModel::setRoom(NeoChatRoom *room)
m_room = room;
Q_EMIT roomChanged();
beginResetModel();
m_stateEvents.clear();
#ifdef QUOTIENT_07
m_stateEvents = m_room->currentState().events().keys();
#endif
endResetModel();
connect(room, &NeoChatRoom::changed, this, [this] {
beginResetModel();
m_stateEvents.clear();
#ifdef QUOTIENT_07
m_stateEvents = m_room->currentState().events().keys();
#endif
endResetModel();
});
}
QByteArray StateModel::stateEventJson(const QModelIndex &index)
{
auto row = index.row();
#ifdef QUOTIENT_07
return QJsonDocument(m_room->currentState().events()[m_stateEvents[row]]->fullJson()).toJson();
#else
return {};
#endif
}

View File

@@ -26,9 +26,8 @@ public:
* @brief Defines the model roles.
*/
enum Roles {
TypeRole, /**< The type of the state event. */
TypeRole = 0, /**< The type of the state event. */
StateKeyRole, /**< The state key of the state event. */
SourceRole, /**< The full event source JSON. */
};
Q_ENUM(Roles);
@@ -57,10 +56,25 @@ public:
* @sa Roles, QAbstractItemModel::roleNames()
*/
QHash<int, QByteArray> roleNames() const override;
/**
* @brief Get the full JSON for an event.
*
* This is used to avoid having the model hold all the JSON data. The JSON for
* a single item is only ever shown, no need to access simultaneously.
*/
Q_INVOKABLE QByteArray stateEventJson(const QModelIndex &index);
Q_SIGNALS:
void roomChanged();
private:
NeoChatRoom *m_room = nullptr;
/**
* @brief The room state events in a QList.
*
* This is done for performance, accessing all the data from the parent QHash
* was slower.
*/
QList<std::pair<QString, QString>> m_stateEvents;
};

View File

@@ -1908,3 +1908,8 @@ void NeoChatRoom::sendLocation(float lat, float lon, const QString &description)
};
postJson("m.room.message", content);
}
QByteArray NeoChatRoom::roomAcountDataJson(const QString &eventType)
{
return QJsonDocument(accountData(eventType)->fullJson()).toJson();
}

View File

@@ -749,6 +749,11 @@ public:
Q_INVOKABLE PollHandler *poll(const QString &eventId);
#endif
/**
* @brief Get the full Json data for a given room account data event.
*/
Q_INVOKABLE QByteArray roomAcountDataJson(const QString &eventType);
private:
QSet<const Quotient::RoomEvent *> highlights;

View File

@@ -0,0 +1,114 @@
// 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
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.kirigamiaddons.labs.mobileform 0.1 as MobileForm
import org.kde.kitemmodels 1.0
import org.kde.neochat 1.0
ColumnLayout {
MobileForm.FormCard {
Layout.fillWidth: true
contentItem: ColumnLayout {
spacing: 0
MobileForm.FormComboBoxDelegate {
text: i18n("Room")
textRole: "name"
valueRole: "id"
model: RoomListModel {
id: roomListModel
connection: Controller.activeConnection
}
Component.onCompleted: currentIndex = indexOfValue(room.id)
onCurrentValueChanged: room = roomListModel.roomByAliasOrId(currentValue)
}
MobileForm.FormCheckDelegate {
text: i18n("Show m.room.member events")
checked: true
onToggled: {
if (checked) {
stateEventFilterModel.removeStateEventTypeFiltered("m.room.member");
} else {
stateEventFilterModel.addStateEventTypeFiltered("m.room.member");
}
}
}
MobileForm.FormCheckDelegate {
id: roomAccoutnDataVisibleCheck
text: i18n("Show room account data")
checked: false
}
}
}
MobileForm.FormCard {
Layout.fillWidth: true
visible: roomAccoutnDataVisibleCheck.checked
contentItem: ColumnLayout {
spacing: 0
MobileForm.FormCardHeader {
title: i18n("Room Account Data for %1 - %2", room.displayName, room.id)
}
Repeater {
model: room.accountDataEventTypes
delegate: MobileForm.FormTextDelegate {
text: modelData
onClicked: applicationWindow().pageStack.pushDialogLayer("qrc:/MessageSourceSheet.qml", {
"sourceText": room.roomAcountDataJson(text)
}, {
"title": i18n("Event Source"),
"width": Kirigami.Units.gridUnit * 25
})
}
}
}
}
MobileForm.FormCard {
Layout.fillWidth: true
Layout.fillHeight: true
contentItem: ColumnLayout {
spacing: 0
MobileForm.FormCardHeader {
id: stateEventListHeader
title: i18n("Room State for %1", room.displayName)
subtitle: room.id
}
QQC2.ScrollView {
Layout.fillWidth: true
Layout.fillHeight: true
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
ListView {
id: stateEventListView
clip: true
model: StateFilterModel {
id: stateEventFilterModel
sourceModel: StateModel {
id: stateModel
room: devtoolsPage.room
}
}
delegate: MobileForm.FormTextDelegate {
text: model.type
description: model.stateKey
onClicked: applicationWindow().pageStack.pushDialogLayer('qrc:/MessageSourceSheet.qml', {
sourceText: stateModel.stateEventJson(stateEventFilterModel.mapToSource(stateEventFilterModel.index(model.index, 0)))
}, {
title: i18n("Event Source"),
width: Kirigami.Units.gridUnit * 25
});
}
}
}
}
}
}

View File

@@ -0,0 +1,63 @@
// 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
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.kirigamiaddons.labs.mobileform 0.1 as MobileForm
import org.kde.kitemmodels 1.0
import org.kde.neochat 1.0
ColumnLayout {
MobileForm.FormCard {
Layout.fillWidth: true
contentItem: ColumnLayout {
spacing: 0
MobileForm.FormCardHeader {
title: i18n("Server Capabilities")
}
MobileForm.FormTextDelegate {
text: i18n("Can change password")
description: Controller.activeConnection.canChangePassword
}
}
}
MobileForm.FormCard {
Layout.fillWidth: true
contentItem: ColumnLayout {
spacing: 0
MobileForm.FormCardHeader {
title: i18n("Default Room Version")
}
MobileForm.FormTextDelegate {
text: Controller.activeConnection.defaultRoomVersion
}
}
}
MobileForm.FormCard {
Layout.fillWidth: true
contentItem: ColumnLayout {
spacing: 0
MobileForm.FormCardHeader {
title: i18n("Available Room Versions")
}
Repeater {
model: Controller.getSupportedRoomVersions(room.connection)
delegate: MobileForm.FormTextDelegate {
text: modelData.id
contentItem.children: QQC2.Label {
text: modelData.status
color: Kirigami.Theme.disabledTextColor
}
}
}
}
}
Item {
Layout.fillHeight: true
}
}

View File

@@ -3,32 +3,40 @@
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.20 as Kirigami
import org.kde.neochat 1.0
Kirigami.ScrollablePage {
Kirigami.Page {
id: devtoolsPage
property var room
title: i18n("Room State - %1", room.displayName)
title: i18n("Developer Tools")
ListView {
anchors.fill: parent
model: StateModel {
room: devtoolsPage.room
leftPadding: 0
rightPadding: 0
header: QQC2.TabBar {
id: tabBar
QQC2.TabButton {
text: qsTr("Room Data")
}
delegate: Kirigami.BasicListItem {
text: model.type
subtitle: model.stateKey
onClicked: applicationWindow().pageStack.pushDialogLayer('qrc:/MessageSourceSheet.qml', {
sourceText: model.source
}, {
title: i18n("Event Source"),
width: Kirigami.Units.gridUnit * 25
});
QQC2.TabButton {
text: qsTr("Server Info")
}
}
StackLayout {
id: swipeView
anchors.fill: parent
currentIndex: tabBar.currentIndex
RoomData {}
ServerData {}
}
}

View File

@@ -41,6 +41,8 @@
<file alias="CompletionMenu.qml">qml/Component/ChatBox/CompletionMenu.qml</file>
<file alias="PieProgressBar.qml">qml/Component/ChatBox/PieProgressBar.qml</file>
<file alias="QuickFormatBar.qml">qml/Component/ChatBox/QuickFormatBar.qml</file>
<file alias="RoomData.qml">qml/Component/Devtools/RoomData.qml</file>
<file alias="ServerData.qml">qml/Component/Devtools/ServerData.qml</file>
<file alias="EmojiPicker.qml">qml/Component/Emoji/EmojiPicker.qml</file>
<file alias="ReplyComponent.qml">qml/Component/Timeline/ReplyComponent.qml</file>
<file alias="StateDelegate.qml">qml/Component/Timeline/StateDelegate.qml</file>