summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorNicolas Werner <nicolas.werner@hotmail.de>2023-07-05 00:08:37 +0200
committerNicolas Werner <nicolas.werner@hotmail.de>2023-07-05 00:08:37 +0200
commitad6e4fef6407b9e39ab8ee329b4e8c12376c8494 (patch)
treebfa2634aabea625a1479817700c35d846273371b
parentdcb6c007087e4689b202e4d2fc051a9a9a31c4b0 (diff)
Add experimental event expiration
Currently disabled by default.
-rw-r--r--CMakeLists.txt5
-rw-r--r--im.nheko.Nheko.yaml2
-rw-r--r--resources/qml/dialogs/EventExpirationDialog.qml167
-rw-r--r--resources/qml/dialogs/RoomSettings.qml18
-rw-r--r--src/ChatPage.cpp1
-rw-r--r--src/UserSettingsPage.cpp33
-rw-r--r--src/UserSettingsPage.h6
-rw-r--r--src/Utils.cpp84
-rw-r--r--src/ui/EventExpiry.cpp124
-rw-r--r--src/ui/EventExpiry.h67
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();
+};
+