diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7434b83cc..30cd36dab 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -163,6 +163,8 @@ add_library(neochat STATIC models/sortfilterroomtreemodel.h mediamanager.cpp mediamanager.h + models/statekeysmodel.cpp + models/statekeysmodel.h ) qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN @@ -327,6 +329,7 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN qml/FeatureFlagPage.qml qml/IgnoredUsersDialog.qml qml/AccountData.qml + qml/StateKeys.qml RESOURCES qml/confetti.png qml/glowdot.png diff --git a/src/models/statekeysmodel.cpp b/src/models/statekeysmodel.cpp new file mode 100644 index 000000000..65d520d79 --- /dev/null +++ b/src/models/statekeysmodel.cpp @@ -0,0 +1,85 @@ +// SPDX-FileCopyrightText: 2024 Tobias Fella +// SPDX-License-Identifier: LGPL-2.0-or-later + +#include "statekeysmodel.h" + +StateKeysModel::StateKeysModel(QObject *parent) + : QAbstractListModel(parent) +{ +} + +QHash StateKeysModel::roleNames() const +{ + return { + {StateKeyRole, "stateKey"}, + }; +} +QVariant StateKeysModel::data(const QModelIndex &index, int role) const +{ + Q_ASSERT(checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid)); + const auto row = index.row(); + switch (role) { + case StateKeyRole: + return m_stateKeys[row]->stateKey(); + } + return {}; +} + +int StateKeysModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return m_stateKeys.count(); +} + +NeoChatRoom *StateKeysModel::room() const +{ + return m_room; +} + +void StateKeysModel::loadState() +{ + if (!m_room || m_eventType.isEmpty()) { + return; + } + + beginResetModel(); + m_stateKeys = m_room->currentState().eventsOfType(m_eventType); + endResetModel(); +} + +void StateKeysModel::setRoom(NeoChatRoom *room) +{ + if (m_room) { + disconnect(m_room, nullptr, this, nullptr); + } + + m_room = room; + Q_EMIT roomChanged(); + loadState(); + + connect(room, &NeoChatRoom::changed, this, [this] { + loadState(); + }); +} + +QString StateKeysModel::eventType() const +{ + return m_eventType; +} + +void StateKeysModel::setEventType(const QString &eventType) +{ + m_eventType = eventType; + Q_EMIT eventTypeChanged(); + loadState(); +} + +QByteArray StateKeysModel::stateEventJson(const QModelIndex &index) +{ + const auto row = index.row(); + const auto event = m_stateKeys[row]; + const auto json = event->fullJson(); + return QJsonDocument(json).toJson(); +} + +#include "moc_statekeysmodel.cpp" diff --git a/src/models/statekeysmodel.h b/src/models/statekeysmodel.h new file mode 100644 index 000000000..a666ab2aa --- /dev/null +++ b/src/models/statekeysmodel.h @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: 2024 Tobias Fella +// SPDX-License-Identifier: LGPL-2.0-or-later + +#pragma once + +#include +#include + +#include "neochatroom.h" + +/** + * @class StateKeysModel + * + * This class defines the model for visualising the state keys for a certain type in a room. + */ +class StateKeysModel : public QAbstractListModel +{ + Q_OBJECT + QML_ELEMENT + + /** + * @brief The current room that the model is getting its state events from. + */ + Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged REQUIRED) + + /** + * @brief The event type to list the stateKeys for + */ + Q_PROPERTY(QString eventType READ eventType WRITE setEventType NOTIFY eventTypeChanged REQUIRED) + +public: + /** + * @brief Defines the model roles. + */ + enum Roles { + StateKeyRole, /**< The state key of the state event. */ + }; + Q_ENUM(Roles) + + explicit StateKeysModel(QObject *parent = nullptr); + + NeoChatRoom *room() const; + void setRoom(NeoChatRoom *room); + + QString eventType() const; + void setEventType(const QString &eventType); + + /** + * @brief Get the given role value at the given index. + * + * @sa QAbstractItemModel::data + */ + QVariant data(const QModelIndex &index, int role) const override; + + /** + * @brief Number of rows in the model. + * + * @sa QAbstractItemModel::rowCount + */ + int rowCount(const QModelIndex &parent) const override; + + /** + * @brief Returns a mapping from Role enum values to role names. + * + * @sa Roles, QAbstractItemModel::roleNames() + */ + QHash roleNames() const override; + + /** + * @brief Get the full JSON for an event. + */ + Q_INVOKABLE QByteArray stateEventJson(const QModelIndex &index); + +Q_SIGNALS: + void roomChanged(); + void eventTypeChanged(); + +private: + NeoChatRoom *m_room = nullptr; + QString m_eventType; + QVector m_stateKeys; + void loadState(); +}; diff --git a/src/models/statemodel.cpp b/src/models/statemodel.cpp index 7555f5f68..cee2ceb79 100644 --- a/src/models/statemodel.cpp +++ b/src/models/statemodel.cpp @@ -10,16 +10,16 @@ StateModel::StateModel(QObject *parent) QHash StateModel::roleNames() const { - return {{TypeRole, "type"}, {StateKeyRole, "stateKey"}}; + return {{TypeRole, "type"}, {EventCountRole, "eventCount"}}; } QVariant StateModel::data(const QModelIndex &index, int role) const { auto row = index.row(); switch (role) { case TypeRole: - return m_stateEvents[row].first; - case StateKeyRole: - return m_stateEvents[row].second; + return m_stateEvents.keys()[row]; + case EventCountRole: + return m_stateEvents.values()[row].count(); } return {}; } @@ -27,7 +27,7 @@ QVariant StateModel::data(const QModelIndex &index, int role) const int StateModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent); - return m_room->currentState().events().size(); + return m_stateEvents.count(); } NeoChatRoom *StateModel::room() const @@ -35,26 +35,39 @@ NeoChatRoom *StateModel::room() const return m_room; } +void StateModel::loadState() +{ + beginResetModel(); + m_stateEvents.clear(); + const auto keys = m_room->currentState().events().keys(); + for (const auto &[type, stateKey] : keys) { + if (!m_stateEvents.contains(type)) { + m_stateEvents[type] = {}; + } + m_stateEvents[type] += stateKey; + } + endResetModel(); +} + void StateModel::setRoom(NeoChatRoom *room) { m_room = room; Q_EMIT roomChanged(); - beginResetModel(); - m_stateEvents.clear(); - m_stateEvents = m_room->currentState().events().keys(); - endResetModel(); + loadState(); + connect(room, &NeoChatRoom::changed, this, [this] { - beginResetModel(); - m_stateEvents.clear(); - m_stateEvents = m_room->currentState().events().keys(); - endResetModel(); + loadState(); }); } QByteArray StateModel::stateEventJson(const QModelIndex &index) { auto row = index.row(); - return QJsonDocument(m_room->currentState().events()[m_stateEvents[row]]->fullJson()).toJson(); + 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(event->fullJson()).toJson(); } #include "moc_statemodel.cpp" diff --git a/src/models/statemodel.h b/src/models/statemodel.h index d3c7ec5c6..effed1e46 100644 --- a/src/models/statemodel.h +++ b/src/models/statemodel.h @@ -29,7 +29,7 @@ public: */ enum Roles { TypeRole = 0, /**< The type of the state event. */ - StateKeyRole, /**< The state key of the state event. */ + EventCountRole, /**< Number of events of this type. */ }; Q_ENUM(Roles) @@ -58,11 +58,9 @@ 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); @@ -73,10 +71,8 @@ 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. + * @brief A map from state event type to number of events of that type */ - QList> m_stateEvents; + QMap> m_stateEvents; + void loadState(); }; diff --git a/src/qml/RoomData.qml b/src/qml/RoomData.qml index d5a637dd3..bb862dd26 100644 --- a/src/qml/RoomData.qml +++ b/src/qml/RoomData.qml @@ -36,38 +36,20 @@ ColumnLayout { FormCard.FormTextDelegate { text: i18n("Room Id: %1", root.room.id) } - FormCard.FormCheckDelegate { - text: i18n("Show m.room.member events") - checked: true - onToggled: { - if (checked) { - stateEventFilterModel.removeStateEventTypeFiltered("m.room.member"); - } else { - stateEventFilterModel.addStateEventTypeFiltered("m.room.member"); - } - } - } - FormCard.FormCheckDelegate { - id: roomAccountDataVisibleCheck - text: i18n("Show room account data") - checked: false - } } FormCard.FormHeader { - visible: roomAccountDataVisibleCheck.checked title: i18n("Room Account Data") } FormCard.FormCard { - visible: roomAccountDataVisibleCheck.checked Repeater { model: root.room.accountDataEventTypes delegate: FormCard.FormButtonDelegate { text: modelData onClicked: applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet.qml'), { - "sourceText": root.room.roomAcountDataJson(text) + sourceText: root.room.roomAcountDataJson(text) }, { - "title": i18n("Event Source"), - "width": Kirigami.Units.gridUnit * 25 + title: i18n("Event Source"), + width: Kirigami.Units.gridUnit * 25 }) } } @@ -78,24 +60,36 @@ ColumnLayout { } FormCard.FormCard { Repeater { - model: StateFilterModel { - id: stateEventFilterModel - sourceModel: StateModel { - id: stateModel - room: root.room - } + model: StateModel { + id: stateModel + room: root.room } delegate: FormCard.FormButtonDelegate { text: model.type - description: model.stateKey - onClicked: applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet.qml'), { - sourceText: stateModel.stateEventJson(stateEventFilterModel.mapToSource(stateEventFilterModel.index(model.index, 0))) - }, { - title: i18n("Event Source"), - width: Kirigami.Units.gridUnit * 25 - }) + 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.qml'), { + sourceText: stateModel.stateEventJson(stateModel.index(model.index, 0)) + }, { + title: i18n("Event Source"), + width: Kirigami.Units.gridUnit * 25 + }) + } else { + pageStack.pushDialogLayer(stateKeysComponent, { + room: root.room, + eventType: model.type + }, { + title: i18nc("'Event' being some JSON data, not something physically happening.", "Event Information") + }); + } + } } } + Component { + id: stateKeysComponent + StateKeys {} + } } } diff --git a/src/qml/StateKeys.qml b/src/qml/StateKeys.qml new file mode 100644 index 000000000..f29093d9b --- /dev/null +++ b/src/qml/StateKeys.qml @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2024 Tobias Fella +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +import QtQuick +import QtQuick.Layouts + +import org.kde.kirigami as Kirigami +import org.kde.kirigamiaddons.formcard as FormCard +import org.kde.kitemmodels + +import org.kde.neochat + +FormCard.FormCardPage { + id: root + + required property NeoChatRoom room + required property string eventType + + title: root.eventType + + FormCard.FormHeader { + title: i18nc("The name of one instance of a state of configuration. Unless you really know what you're doing, best leave this untranslated.", "State Keys") + } + FormCard.FormCard { + Repeater { + model: StateKeysModel { + id: stateKeysModel + room: root.room + eventType: root.eventType + } + + delegate: FormCard.FormButtonDelegate { + text: model.stateKey + onClicked: applicationWindow().pageStack.pushDialogLayer('qrc:/org/kde/neochat/qml/MessageSourceSheet.qml', { + sourceText: stateKeysModel.stateEventJson(stateKeysModel.index(model.index, 0)) + }, { + title: i18nc("@title:window", "Event Source"), + width: Kirigami.Units.gridUnit * 25 + }) + } + } + } +}