From 85b40ca536506a7619b7db9a569cef453e1e5337 Mon Sep 17 00:00:00 2001 From: James Graham Date: Sat, 29 Apr 2023 15:20:51 +0000 Subject: [PATCH] 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 --- src/CMakeLists.txt | 1 + src/controller.cpp | 16 +++ src/controller.h | 2 + src/main.cpp | 2 + src/models/statefiltermodel.cpp | 32 ++++++ src/models/statefiltermodel.h | 44 +++++++++ src/models/statemodel.cpp | 26 ++++- src/models/statemodel.h | 18 +++- src/neochatroom.cpp | 5 + src/neochatroom.h | 5 + src/qml/Component/Devtools/RoomData.qml | 114 ++++++++++++++++++++++ src/qml/Component/Devtools/ServerData.qml | 63 ++++++++++++ src/qml/Page/DevtoolsPage.qml | 40 +++++--- src/res.qrc | 2 + 14 files changed, 347 insertions(+), 23 deletions(-) create mode 100644 src/models/statefiltermodel.cpp create mode 100644 src/models/statefiltermodel.h create mode 100644 src/qml/Component/Devtools/RoomData.qml create mode 100644 src/qml/Component/Devtools/ServerData.qml diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a2db43d12..be65742ff 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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 diff --git a/src/controller.cpp b/src/controller.cpp index de5e8d4da..32e66ecc1 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -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; +} diff --git a/src/controller.h b/src/controller.h index 27e6893d3..73725059b 100644 --- a/src/controller.h +++ b/src/controller.h @@ -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); diff --git a/src/main.cpp b/src/main.cpp index 8710a3412..c9ca63b97 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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("org.kde.neochat", 1, 0, "LinkPreviewer"); qmlRegisterType("org.kde.neochat", 1, 0, "CompletionModel"); qmlRegisterType("org.kde.neochat", 1, 0, "StateModel"); + qmlRegisterType("org.kde.neochat", 1, 0, "StateFilterModel"); qmlRegisterType("org.kde.neochat", 1, 0, "SearchModel"); #ifdef QUOTIENT_07 qmlRegisterType("org.kde.neochat", 1, 0, "PollHandler"); diff --git a/src/models/statefiltermodel.cpp b/src/models/statefiltermodel.cpp new file mode 100644 index 000000000..0d8a8dfbb --- /dev/null +++ b/src/models/statefiltermodel.cpp @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: 2022 James Graham +// 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(); + } +} diff --git a/src/models/statefiltermodel.h b/src/models/statefiltermodel.h new file mode 100644 index 000000000..8a1efebd7 --- /dev/null +++ b/src/models/statefiltermodel.h @@ -0,0 +1,44 @@ +// 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 + +/** + * @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; +}; diff --git a/src/models/statemodel.cpp b/src/models/statemodel.cpp index 18144399d..3f8008c81 100644 --- a/src/models/statemodel.cpp +++ b/src/models/statemodel.cpp @@ -10,7 +10,7 @@ StateModel::StateModel(QObject *parent) QHash 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 +} diff --git a/src/models/statemodel.h b/src/models/statemodel.h index 4da26d69c..ef0ebac63 100644 --- a/src/models/statemodel.h +++ b/src/models/statemodel.h @@ -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 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> m_stateEvents; }; diff --git a/src/neochatroom.cpp b/src/neochatroom.cpp index 487a2ce50..89afd935e 100644 --- a/src/neochatroom.cpp +++ b/src/neochatroom.cpp @@ -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(); +} diff --git a/src/neochatroom.h b/src/neochatroom.h index be109b9cf..cabbe0f49 100644 --- a/src/neochatroom.h +++ b/src/neochatroom.h @@ -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 highlights; diff --git a/src/qml/Component/Devtools/RoomData.qml b/src/qml/Component/Devtools/RoomData.qml new file mode 100644 index 000000000..8dfe89ce4 --- /dev/null +++ b/src/qml/Component/Devtools/RoomData.qml @@ -0,0 +1,114 @@ +// SPDX-FileCopyrightText: 2022 James Graham +// 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 + }); + } + } + } + } + } +} diff --git a/src/qml/Component/Devtools/ServerData.qml b/src/qml/Component/Devtools/ServerData.qml new file mode 100644 index 000000000..57369d919 --- /dev/null +++ b/src/qml/Component/Devtools/ServerData.qml @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: 2022 James Graham +// 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 + } +} diff --git a/src/qml/Page/DevtoolsPage.qml b/src/qml/Page/DevtoolsPage.qml index 07e486f97..fa1794cb7 100644 --- a/src/qml/Page/DevtoolsPage.qml +++ b/src/qml/Page/DevtoolsPage.qml @@ -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 {} + } } diff --git a/src/res.qrc b/src/res.qrc index c4f0150cc..ca80eba76 100644 --- a/src/res.qrc +++ b/src/res.qrc @@ -41,6 +41,8 @@ qml/Component/ChatBox/CompletionMenu.qml qml/Component/ChatBox/PieProgressBar.qml qml/Component/ChatBox/QuickFormatBar.qml + qml/Component/Devtools/RoomData.qml + qml/Component/Devtools/ServerData.qml qml/Component/Emoji/EmojiPicker.qml qml/Component/Timeline/ReplyComponent.qml qml/Component/Timeline/StateDelegate.qml