Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F37030239
D275.1770067692.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
13 KB
Referenced Files
None
Subscribers
None
D275.1770067692.diff
View Options
diff --git a/src/contents/ui/EventView.qml b/src/contents/ui/EventView.qml
--- a/src/contents/ui/EventView.qml
+++ b/src/contents/ui/EventView.qml
@@ -257,6 +257,10 @@
paginateBackRequested(event.eventId);
}
+ function backfill() {
+ backfillRequested(event.eventId);
+ }
+
function reactWith(reactionText) {
room.sendReaction(reactionText, event.eventId);
}
@@ -275,6 +279,8 @@
function maybePaginateBack() {
if (isGapped) {
paginateBack();
+ } else if (ListView.view && index === ListView.view.timeline.count - 1) {
+ backfill();
}
}
}
diff --git a/src/contents/ui/Main.qml b/src/contents/ui/Main.qml
--- a/src/contents/ui/Main.qml
+++ b/src/contents/ui/Main.qml
@@ -28,6 +28,7 @@
onLoginSuccessful: {
switchToMainPage();
recordLastSession();
+ root.purgeTimer.start();
}
onLoginFailed: {
console.log("Login Failed");
@@ -40,6 +41,7 @@
onSessionChanged: {
console.log('session changed');
reloadSdkVariables();
+ root.purgeTimer.start();
}
onLogoutSuccessful: {
root.loggedIn = false;
@@ -73,6 +75,16 @@
property string currentRoomId: ''
}
+ property var purgeTimer: Timer {
+ interval: 1000 * 60 * 5
+ repeat: true
+ triggeredOnStart: true
+ onTriggered: () => {
+ matrixSdk.purgeEventsExceptRooms(
+ sdkVars.currentRoomId ? [sdkVars.currentRoomId] : []);
+ }
+ }
+
property var l10nProvider: MK.L10nProvider {
}
diff --git a/src/contents/ui/RoomPage.qml b/src/contents/ui/RoomPage.qml
--- a/src/contents/ui/RoomPage.qml
+++ b/src/contents/ui/RoomPage.qml
@@ -31,6 +31,7 @@
signal mentionUserRequested(string userId)
signal replaceDraftRequested(string newDraft)
signal paginateBackRequested(string eventId)
+ signal backfillRequested(string eventId)
signal viewEventHistoryRequested(string eventId)
title: roomNameProvider.name
@@ -262,6 +263,18 @@
}
}
+ onBackfillRequested: (eventId) => {
+ if (!paginationRequests[eventId]) {
+ paginationRequests[eventId] = matrixSdk.backfillRoomFromEvent(roomId, eventId);
+ paginationRequests[eventId].resolved.connect((isSuccess, data) => {
+ console.debug(
+ 'finished backfill from', eventId,
+ 'success=', isSuccess, JSON.stringify(data));
+ delete paginationRequests[eventId];
+ });
+ }
+ }
+
onViewEventHistoryRequested: (eventId) => {
eventHistoryPopupComp.createObject(applicationWindow().overlay, { eventId }).open();
}
diff --git a/src/db-store.hpp b/src/db-store.hpp
--- a/src/db-store.hpp
+++ b/src/db-store.hpp
@@ -66,7 +66,24 @@
* @param eventId The id of the event.
* @return A pair of (Event, isInTimeline) associated with the ids or std::nullopt if it is not found.
*/
- QCoro::Task<std::optional<std::pair<Kazv::Event, bool>>> getEventById(const QString &roomId, const QString &eventId);
+ QCoro::Task<std::optional<std::pair<Kazv::Event, bool>>> getEventById(QString roomId, QString eventId);
+
+ struct GetEventsBeforeResult
+ {
+ immer::map<std::string, Kazv::EventList> timelineEvents;
+ immer::map<std::string, Kazv::EventList> relatedEvents;
+ };
+ /**
+ * Get events before a certain event id.
+ *
+ * Up to `limit` events sorted before the reference event will be returned.
+ * @param roomId The room id for which to get events.
+ * @param eventId The event id of the reference event.
+ * @param limit The maximum number of timeline events to return.
+ * @return A tuple-ish, the content of which is to be passed to
+ * Kazv::Client::loadEventsFromStorage() .
+ */
+ QCoro::Task<GetEventsBeforeResult> getEventsBefore(QString roomId, QString eventId, std::size_t limit = 10);
private:
void cleanup();
diff --git a/src/db-store.cpp b/src/db-store.cpp
--- a/src/db-store.cpp
+++ b/src/db-store.cpp
@@ -141,7 +141,7 @@
FROM events WHERE
room_id = :room_id AND event_id = :event_id
;)xxx"_s;
-QCoro::Task<std::optional<std::pair<Kazv::Event, bool>>> DbStore::getEventById(const QString &roomId, const QString &eventId)
+QCoro::Task<std::optional<std::pair<Kazv::Event, bool>>> DbStore::getEventById(QString roomId, QString eventId)
{
QPromise<std::optional<std::pair<Kazv::Event, bool>>> p;
auto fut = p.future();
@@ -177,6 +177,75 @@
co_return co_await fut;
}
+inline const auto getEventsBeforeQuery = uR"xxx(SELECT
+ encrypted, decrypted,
+ original_json, decrypted_json, is_in_timeline
+FROM events WHERE
+ room_id = :room_id AND (
+ origin_server_ts < :ref_ts OR (
+ origin_server_ts = :ref_ts AND event_id < :ref_event_id))
+ORDER BY origin_server_ts DESC, event_id DESC
+LIMIT :limit
+;)xxx"_s;
+auto DbStore::getEventsBefore(QString roomId, QString eventId, std::size_t limit) -> QCoro::Task<GetEventsBeforeResult>
+{
+ auto getByIdRes = co_await getEventById(roomId, eventId);
+ if (!getByIdRes.has_value()) {
+ // reference event not in database
+ co_return {};
+ }
+ auto refEventTs = std::move(getByIdRes).value().first.originServerTs();
+ QPromise<GetEventsBeforeResult> p;
+ auto fut = p.future();
+ QMetaObject::invokeMethod(m_obj, [this, p=std::move(p), roomId, refEventTs, eventId, limit]() mutable {
+ QSqlQuery q(m_d.value());
+ auto res = q.prepare(getEventsBeforeQuery);
+ if (!res) {
+ p.addResult(GetEventsBeforeResult{});
+ p.finish();
+ return;
+ }
+ q.bindValue(u":room_id"_s, roomId);
+ q.bindValue(u":ref_ts"_s, static_cast<qint64>(refEventTs));
+ q.bindValue(u":ref_event_id"_s, eventId);
+ q.bindValue(u":limit"_s, static_cast<qint64>(limit));
+ res = q.exec();
+ if (!res) {
+ p.addResult(GetEventsBeforeResult{});
+ p.finish();
+ return;
+ }
+ EventList::transient_type tlEvents;
+ EventList::transient_type nonTlEvents;
+ while (q.next()) {
+ auto encrypted = q.value(0).toBool();
+ auto decrypted = q.value(1).toBool();
+ auto originalJson = q.value(2).toString();
+ auto decryptedJson = q.value(3).toString();
+ auto isInTimeline = q.value(4).toBool();
+ auto eventOpt = resultToEvent(encrypted, decrypted, originalJson, std::move(decryptedJson));
+ if (!eventOpt) {
+ qCWarning(kazvLog) << "event in database is malformed:" << originalJson;
+ continue;
+ }
+ if (isInTimeline) {
+ tlEvents.push_back(eventOpt.value());
+ } else {
+ nonTlEvents.push_back(eventOpt.value());
+ }
+ }
+ auto roomIdStd = roomId.toStdString();
+ // sqlite returns in DESC order, but we want ASC in timeline order
+ auto ret = GetEventsBeforeResult{
+ {{roomIdStd, intoImmer(EventList{}, zug::reversed, std::move(tlEvents))}},
+ {{roomIdStd, intoImmer(EventList{}, zug::reversed, std::move(nonTlEvents))}},
+ };
+ p.addResult(ret);
+ p.finish();
+ });
+ co_return co_await fut;
+}
+
bool DbStore::valid() const
{
return m_d.has_value();
diff --git a/src/matrix-sdk.hpp b/src/matrix-sdk.hpp
--- a/src/matrix-sdk.hpp
+++ b/src/matrix-sdk.hpp
@@ -311,6 +311,26 @@
MatrixPromise *getRoomIdByAlias(const QString &roomAlias);
+ /**
+ * Purge events in all rooms except those specified in roomIds
+ *
+ * @param roomIds The ids for rooms to NOT be purged. This allows
+ * keeping the rooms that are already open intact.
+ * @return A MatrixPromise that resolves when the events are purged.
+ */
+ MatrixPromise *purgeEventsExceptRooms(const QStringList &roomIds);
+
+ /**
+ * Backfill from the event storage from an existing event in a room.
+ *
+ * @param roomId The id of the room to operate on.
+ * @param eventId The id of the event to backfill from.
+ * @return A MatrixPromise that resolves when the events are backfilled.
+ * The Promise is considered successful if and only if at least one event
+ * is loaded.
+ */
+ MatrixPromise *backfillRoomFromEvent(const QString &roomId, const QString &eventId);
+
private:
MatrixPromise *sendAccountDataImpl(Kazv::Event event);
diff --git a/src/matrix-sdk.cpp b/src/matrix-sdk.cpp
--- a/src/matrix-sdk.cpp
+++ b/src/matrix-sdk.cpp
@@ -1051,6 +1051,54 @@
}));
}
+inline constexpr std::size_t purgeEventsKeepNumber = 5;
+MatrixPromise *MatrixSdk::purgeEventsExceptRooms(const QStringList &roomIds)
+{
+ return new MatrixPromise(
+ m_d->sdk.context().createResolvedPromise({})
+ .then([client=m_d->sdk.client(), roomIds=qStringListToStdF(roomIds)
+ ]([[maybe_unused]] auto &&stat) {
+ auto map = intoImmer(immer::map<std::string, std::size_t>{},
+ zug::filter([&roomIds](const std::string &roomId) {
+ return std::find(roomIds.begin(), roomIds.end(), roomId) == roomIds.end();
+ })
+ | zug::map([](const std::string &roomId) {
+ return std::make_pair(roomId, purgeEventsKeepNumber);
+ }),
+ client.roomIds().make().get()
+ );
+ return client.purgeRoomEvents(map);
+ }).then([](const auto &stat) {
+ qCDebug(kazvLog) << "Purge room events stat:" << !!stat;
+ return stat;
+ })
+ );
+}
+
+MatrixPromise *MatrixSdk::backfillRoomFromEvent(const QString &roomId, const QString &eventId)
+{
+ return new MatrixPromise(
+ m_d->sdk.context().createPromise([
+ dbStore=m_d->dbStore.get(),
+ client=m_d->sdk.client(),
+ roomId,
+ eventId
+ ](auto resolve) {
+ dbStore->getEventsBefore(roomId, eventId).then([client, resolve, roomId](auto &&res) {
+ auto [timelineEvents, relatedEvents] = std::move(res);
+ auto loadedCount = timelineEvents[roomId.toStdString()].size();
+ client.loadEventsFromStorage(timelineEvents, relatedEvents)
+ .then([resolve, loadedCount](auto &&) {
+ resolve(EffectStatus(
+ !!loadedCount,
+ json{{"loadedCount", loadedCount}}
+ ));
+ });
+ });
+ })
+ );
+}
+
void MatrixSdk::setUserDataDir(const std::string &userDataDir)
{
m_d->userDataDir = userDataDir;
diff --git a/src/tests/db-store-test.cpp b/src/tests/db-store-test.cpp
--- a/src/tests/db-store-test.cpp
+++ b/src/tests/db-store-test.cpp
@@ -25,6 +25,7 @@
void testImportAllFrom();
void testResultToEvent();
void testSaveEvents();
+ void testGetEventsBefore();
void cleanup();
private:
@@ -114,5 +115,37 @@
QVERIFY(res.has_value() && res.value().first == e3 && !res.value().second);
}
+void DbStoreTest::testGetEventsBefore()
+{
+ DbStore dbStore;
+ {
+ auto stat = QCoro::waitFor(dbStore.setup(m_tempDir.path().toStdString(), "@userid:example.com", "device1"));
+ QVERIFY(stat.first);
+ }
+ auto es = EventList{
+ makeEvent(),
+ makeEvent(),
+ makeEvent(),
+ makeEvent(),
+ makeEvent(),
+ makeEvent(),
+ makeEvent(),
+ };
+ SaveEventsRequested s{
+ {{"!room1:example.com", es}},
+ {}
+ };
+
+ {
+ auto stat = QCoro::waitFor(dbStore.saveEvents(s));
+ QVERIFY(stat.first);
+ }
+ auto res = QCoro::waitFor(dbStore.getEventsBefore(
+ u"!room1:example.com"_s, QString::fromStdString(es[5].id()), 3));
+
+ auto returnedEvents = res.timelineEvents["!room1:example.com"];
+ QVERIFY(returnedEvents == (EventList{es[2], es[3], es[4]}));
+}
+
QTEST_MAIN(DbStoreTest)
#include "db-store-test.moc"
diff --git a/src/tests/quick-tests/tst_RoomTimelineView.qml b/src/tests/quick-tests/tst_RoomTimelineView.qml
--- a/src/tests/quick-tests/tst_RoomTimelineView.qml
+++ b/src/tests/quick-tests/tst_RoomTimelineView.qml
@@ -68,6 +68,8 @@
timeline: item.timeline
}
+ property var backfillRequested: mockHelper.noop()
+
property var timeline: ListModel {
ListElement {}
ListElement {}
diff --git a/src/tests/quick-tests/tst_RoomTimelineViewBackfill.qml b/src/tests/quick-tests/tst_RoomTimelineViewBackfill.qml
new file mode 100644
--- /dev/null
+++ b/src/tests/quick-tests/tst_RoomTimelineViewBackfill.qml
@@ -0,0 +1,68 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2026 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import QtQuick
+import QtQuick.Layouts
+import QtQuick.Window
+import QtQuick.Controls
+import QtTest 1.0
+import moe.kazv.mxc.kazv 0.0 as MK
+import org.kde.kirigami 2.13 as Kirigami
+import '../../contents/ui' as Kazv
+import 'test-helpers.js' as JsHelpers
+import 'test-helpers' as QmlHelpers
+
+QmlHelpers.TestItem {
+ id: item
+
+ property var makeTextEvent: (i) => ({
+ eventId: '$' + i,
+ sender: '@foo:tusooa.xyz',
+ type: 'm.room.message',
+ stateKey: '',
+ content: {
+ msgtype: 'm.text',
+ body: 'some body',
+ },
+ formattedTime: '4:06 P.M.',
+ })
+
+ property var viewComp: Component {
+ Kazv.RoomTimelineView {
+ anchors.fill: parent
+ timeline: item.timeline
+ }
+ }
+
+ property var backfillRequested: mockHelper.noop()
+
+ property var timeline: ListModel {
+ ListElement {}
+ ListElement {}
+ ListElement {}
+ ListElement {}
+
+ function at(index) {
+ return makeTextEvent(index);
+ }
+ }
+
+ TestCase {
+ id: roomTimelineViewBackfillTest
+ name: 'RoomTimelineViewBackfillTest'
+ when: windowShown
+
+ function init() {
+ mockHelper.clearAll();
+ }
+
+ function test_backfill() {
+ const view = viewComp.createObject(item);
+ tryVerify(() => item.backfillRequested.calledTimes() >= 1);
+ verify(JsHelpers.deepEqual(item.backfillRequested.lastArgs(), ['$3']));
+ }
+ }
+}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Mon, Feb 2, 1:28 PM (5 h, 26 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1048368
Default Alt Text
D275.1770067692.diff (13 KB)
Attached To
Mode
D275: Purge and backfill events
Attached
Detach File
Event Timeline
Log In to Comment