Page MenuHomePhorge

D275.1770067692.diff
No OneTemporary

Size
13 KB
Referenced Files
None
Subscribers
None

D275.1770067692.diff

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

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)

Event Timeline