From c344a3ee55f6f155202552725087e55477b5bd5c Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sun, 3 Mar 2024 00:19:19 +0100 Subject: [PATCH] Devtools: Implement changing room state --- src/CMakeLists.txt | 1 + src/devtools/RoomData.qml | 25 ++++--- src/devtools/StateKeys.qml | 20 ++++-- src/models/statekeysmodel.cpp | 12 ++-- src/models/statekeysmodel.h | 7 +- src/models/statemodel.cpp | 20 ++++-- src/models/statemodel.h | 10 ++- src/neochatroom.cpp | 5 ++ src/neochatroom.h | 2 + src/qml/EditStateDialog.qml | 120 +++++++++++++++++++++++++++++++++ src/qml/MessageSourceSheet.qml | 38 ++++++++++- src/utils.cpp | 7 ++ src/utils.h | 2 + 13 files changed, 236 insertions(+), 33 deletions(-) create mode 100644 src/qml/EditStateDialog.qml diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 937a14abb..c2f14e6cb 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -277,6 +277,7 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN qml/AccountSwitchDialog.qml qml/ConfirmLeaveDialog.qml qml/CodeMaximizeComponent.qml + qml/EditStateDialog.qml RESOURCES qml/confetti.png qml/glowdot.png diff --git a/src/devtools/RoomData.qml b/src/devtools/RoomData.qml index 8a1329dc3..562025058 100644 --- a/src/devtools/RoomData.qml +++ b/src/devtools/RoomData.qml @@ -74,14 +74,9 @@ ColumnLayout { description: i18ncp("'Event' being some JSON data, not something physically happening.", "%1 event of this type", "%1 events of this type", model.eventCount) onClicked: { if (model.eventCount === 1) { - onClicked: applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet'), { - sourceText: stateModel.stateEventJson(stateModel.index(model.index, 0)) - }, { - title: i18n("Event Source"), - width: Kirigami.Units.gridUnit * 25 - }) + openEventSource(model.type, model.stateKey); } else { - pageStack.pushDialogLayer(stateKeysComponent, { + pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat.devtools', 'StateKeys'), { room: root.room, eventType: model.type }, { @@ -91,9 +86,17 @@ ColumnLayout { } } } - Component { - id: stateKeysComponent - StateKeys {} - } + } + function openEventSource(type: string, stateKey: string): void { + onClicked: applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet'), { + model: stateModel, + allowEdit: true, + room: root.room, + type: type, + stateKey: stateKey, + }, { + title: i18n("Event Source"), + width: Kirigami.Units.gridUnit * 25 + }); } } diff --git a/src/devtools/StateKeys.qml b/src/devtools/StateKeys.qml index 09677796b..6b69bf297 100644 --- a/src/devtools/StateKeys.qml +++ b/src/devtools/StateKeys.qml @@ -31,13 +31,21 @@ FormCard.FormCardPage { delegate: FormCard.FormButtonDelegate { text: model.stateKey - onClicked: applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet'), { - sourceText: stateKeysModel.stateEventJson(stateKeysModel.index(model.index, 0)) - }, { - title: i18nc("@title:window", "Event Source"), - width: Kirigami.Units.gridUnit * 25 - }) + onClicked: openEventSource(model.stateKey) } } } + + function openEventSource(stateKey: string): void { + applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet'), { + model: stateKeysModel, + allowEdit: true, + room: root.room, + type: root.eventType, + stateKey: stateKey + }, { + title: i18nc("@title:window", "Event Source"), + width: Kirigami.Units.gridUnit * 25 + }); + } } diff --git a/src/models/statekeysmodel.cpp b/src/models/statekeysmodel.cpp index fd1b97958..d11f0cccc 100644 --- a/src/models/statekeysmodel.cpp +++ b/src/models/statekeysmodel.cpp @@ -77,12 +77,14 @@ void StateKeysModel::setEventType(const QString &eventType) loadState(); } -QByteArray StateKeysModel::stateEventJson(const QModelIndex &index) +QByteArray StateKeysModel::stateEventJson(const QString &type, const QString &stateKey) { - const auto row = index.row(); - const auto event = m_stateKeys[row]; - const auto json = event->fullJson(); - return QJsonDocument(json).toJson(); + return QJsonDocument(m_room->currentState().get(type, stateKey)->fullJson()).toJson(); +} + +QByteArray StateKeysModel::stateEventContentJson(const QString &type, const QString &stateKey) +{ + return QJsonDocument(m_room->currentState().get(type, stateKey)->contentJson()).toJson(); } #include "moc_statekeysmodel.cpp" diff --git a/src/models/statekeysmodel.h b/src/models/statekeysmodel.h index 9de91a433..7c5f3fd25 100644 --- a/src/models/statekeysmodel.h +++ b/src/models/statekeysmodel.h @@ -69,7 +69,12 @@ public: /** * @brief Get the full JSON for an event. */ - Q_INVOKABLE QByteArray stateEventJson(const QModelIndex &index); + Q_INVOKABLE QByteArray stateEventJson(const QString &type, const QString &stateKey); + + /** + * @brief Get the content JSON for an event. + */ + Q_INVOKABLE QByteArray stateEventContentJson(const QString &type, const QString &stateKey); Q_SIGNALS: void roomChanged(); diff --git a/src/models/statemodel.cpp b/src/models/statemodel.cpp index 9a841fd0f..1d11f1864 100644 --- a/src/models/statemodel.cpp +++ b/src/models/statemodel.cpp @@ -10,7 +10,11 @@ StateModel::StateModel(QObject *parent) QHash StateModel::roleNames() const { - return {{TypeRole, "type"}, {EventCountRole, "eventCount"}}; + return { + {TypeRole, "type"}, + {EventCountRole, "eventCount"}, + {StateKeyRole, "stateKey"}, + }; } QVariant StateModel::data(const QModelIndex &index, int role) const { @@ -20,6 +24,8 @@ QVariant StateModel::data(const QModelIndex &index, int role) const return m_stateEvents.keys()[row]; case EventCountRole: return m_stateEvents.values()[row].count(); + case StateKeyRole: + return m_stateEvents.values()[row][0]; } return {}; } @@ -63,14 +69,14 @@ void StateModel::setRoom(NeoChatRoom *room) }); } -QByteArray StateModel::stateEventJson(const QModelIndex &index) +QByteArray StateModel::stateEventJson(const QString &type, const QString &stateKey) { - auto row = index.row(); - const auto type = m_stateEvents.keys()[row]; - const auto stateKey = m_stateEvents.values()[row][0]; - const auto event = m_room->currentState().get(type, stateKey); + return QJsonDocument(m_room->currentState().get(type, stateKey)->fullJson()).toJson(); +} - return QJsonDocument(event->fullJson()).toJson(); +QByteArray StateModel::stateEventContentJson(const QString &type, const QString &stateKey) +{ + return QJsonDocument(m_room->currentState().get(type, stateKey)->contentJson()).toJson(); } #include "moc_statemodel.cpp" diff --git a/src/models/statemodel.h b/src/models/statemodel.h index 9bdad1335..49f9ae751 100644 --- a/src/models/statemodel.h +++ b/src/models/statemodel.h @@ -30,6 +30,7 @@ public: enum Roles { TypeRole = 0, /**< The type of the state event. */ EventCountRole, /**< Number of events of this type. */ + StateKeyRole, /** m_room; /** - * @brief A map from state event type to number of events of that type + * @brief A map from state event type to state keys */ QMap> m_stateEvents; void loadState(); diff --git a/src/neochatroom.cpp b/src/neochatroom.cpp index 6375e8131..9cd3ff8da 100644 --- a/src/neochatroom.cpp +++ b/src/neochatroom.cpp @@ -1979,4 +1979,9 @@ User *NeoChatRoom::invitingUser() const return connection()->user(currentState().get(connection()->userId())->senderId()); } +void NeoChatRoom::setRoomState(const QString &type, const QString &stateKey, const QByteArray &content) +{ + setState(type, stateKey, QJsonDocument::fromJson(content).object()); +} + #include "moc_neochatroom.cpp" diff --git a/src/neochatroom.h b/src/neochatroom.h index 02e29d04f..ab4216d89 100644 --- a/src/neochatroom.h +++ b/src/neochatroom.h @@ -659,6 +659,8 @@ public: * */ Q_INVOKABLE void setCanonicalAlias(const QString &newAlias); + Q_INVOKABLE void setRoomState(const QString &type, const QString &stateKey, const QByteArray &content); + PushNotificationState::State pushNotificationState() const; void setPushNotificationState(PushNotificationState::State state); diff --git a/src/qml/EditStateDialog.qml b/src/qml/EditStateDialog.qml new file mode 100644 index 000000000..22adee8ae --- /dev/null +++ b/src/qml/EditStateDialog.qml @@ -0,0 +1,120 @@ +// SPDX-FileCopyrightText: 2024 Tobias Fella +// SPDX-License-Identifier: GPL-2.0-or-later + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts + +import org.kde.kirigami as Kirigami +import org.kde.syntaxhighlighting + +import org.kde.neochat + +Kirigami.Page { + id: root + + required property string sourceText + property bool allowEdit: false + + property NeoChatRoom room + property string type + property string stateKey + + topPadding: 0 + leftPadding: 0 + rightPadding: 0 + bottomPadding: 0 + + title: i18nc("@title As in 'edit the state of this rooms'", "Edit State") + + actions: [ + Kirigami.Action { + text: i18nc("@action", "Revert changes") + icon.name: "document-revert" + onTriggered: sourceTextArea.text = root.sourceText + enabled: sourceTextArea.text !== root.sourceText + }, + Kirigami.Action { + text: i18nc("@action As in 'Apply the changes'", "Apply") + icon.name: "document-edit" + onTriggered: { + root.room.setRoomState(root.type, root.stateKey, sourceTextArea.text); + root.closeDialog(); + } + enabled: QmlUtils.isValidJson(sourceTextArea.text) + } + ] + + QQC2.ScrollView { + id: scrollView + anchors.fill: parent + contentWidth: availableWidth + + QQC2.TextArea { + id: sourceTextArea + Layout.fillWidth: true + + leftPadding: lineNumberColumn.width + lineNumberColumn.anchors.leftMargin + Kirigami.Units.smallSpacing * 2 + + text: root.sourceText + textFormat: TextEdit.PlainText + wrapMode: TextEdit.Wrap + + Kirigami.SpellCheck.enabled: false + + onWidthChanged: lineModel.resetModel() + onHeightChanged: lineModel.resetModel() + + SyntaxHighlighter { + textEdit: sourceTextArea + definition: "JSON" + repository: Repository + } + + ColumnLayout { + id: lineNumberColumn + + anchors { + top: sourceTextArea.top + topMargin: sourceTextArea.topPadding + left: sourceTextArea.left + leftMargin: Kirigami.Units.smallSpacing + } + spacing: 0 + Repeater { + id: repeater + model: LineModel { + id: lineModel + document: sourceTextArea.textDocument + } + delegate: QQC2.Label { + id: label + required property int index + required property int docLineHeight + Layout.fillWidth: true + Layout.preferredHeight: docLineHeight + topPadding: 1 + horizontalAlignment: Text.AlignRight + text: index + 1 + color: Kirigami.Theme.disabledTextColor + } + } + } + + background: Rectangle { + Kirigami.Theme.colorSet: Kirigami.Theme.View + Kirigami.Theme.inherit: false + color: Kirigami.Theme.backgroundColor + } + } + } + + Kirigami.Separator { + anchors { + top: parent.top + bottom: parent.bottom + left: parent.left + leftMargin: lineNumberColumn.width + lineNumberColumn.anchors.leftMargin + Kirigami.Units.smallSpacing + } + } +} diff --git a/src/qml/MessageSourceSheet.qml b/src/qml/MessageSourceSheet.qml index 27d3977af..0936d700e 100644 --- a/src/qml/MessageSourceSheet.qml +++ b/src/qml/MessageSourceSheet.qml @@ -13,15 +13,51 @@ import org.kde.neochat Kirigami.Page { id: root - property string sourceText + + property var model + property NeoChatRoom room + + property string type + property string stateKey + + property bool allowEdit: false + + property string contentJson: model.stateEventContentJson(root.type, root.stateKey) + property string sourceText: model.stateEventJson(root.type, root.stateKey) topPadding: 0 leftPadding: 0 rightPadding: 0 bottomPadding: 0 + Connections { + enabled: root.model + target: root.room + function onChanged(): void { + root.contentJson = model.stateEventContentJson(root.type, root.stateKey); + root.sourceText = model.stateEventJson(root.type, root.stateKey); + } + } + title: i18n("Event Source") + actions: [ + Kirigami.Action { + text: i18nc("@action As in 'edit the state of this rooms'", "Edit state") + icon.name: "document-edit" + visible: root.allowEdit + enabled: room.canSendState(root.type) && (!root.stateKey.startsWith("@") || root.stateKey === root.room.connection.localUserId) && root.type !== "m.room.create" + onTriggered: pageStack.pushDialogLayer(Qt.createComponent("org.kde.neochat", "EditStateDialog.qml"), { + room: root.room, + type: root.type, + stateKey: root.stateKey, + sourceText: root.contentJson, + }, { + title: i18nc("@title As in 'edit the state of this rooms'", "Edit State") + }) + } + ] + QQC2.ScrollView { id: scrollView anchors.fill: parent diff --git a/src/utils.cpp b/src/utils.cpp index 322596fd7..aeec5dee9 100644 --- a/src/utils.cpp +++ b/src/utils.cpp @@ -3,6 +3,8 @@ #include "utils.h" +#include + using namespace Quotient; static const QVariantMap emptyUser = { @@ -41,3 +43,8 @@ QVariantMap QmlUtils::getUser(User *user) const {QStringLiteral("object"), QVariant::fromValue(user)}, }; } + +bool QmlUtils::isValidJson(const QByteArray &json) +{ + return !QJsonDocument::fromJson(json).isNull(); +} diff --git a/src/utils.h b/src/utils.h index 89251fecb..c7c8e204d 100644 --- a/src/utils.h +++ b/src/utils.h @@ -30,11 +30,13 @@ public: } Q_INVOKABLE QVariantMap getUser(Quotient::User *user) const; + Q_INVOKABLE bool isValidJson(const QByteArray &json); private: QmlUtils() = default; }; + namespace Utils {