diff options
-rw-r--r-- | CMakeLists.txt | 5 | ||||
-rw-r--r-- | im.nheko.Nheko.yaml | 2 | ||||
-rw-r--r-- | resources/qml/dialogs/EventExpirationDialog.qml | 167 | ||||
-rw-r--r-- | resources/qml/dialogs/RoomSettings.qml | 18 | ||||
-rw-r--r-- | src/ChatPage.cpp | 1 | ||||
-rw-r--r-- | src/UserSettingsPage.cpp | 33 | ||||
-rw-r--r-- | src/UserSettingsPage.h | 6 | ||||
-rw-r--r-- | src/Utils.cpp | 84 | ||||
-rw-r--r-- | src/ui/EventExpiry.cpp | 124 | ||||
-rw-r--r-- | src/ui/EventExpiry.h | 67 |
10 files changed, 475 insertions, 32 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 50940246..1d43cfe6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -387,6 +387,8 @@ set(SRC_FILES # UI components src/ui/HiddenEvents.cpp src/ui/HiddenEvents.h + src/ui/EventExpiry.cpp + src/ui/EventExpiry.h src/ui/MxcAnimatedImage.cpp src/ui/MxcAnimatedImage.h src/ui/MxcMediaProxy.cpp @@ -599,7 +601,7 @@ if(USE_BUNDLED_MTXCLIENT) FetchContent_Declare( MatrixClient GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git - GIT_TAG f4425af712afc6ad704a39b93c912432bd3c1914 + GIT_TAG 0a4cc9421a97bea81a8921f3f5e040f0a34278fc ) set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "") set(BUILD_LIB_TESTS OFF CACHE INTERNAL "") @@ -763,6 +765,7 @@ set(QML_SOURCES resources/qml/dialogs/CreateDirect.qml resources/qml/dialogs/CreateRoom.qml resources/qml/dialogs/HiddenEventsDialog.qml + resources/qml/dialogs/EventExpirationDialog.qml resources/qml/dialogs/ImageOverlay.qml resources/qml/dialogs/ImagePackEditorDialog.qml resources/qml/dialogs/ImagePackSettingsDialog.qml diff --git a/im.nheko.Nheko.yaml b/im.nheko.Nheko.yaml index 5a744996..4fa8ccfb 100644 --- a/im.nheko.Nheko.yaml +++ b/im.nheko.Nheko.yaml @@ -214,7 +214,7 @@ modules: buildsystem: cmake-ninja name: mtxclient sources: - - commit: f4425af712afc6ad704a39b93c912432bd3c1914 + - commit: 0a4cc9421a97bea81a8921f3f5e040f0a34278fc #tag: v0.9.2 type: git url: https://github.com/Nheko-Reborn/mtxclient.git diff --git a/resources/qml/dialogs/EventExpirationDialog.qml b/resources/qml/dialogs/EventExpirationDialog.qml new file mode 100644 index 00000000..5d12bda8 --- /dev/null +++ b/resources/qml/dialogs/EventExpirationDialog.qml @@ -0,0 +1,167 @@ +// SPDX-FileCopyrightText: Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import ".." +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import im.nheko + +ApplicationWindow { + id: dialog + + property string roomid: "" + property string roomName: "" + property var onAccepted: undefined + + modality: Qt.NonModal + flags: Qt.Dialog | Qt.WindowTitleHint + width: 275 + height: 330 + minimumWidth: 250 + minimumHeight: 220 + + EventExpiry { + id: eventExpiry + + roomid: dialog.roomid + } + + title: { + if (roomid) { + return qsTr("Event expiration for %1").arg(roomName); + } + else { + return qsTr("Event expiration"); + } + } + + Shortcut { + sequence: StandardKey.Cancel + onActivated: dbb.rejected() + } + + ColumnLayout { + spacing: Nheko.paddingMedium + anchors.margins: Nheko.paddingMedium + anchors.fill: parent + + MatrixText { + id: promptLabel + text: { + if (roomid) { + return qsTr("You can configure when your messages will be deleted in %1. This only happens when Nheko is open and has permissions to delete messages until Matrix servers support this feature natively. In general 0 means disable.").arg(roomName); + } + else { + return qsTr("You can configure when your messages will be deleted in all rooms unless configured otherwise. This only happens when Nheko is open and has permissions to delete messages until Matrix servers support this feature natively. In general 0 means disable."); + } + } + font.pixelSize: Math.floor(fontMetrics.font.pixelSize * 1.2) + Layout.fillWidth: true + Layout.fillHeight: false + } + + GridLayout { + columns: 2 + rowSpacing: Nheko.paddingMedium + Layout.fillWidth: true + Layout.fillHeight: true + + MatrixText { + text: qsTr("Expire events after X days") + ToolTip.text: qsTr("Automatically redacts messages after X days, unless otherwise protected. Set to 0 to disable.") + ToolTip.visible: hh1.hovered + Layout.fillWidth: true + + HoverHandler { + id: hh1 + } + } + + SpinBox { + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + from: 0 + to: 1000 + stepSize: 1 + value: eventExpiry.expireEventsAfterDays + onValueChanged: eventExpiry.expireEventsAfterDays = value + editable: true + } + + MatrixText { + text: qsTr("Only keep latest X events") + ToolTip.text: qsTr("Deletes your events in this room if there are more than X newer messages unless otherwise protected. Set to 0 to disable.") + ToolTip.visible: hh2.hovered + Layout.fillWidth: true + + HoverHandler { + id: hh2 + } + } + + + SpinBox { + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + from: 0 + to: 1000 + stepSize: 1 + value: eventExpiry.expireEventsAfterCount + onValueChanged: eventExpiry.expireEventsAfterCount = value + editable: true + } + + MatrixText { + text: qsTr("Always keep latest X events") + ToolTip.text: qsTr("This prevents events to be deleted by the above 2 settings if they are the latest X messages from you in the room.") + ToolTip.visible: hh3.hovered + Layout.fillWidth: true + + HoverHandler { + id: hh3 + } + } + + + SpinBox { + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + from: 0 + to: 1000 + stepSize: 1 + value: eventExpiry.protectLatestEvents + onValueChanged: eventExpiry.protectLatestEvents = value + editable: true + } + + MatrixText { + text: qsTr("Include state events") + ToolTip.text: qsTr("If this is turned on, old state events also get redacted. The latest state event of any type+key combination is excluded from redaction to not remove the room name and similar state by accident.") + ToolTip.visible: hh4.hovered + Layout.fillWidth: true + + HoverHandler { + id: hh4 + } + } + + ToggleButton { + Layout.alignment: Qt.AlignRight + checked: eventExpiry.expireStateEvents + onToggled: eventExpiry.expireStateEvents = checked + } + } + } + + footer: DialogButtonBox { + id: dbb + + standardButtons: DialogButtonBox.Ok | DialogButtonBox.Cancel + onAccepted: { + eventExpiry.save(); + dialog.close(); + } + onRejected: dialog.close(); + } + +} + diff --git a/resources/qml/dialogs/RoomSettings.qml b/resources/qml/dialogs/RoomSettings.qml index a3b3663f..3b8e1903 100644 --- a/resources/qml/dialogs/RoomSettings.qml +++ b/resources/qml/dialogs/RoomSettings.qml @@ -502,6 +502,24 @@ ApplicationWindow { } Label { + text: qsTr("Automatic event deletion") + color: palette.text + } + + EventExpirationDialog { + id: eventExpirationDialog + roomid: roomSettings.roomId + roomName: roomSettings.roomName + } + + Button { + text: qsTr("Configure") + ToolTip.text: qsTr("Select if your events get automatically deleted in this room.") + onClicked: eventExpirationDialog.show() + Layout.alignment: Qt.AlignRight + } + + Label { text: qsTr("GENERAL SETTINGS") font.bold: true color: palette.text diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index c305a54a..4686b0f5 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -87,6 +87,7 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QObject *parent) if (lastSpacesUpdate < QDateTime::currentDateTime().addSecs(-20 * 60)) { lastSpacesUpdate = QDateTime::currentDateTime(); utils::updateSpaceVias(); + utils::removeExpiredEvents(); } if (!isConnected_) diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp index ea7f22c4..7c30f877 100644 --- a/src/UserSettingsPage.cpp +++ b/src/UserSettingsPage.cpp @@ -101,6 +101,8 @@ UserSettings::load(std::optional<QString> profile) exposeDBusApi_ = settings.value(QStringLiteral("user/expose_dbus_api"), false).toBool(); updateSpaceVias_ = settings.value(QStringLiteral("user/space_background_maintenance"), true).toBool(); + expireEvents_ = + settings.value(QStringLiteral("user/expired_events_background_maintenance"), false).toBool(); mobileMode_ = settings.value(QStringLiteral("user/mobile_mode"), false).toBool(); emojiFont_ = settings.value(QStringLiteral("user/emoji_font_family"), "emoji").toString(); @@ -309,6 +311,17 @@ UserSettings::setUpdateSpaceVias(bool state) } void +UserSettings::setExpireEvents(bool state) +{ + if (expireEvents_ == state) + return; + + expireEvents_ = state; + emit expireEventsChanged(state); + save(); +} + +void UserSettings::setMarkdown(bool state) { if (state == markdown_) @@ -924,6 +937,7 @@ UserSettings::save() settings.setValue(QStringLiteral("open_video_external"), openVideoExternal_); settings.setValue(QStringLiteral("expose_dbus_api"), exposeDBusApi_); settings.setValue(QStringLiteral("space_background_maintenance"), updateSpaceVias_); + settings.setValue(QStringLiteral("expired_events_background_maintenance"), expireEvents_); settings.endGroup(); // user @@ -1129,6 +1143,8 @@ UserSettingsModel::data(const QModelIndex &index, int role) const return tr("Expose room information via D-Bus"); case UpdateSpaceVias: return tr("Periodically update community routing information"); + case ExpireEvents: + return tr("Periodically delete expired events"); } } else if (role == Value) { switch (index.row()) { @@ -1266,6 +1282,8 @@ UserSettingsModel::data(const QModelIndex &index, int role) const return i->exposeDBusApi(); case UpdateSpaceVias: return i->updateSpaceVias(); + case ExpireEvents: + return i->expireEvents(); } } else if (role == Description) { switch (index.row()) { @@ -1449,6 +1467,10 @@ UserSettingsModel::data(const QModelIndex &index, int role) const "information about what servers participate in a room to community members. Since " "the room participants can change over time, this needs to be updated from time to " "time. This setting enables a background job to do that automatically."); + case ExpireEvents: + return tr("Regularly redact expired events as specified in the event expiration " + "configuration. Since this is currently not executed server side, you need " + "to have one client running this regularly."); } } else if (role == Type) { switch (index.row()) { @@ -1499,6 +1521,7 @@ UserSettingsModel::data(const QModelIndex &index, int role) const case UseOnlineKeyBackup: case ExposeDBusApi: case UpdateSpaceVias: + case ExpireEvents: case SpaceNotifications: case FancyEffects: case ReducedMotion: @@ -1994,6 +2017,13 @@ UserSettingsModel::setData(const QModelIndex &index, const QVariant &value, int } else return false; } + case ExpireEvents: { + if (value.userType() == QMetaType::Bool) { + i->setExpireEvents(value.toBool()); + return true; + } else + return false; + } } } return false; @@ -2249,4 +2279,7 @@ UserSettingsModel::UserSettingsModel(QObject *p) connect(s.get(), &UserSettings::updateSpaceViasChanged, this, [this] { emit dataChanged(index(UpdateSpaceVias), index(UpdateSpaceVias), {Value}); }); + connect(s.get(), &UserSettings::expireEventsChanged, this, [this] { + emit dataChanged(index(ExpireEvents), index(ExpireEvents), {Value}); + }); } diff --git a/src/UserSettingsPage.h b/src/UserSettingsPage.h index 34dae2ea..4e2691e5 100644 --- a/src/UserSettingsPage.h +++ b/src/UserSettingsPage.h @@ -128,6 +128,7 @@ class UserSettings final : public QObject bool exposeDBusApi READ exposeDBusApi WRITE setExposeDBusApi NOTIFY exposeDBusApiChanged) Q_PROPERTY(bool updateSpaceVias READ updateSpaceVias WRITE setUpdateSpaceVias NOTIFY updateSpaceViasChanged) + Q_PROPERTY(bool expireEvents READ expireEvents WRITE setExpireEvents NOTIFY expireEventsChanged) UserSettings(); @@ -233,6 +234,7 @@ public: void setCollapsedSpaces(QList<QStringList> spaces); void setExposeDBusApi(bool state); void setUpdateSpaceVias(bool state); + void setExpireEvents(bool state); QString theme() const { return !theme_.isEmpty() ? theme_ : defaultTheme_; } bool messageHoverHighlight() const { return messageHoverHighlight_; } @@ -308,6 +310,7 @@ public: QList<QStringList> collapsedSpaces() const { return collapsedSpaces_; } bool exposeDBusApi() const { return exposeDBusApi_; } bool updateSpaceVias() const { return updateSpaceVias_; } + bool expireEvents() const { return expireEvents_; } signals: void groupViewStateChanged(bool state); @@ -372,6 +375,7 @@ signals: void recentReactionsChanged(); void exposeDBusApiChanged(bool state); void updateSpaceViasChanged(bool state); + void expireEventsChanged(bool state); private: // Default to system theme if QT_QPA_PLATFORMTHEME var is set. @@ -446,6 +450,7 @@ private: bool openVideoExternal_; bool exposeDBusApi_; bool updateSpaceVias_; + bool expireEvents_; QSettings settings; @@ -478,6 +483,7 @@ class UserSettingsModel : public QAbstractListModel ExposeDBusApi, #endif UpdateSpaceVias, + ExpireEvents, AccessibilitySection, ReducedMotion, diff --git a/src/Utils.cpp b/src/Utils.cpp index 7a412db0..663609fe 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -1610,8 +1610,7 @@ std::atomic<bool> event_expiration_running = false; void utils::removeExpiredEvents() { - // TODO(Nico): Add its own toggle... - if (!UserSettings::instance()->updateSpaceVias()) + if (!UserSettings::instance()->expireEvents()) return; if (event_expiration_running.exchange(true)) { @@ -1645,18 +1644,20 @@ utils::removeExpiredEvents() std::string currentRoom; std::uint64_t currentRoomCount = 0; std::string currentRoomPrevToken; + std::set<std::pair<std::string, std::string>> currentRoomStateEvents; std::vector<std::string> currentRoomRedactionQueue; mtx::events::account_data::nheko_extensions::EventExpiry currentExpiry; static void next(std::shared_ptr<ApplyEventExpiration> state) { if (!state->currentRoomRedactionQueue.empty()) { + auto evid = state->currentRoomRedactionQueue.back(); + auto room = state->currentRoom; http::client()->redact_event( - state->currentRoom, - state->currentRoomRedactionQueue.back(), - [state = std::move(state)](const mtx::responses::EventId &, - mtx::http::RequestErr e) mutable { - const auto &event_id = state->currentRoomRedactionQueue.back(); + room, + evid, + [state = std::move(state), evid](const mtx::responses::EventId &, + mtx::http::RequestErr e) mutable { if (e) { if (e->status_code == 429 && e->matrix_error.retry_after.count() != 0) { ChatPage::instance()->callFunctionOnGuiThread( @@ -1669,17 +1670,19 @@ utils::removeExpiredEvents() }); }); return; + } else { + nhlog::net()->error("Failed to redact event {} in {}: {}", + evid, + state->currentRoom, + *e); + state->currentRoomRedactionQueue.pop_back(); + next(std::move(state)); } - - nhlog::net()->error("Failed to redact event {} in {}: {}", - event_id, - state->currentRoom, - *e); + } else { + nhlog::net()->info("Redacted event {} in {}", evid, state->currentRoom); + state->currentRoomRedactionQueue.pop_back(); + next(std::move(state)); } - nhlog::net()->info( - "Redacted event {} in {}: {}", event_id, state->currentRoom, *e); - state->currentRoomRedactionQueue.pop_back(); - next(std::move(state)); }); } else if (!state->currentRoom.empty()) { mtx::http::MessagesOpts opts{}; @@ -1687,6 +1690,7 @@ utils::removeExpiredEvents() opts.from = state->currentRoomPrevToken; opts.limit = 1000; opts.filter = state->filter; + opts.room_id = state->currentRoom; http::client()->messages( opts, @@ -1708,6 +1712,19 @@ utils::removeExpiredEvents() mtx::events::RedactionEvent<mtx::events::msg::Redaction>>(e)) continue; + if (std::holds_alternative< + mtx::events::RoomEvent<mtx::events::msg::Redacted>>(e)) + continue; + + if (std::holds_alternative< + mtx::events::StateEvent<mtx::events::msg::Redacted>>(e)) + continue; + + // skip events we don't know to protect us from mistakes. + if (std::holds_alternative< + mtx::events::RoomEvent<mtx::events::Unknown>>(e)) + continue; + if (mtx::accessors::sender(e) != us) continue; @@ -1720,6 +1737,21 @@ utils::removeExpiredEvents() mtx::accessors::is_state_event(e)) continue; + if (mtx::accessors::is_state_event(e)) { + // skip the first state event of a type + if (std::visit( + [&state](const auto &se) { + if constexpr (requires { se.state_key; }) + return state->currentRoomStateEvents + .emplace(to_string(se.type), se.state_key) + .second; + else + return false; + }, + e)) + continue; + } + if (state->currentExpiry.keep_only_latest && state->currentRoomCount > state->currentExpiry.keep_only_latest) { state->currentRoomRedactionQueue.push_back( @@ -1738,6 +1770,7 @@ utils::removeExpiredEvents() state->currentRoom.clear(); state->currentRoomCount = 0; state->currentRoomPrevToken.clear(); + state->currentRoomStateEvents.clear(); } next(std::move(state)); @@ -1764,20 +1797,11 @@ utils::removeExpiredEvents() auto asus = std::make_shared<ApplyEventExpiration>(); - asus->filter = - nlohmann::json{ - "room", - nlohmann::json::object({ - { - "timeline", - nlohmann::json::object({ - {"senders", nlohmann::json::array({us})}, - {"not_types", nlohmann::json::array({"m.room.redaction"})}, - }), - }, - }), - } - .dump(); + nlohmann::json filter; + filter["timeline"]["senders"] = nlohmann::json::array({us}); + filter["timeline"]["not_types"] = nlohmann::json::array({"m.room.redaction"}); + + asus->filter = filter.dump(); asus->globalExpiry = getExpEv(); diff --git a/src/ui/EventExpiry.cpp b/src/ui/EventExpiry.cpp new file mode 100644 index 00000000..ca149dc3 --- /dev/null +++ b/src/ui/EventExpiry.cpp @@ -0,0 +1,124 @@ +// SPDX-FileCopyrightText: Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "EventExpiry.h" + +#include "Cache_p.h" +#include "MainWindow.h" +#include "MatrixClient.h" +#include "timeline/TimelineModel.h" + +void +EventExpiry::load() +{ + using namespace mtx::events; + + this->event = {}; + + if (auto temp = cache::client()->getAccountData(mtx::events::EventType::NhekoEventExpiry, "")) { + auto h = std::get< + mtx::events::AccountDataEvent<mtx::events::account_data::nheko_extensions::EventExpiry>>( + *temp); + this->event = std::move(h.content); + } + + if (!roomid_.isEmpty()) { + if (auto temp = cache::client()->getAccountData(mtx::events::EventType::NhekoEventExpiry, + roomid_.toStdString())) { + auto h = std::get<mtx::events::AccountDataEvent< + mtx::events::account_data::nheko_extensions::EventExpiry>>(*temp); + this->event = std::move(h.content); + } + } + + emit expireEventsAfterDaysChanged(); + emit expireEventsAfterCountChanged(); + emit protectLatestEventsChanged(); + emit expireStateEventsChanged(); +} + +void +EventExpiry::save() +{ + if (roomid_.isEmpty()) + http::client()->put_account_data(event, [](mtx::http::RequestErr e) { + if (e) { + nhlog::net()->error("Failed to set hidden events: {}", *e); + MainWindow::instance()->showNotification( + tr("Failed to set hidden events: %1") + .arg(QString::fromStdString(e->matrix_error.error))); + } + }); + else + http::client()->put_room_account_data( + roomid_.toStdString(), event, [](mtx::http::RequestErr e) { + if (e) { + nhlog::net()->error("Failed to set hidden events: {}", *e); + MainWindow::instance()->showNotification( + tr("Failed to set hidden events: %1") + .arg(QString::fromStdString(e->matrix_error.error))); + } + }); +} + +int +EventExpiry::expireEventsAfterDays() const +{ + return event.expire_after_ms / (1000 * 60 * 60 * 24); +} + +int +EventExpiry::expireEventsAfterCount() const +{ + return event.keep_only_latest; +} + +int +EventExpiry::protectLatestEvents() const +{ + return event.protect_latest; +} + +bool +EventExpiry::expireStateEvents() const +{ + return !event.exclude_state_events; +} + +void +EventExpiry::setExpireEventsAfterDays(int val) +{ + if (val > 0) + this->event.expire_after_ms = val * (1000 * 60 * 60 * 24); + else + this->event.expire_after_ms = 0; + emit expireEventsAfterDaysChanged(); +} + +void +EventExpiry::setProtectLatestEvents(int val) +{ + if (val > 0) + this->event.protect_latest = val; + else + this->event.expire_after_ms = 0; + emit protectLatestEventsChanged(); +} + +void +EventExpiry::setExpireEventsAfterCount(int val) +{ + if (val > 0) + this->event.keep_only_latest = val; + else + this->event.keep_only_latest = 0; + emit expireEventsAfterCountChanged(); +} + +void +EventExpiry::setExpireStateEvents(bool val) +{ + this->event.exclude_state_events = !val; + emit expireEventsAfterCountChanged(); +} diff --git a/src/ui/EventExpiry.h b/src/ui/EventExpiry.h new file mode 100644 index 00000000..aa144dc3 --- /dev/null +++ b/src/ui/EventExpiry.h @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <QObject> +#include <QQmlEngine> +#include <QString> +#include <QVariantList> + +#include <mtx/events/nheko_extensions/event_expiry.hpp> + +class EventExpiry : public QObject +{ + Q_OBJECT + QML_ELEMENT + Q_PROPERTY(QString roomid READ roomid WRITE setRoomid NOTIFY roomidChanged REQUIRED) + Q_PROPERTY(int expireEventsAfterDays READ expireEventsAfterDays WRITE setExpireEventsAfterDays + NOTIFY expireEventsAfterDaysChanged) + Q_PROPERTY(bool expireStateEvents READ expireStateEvents WRITE setExpireStateEvents NOTIFY + expireStateEventsChanged) + Q_PROPERTY(int expireEventsAfterCount READ expireEventsAfterCount WRITE + setExpireEventsAfterCount NOTIFY expireEventsAfterCountChanged) + Q_PROPERTY(int protectLatestEvents READ protectLatestEvents WRITE setProtectLatestEvents NOTIFY + protectLatestEventsChanged) +public: + explicit EventExpiry(QObject *p = nullptr) + : QObject(p) + { + } + + Q_INVOKABLE void save(); + + [[nodiscard]] QString roomid() const { return roomid_; } + void setRoomid(const QString &r) + { + roomid_ = r; + emit roomidChanged(); + + load(); + } + + [[nodiscard]] int expireEventsAfterDays() const; + [[nodiscard]] int expireEventsAfterCount() const; + [[nodiscard]] int protectLatestEvents() const; + [[nodiscard]] bool expireStateEvents() const; + void setExpireEventsAfterDays(int); + void setExpireEventsAfterCount(int); + void setProtectLatestEvents(int); + void setExpireStateEvents(bool); + +signals: + void roomidChanged(); + + void expireEventsAfterDaysChanged(); + void expireEventsAfterCountChanged(); + void protectLatestEventsChanged(); + void expireStateEventsChanged(); + +private: + QString roomid_; + mtx::events::account_data::nheko_extensions::EventExpiry event = {}; + + void load(); +}; + |