Page MenuHomePhorge

D366.1791415718.diff
No OneTemporary

Size
29 KB
Referenced Files
None
Subscribers
None

D366.1791415718.diff

diff --git a/src/contents/ui/RoomTimelineView.qml b/src/contents/ui/RoomTimelineView.qml
--- a/src/contents/ui/RoomTimelineView.qml
+++ b/src/contents/ui/RoomTimelineView.qml
@@ -187,11 +187,14 @@
delegate: EventViewWrapper {
property var eventListView: ListView.view
- objectName: `event-${event.eventId || event.txnId}`
+ required property int index
+ required property string eventOrTxnId
+ required property bool isLocalEcho
+ objectName: `event-${eventOrTxnId}`
prevEvent: index < timeline.count - 1 ? timeline.at(index + 1) : undefined
- event: timeline.at(index)
+ event: timeline.byId(eventOrTxnId, isLocalEcho)
width: ListView.view.width
- isSelected: roomTimelineView.selectedEventId && event.eventId === roomTimelineView.selectedEventId
+ isSelected: roomTimelineView.selectedEventId && eventOrTxnId === roomTimelineView.selectedEventId
Accessible.role: Accessible.ListItem
}
diff --git a/src/list-diff.hpp b/src/list-diff.hpp
new file mode 100644
--- /dev/null
+++ b/src/list-diff.hpp
@@ -0,0 +1,96 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2026 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#pragma once
+#include <kazv-defs.hpp>
+#include <immer/flex_vector.hpp>
+#include <vector>
+#include <unordered_map>
+#include <type_traits>
+#include <variant>
+#include <ranges>
+
+struct ListMove
+{
+ std::size_t from;
+ std::size_t to;
+};
+
+template<class IdT>
+struct ListAdd
+{
+ std::size_t index;
+ IdT id;
+};
+
+struct ListRemove
+{
+ std::size_t index;
+};
+
+template<class IdT>
+using ListChange = std::variant<ListAdd<IdT>, ListRemove, ListMove>;
+
+template<std::ranges::range Range, std::ranges::range Range2, class Func, class IdT = std::invoke_result_t<Func, std::ranges::range_value_t<Range>>>
+std::vector<ListChange<IdT>> listDiff(Range &&last, Range2 &&next, Func &&idFunc)
+{
+ auto makeMap = [idFunc](auto &&r) {
+ std::unordered_map<IdT, std::size_t> res;
+ auto end = std::ranges::end(r);
+ for (auto i = std::ranges::begin(r); i != end; ++i) {
+ res[idFunc(*i)] = i.index();
+ }
+ return res;
+ };
+ std::unordered_map<IdT, std::size_t> lastMap = makeMap(last);
+ std::unordered_map<IdT, std::size_t> nextMap = makeMap(next);
+ std::vector<ListChange<IdT>> res;
+
+ std::size_t oldIndex = 0;
+ std::size_t workIndex = 0;
+ std::size_t removed = 0;
+ while (oldIndex < last.size()) {
+ auto lastId = idFunc(last[oldIndex]);
+ lastMap[lastId] -= removed;
+ if (nextMap.count(lastId) == 0) { // removed
+ res.push_back(ListRemove{workIndex});
+ lastMap.erase(lastId);
+ ++removed;
+ ++oldIndex;
+ } else {
+ ++oldIndex;
+ ++workIndex;
+ }
+ }
+
+ std::size_t changes = 0;
+ workIndex = 0;
+
+ // Change OLD into NEW. Let the process be WORK.
+ // After iteration i, WORK[0...i] is in place.
+ // Only move things to the front
+ while (workIndex < next.size()) {
+ // N = NEW[i]
+ auto nextId = idFunc(next[workIndex]);
+ if (lastMap.count(nextId) == 0) { // added
+ res.push_back(ListAdd<IdT>{workIndex, nextId});
+ ++changes;
+ } else {
+ // Calculate where N is in WORK
+ // if there are A inserts and B moves before N
+ // each insert or move will increase index of N in work by 1
+ auto oldIndex = lastMap[nextId] + changes;
+ if (oldIndex == workIndex) { // not changed
+ } else { // moved from a later entry to here
+ res.push_back(ListMove{oldIndex, workIndex});
+ ++changes;
+ }
+ }
+ ++workIndex;
+ }
+
+ return res;
+}
diff --git a/src/matrix-room-timeline-p.hpp b/src/matrix-room-timeline-p.hpp
new file mode 100644
--- /dev/null
+++ b/src/matrix-room-timeline-p.hpp
@@ -0,0 +1,71 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2026 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <kazv-defs.hpp>
+#include "matrix-room-timeline.hpp"
+#include <functional>
+#include <ranges>
+
+struct TimelineItemDesc
+{
+ std::string id;
+ bool isLocalEcho;
+ friend bool operator==(const TimelineItemDesc &a, const TimelineItemDesc &b) = default;
+};
+
+struct EnhancedTotalItems : public TotalItems
+{
+ TimelineItemDesc at(std::size_t index) const;
+ TimelineItemDesc operator[](std::size_t index) const { return at(index); }
+ std::size_t size() const;
+ void eraseInPlace(std::size_t index);
+ void insertInPlace(std::size_t index, const TimelineItemDesc &item);
+
+ struct Iterator
+ {
+ using difference_type = std::ptrdiff_t;
+ using value_type = TimelineItemDesc;
+ using reference = TimelineItemDesc;
+ using iterator_category = std::random_access_iterator_tag;
+ const EnhancedTotalItems *m_range;
+ std::size_t m_index;
+
+ std::size_t index() const { return m_index; }
+ TimelineItemDesc operator*() const
+ {
+ return m_range->at(m_index);
+ }
+
+ Iterator &operator++()
+ {
+ ++m_index;
+ return *this;
+ }
+ void operator++(int) { ++*this; }
+ friend bool operator==(const Iterator &a, const Iterator &b) = default;
+ friend bool operator!=(const Iterator &a, const Iterator &b) = default;
+ };
+
+ Iterator begin() const
+ {
+ return Iterator{this, 0};
+ }
+
+ Iterator end() const
+ {
+ return Iterator{this, size()};
+ }
+};
+
+template<>
+struct std::hash<TimelineItemDesc>
+{
+ std::size_t operator()(const TimelineItemDesc &i) const noexcept
+ {
+ std::size_t res = std::hash<std::string>()(i.id) + std::hash<bool>()(i.isLocalEcho);
+ return res;
+ }
+};
diff --git a/src/matrix-room-timeline.hpp b/src/matrix-room-timeline.hpp
--- a/src/matrix-room-timeline.hpp
+++ b/src/matrix-room-timeline.hpp
@@ -10,18 +10,24 @@
#include <QObject>
#include <QQmlEngine>
#include <QJsonObject>
+#include <QAbstractListModel>
#include <lager/extra/qt.hpp>
#include <client/room/room.hpp>
-#include "kazv-abstract-list-model.hpp"
class MatrixEvent;
+struct TotalItems
+{
+ immer::flex_vector<std::string> timeline;
+ immer::flex_vector<std::string> localEchoes;
+};
-class MatrixRoomTimeline : public KazvAbstractListModel
+class MatrixRoomTimeline : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
+ Q_PROPERTY(int count READ count NOTIFY countChanged)
Kazv::Room m_room;
@@ -29,6 +35,8 @@
lager::reader<immer::map<std::string, Kazv::Event>> m_messagesMap;
lager::reader<immer::flex_vector<Kazv::LocalEchoDesc>> m_localEchoes;
lager::reader<immer::map<std::string, std::string>> m_timelineGaps;
+ lager::reader<TotalItems> m_totalItems;
+ TotalItems m_innerTotalItems;
public:
explicit MatrixRoomTimeline(Kazv::Room room, QObject *parent = 0);
@@ -39,5 +47,20 @@
Q_INVOKABLE MatrixEvent *at(int index) const;
+ Q_INVOKABLE MatrixEvent *byId(const QString &id, bool isLocalEcho) const;
+
Q_INVOKABLE int indexOfEvent(const QString &eventId) const;
+
+ QVariant data(const QModelIndex &index, int role) const override;
+
+ int rowCount(const QModelIndex &parent) const override;
+
+ int count() const;
+
+ QHash<int, QByteArray> roleNames() const override;
+
+ Q_SIGNAL void countChanged();
+
+private:
+ void handleChange(const TotalItems &next);
};
diff --git a/src/matrix-room-timeline.cpp b/src/matrix-room-timeline.cpp
--- a/src/matrix-room-timeline.cpp
+++ b/src/matrix-room-timeline.cpp
@@ -14,22 +14,85 @@
#include <QJsonValue>
#include <cursorutil.hpp>
-#include "matrix-room-timeline.hpp"
+#include "matrix-room-timeline-p.hpp"
#include "matrix-event.hpp"
#include "helper.hpp"
#include "kazv-log.hpp"
+#include "list-diff.hpp"
using namespace Kazv;
+std::size_t EnhancedTotalItems::size() const
+{
+ return timeline.size() + localEchoes.size();
+}
+
+TimelineItemDesc EnhancedTotalItems::at(std::size_t index) const
+{
+ auto rIndex = size() - index - 1;
+ if (rIndex < timeline.size()) {
+ return TimelineItemDesc{timeline[rIndex], /* isLocalEcho = */ false};
+ } else {
+ auto remainingIndex = rIndex - timeline.size();
+ if (remainingIndex < localEchoes.size()) {
+ return TimelineItemDesc{localEchoes[remainingIndex], /* isLocalEcho = */ true};
+ } else {
+ return TimelineItemDesc{"", /* isLocalEcho */ false};
+ }
+ }
+}
+
+void EnhancedTotalItems::eraseInPlace(std::size_t index)
+{
+ auto rIndex = size() - index - 1;
+ if (rIndex < timeline.size()) {
+ timeline = std::move(timeline).erase(rIndex);
+ } else {
+ auto remainingIndex = rIndex - timeline.size();
+ Q_ASSERT(remainingIndex < localEchoes.size());
+ localEchoes = std::move(localEchoes).erase(remainingIndex);
+ }
+}
+
+void EnhancedTotalItems::insertInPlace(std::size_t index, const TimelineItemDesc &item)
+{
+ auto rIndex = size() - index;
+ if (rIndex < timeline.size() || (rIndex == timeline.size() && !item.isLocalEcho)) {
+ Q_ASSERT(!item.isLocalEcho);
+ timeline = std::move(timeline).insert(rIndex, item.id);
+ } else {
+ auto remainingIndex = rIndex - timeline.size();
+ Q_ASSERT(remainingIndex <= localEchoes.size());
+ Q_ASSERT(item.isLocalEcho);
+ localEchoes = std::move(localEchoes).insert(remainingIndex, item.id);
+ }
+}
+
+TotalItems makeTotalItems(immer::flex_vector<std::string> tl, immer::flex_vector<LocalEchoDesc> local)
+{
+ return {
+ std::move(tl),
+ intoImmer(
+ immer::flex_vector<std::string>{},
+ zug::map([](const LocalEchoDesc &d) { return d.txnId; }),
+ local
+ ),
+ };
+}
+
MatrixRoomTimeline::MatrixRoomTimeline(lager::reader<immer::flex_vector<std::string>> eventIds, lager::reader<immer::map<std::string, Kazv::Event>> messagesMap, lager::reader<immer::flex_vector<LocalEchoDesc>> localEchoes, lager::reader<immer::map<std::string, std::string>> timelineGaps, Kazv::Room room, QObject *parent)
- : KazvAbstractListModel(parent)
+ : QAbstractListModel(parent)
, m_room(room)
, m_timelineEventIds(eventIds)
, m_messagesMap(messagesMap)
, m_localEchoes(localEchoes)
, m_timelineGaps(timelineGaps)
+ , m_totalItems(lager::with(
+ m_timelineEventIds,
+ m_localEchoes
+ ).map(makeTotalItems))
, LAGER_QT(gaps)(m_timelineGaps.map([](auto g) {
QJsonObject res;
for (const auto &[k, _v] : g) {
@@ -38,10 +101,14 @@
return res;
}))
{
- initCountCursor(lager::with(
- m_timelineEventIds.xform(containerSize),
- m_localEchoes.xform(containerSize)
- ).map([](const auto &tlSize, const auto &localSize) { return tlSize + localSize; }), InitLater);
+ lager::watch(m_totalItems, [this](const TotalItems &next) {
+ handleChange(next);
+ });
+ QTimer::singleShot(0, [this, total=m_totalItems.get()] {
+ Q_EMIT beginInsertRows(QModelIndex(), 0, static_cast<const EnhancedTotalItems &>(total).size() - 1);
+ m_innerTotalItems = total;
+ Q_EMIT endInsertRows();
+ });
}
MatrixRoomTimeline::MatrixRoomTimeline(Kazv::Room room, QObject *parent)
@@ -60,8 +127,9 @@
MatrixEvent *MatrixRoomTimeline::at(int index) const
{
- return new MatrixEvent(lager::with(m_timelineEventIds, m_messagesMap, m_localEchoes, LAGER_QT(count))
- .map([index](const auto &tlEventIds, const auto &messagesMap, const auto &localEchoes, const auto &count) -> std::variant<Kazv::Event, Kazv::LocalEchoDesc, std::string> {
+ return new MatrixEvent(lager::with(m_timelineEventIds, m_messagesMap, m_localEchoes)
+ .map([index](const auto &tlEventIds, const auto &messagesMap, const auto &localEchoes) -> std::variant<Kazv::Event, Kazv::LocalEchoDesc, std::string> {
+ auto count = tlEventIds.size() + localEchoes.size();
auto rIndex = count - index - 1;
if (static_cast<unsigned int>(rIndex) < tlEventIds.size()) {
auto eventId = tlEventIds[rIndex];
@@ -82,6 +150,35 @@
}), m_room);
}
+QVariant MatrixRoomTimeline::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid() || std::size_t(index.row()) >= static_cast<const EnhancedTotalItems &>(m_innerTotalItems).size()) {
+ return QVariant();
+ } else if (role == Qt::UserRole) {
+ return QString::fromStdString(static_cast<const EnhancedTotalItems &>(m_innerTotalItems).at(index.row()).id);
+ } else if (role == Qt::UserRole + 1) {
+ return static_cast<const EnhancedTotalItems &>(m_innerTotalItems).at(index.row()).isLocalEcho;
+ }
+ return QVariant();
+}
+
+QHash<int, QByteArray> MatrixRoomTimeline::roleNames() const
+{
+ return {
+ {Qt::UserRole, "eventOrTxnId"},
+ {Qt::UserRole + 1, "isLocalEcho"},
+ };
+}
+
+int MatrixRoomTimeline::rowCount(const QModelIndex &parent) const
+{
+ if (parent.isValid()) {
+ return 0;
+ } else {
+ return count();
+ }
+}
+
int MatrixRoomTimeline::indexOfEvent(const QString &eventId) const
{
auto eventMap = m_messagesMap.get();
@@ -110,3 +207,65 @@
return count() - it.index() - 1;
}
}
+
+MatrixEvent *MatrixRoomTimeline::byId(const QString &id, bool isLocalEcho) const
+{
+ return new MatrixEvent(lager::with(m_messagesMap, m_localEchoes)
+ .map([id=id.toStdString(), isLocalEcho](const auto &messagesMap, const auto &localEchoes) -> std::variant<Kazv::Event, Kazv::LocalEchoDesc, std::string> {
+ if (isLocalEcho) {
+ auto it = std::find_if(localEchoes.begin(), localEchoes.end(), [id](const auto &e) {
+ return e.txnId == id;
+ });
+ if (it != localEchoes.end()) {
+ return *it;
+ }
+ return Event();
+ }
+ if (messagesMap.count(id)) {
+ return messagesMap[id];
+ } else {
+ return id;
+ }
+ }), m_room);
+}
+
+int MatrixRoomTimeline::count() const
+{
+ return static_cast<const EnhancedTotalItems &>(m_innerTotalItems).size();
+}
+
+void MatrixRoomTimeline::handleChange(const TotalItems &next)
+{
+ using IdType = TimelineItemDesc;
+ const auto &nextCast = static_cast<const EnhancedTotalItems &>(next);
+ auto last = static_cast<EnhancedTotalItems>(m_innerTotalItems);
+
+ auto diff = listDiff(last, nextCast, [](auto x) { return x; });
+ auto &cur = static_cast<EnhancedTotalItems &>(m_innerTotalItems);
+
+ for (auto change : diff) {
+ lager::match(change)(
+ [&cur, this](ListMove m) {
+ auto item = cur.at(m.from);
+ Q_EMIT beginMoveRows(QModelIndex(), m.from, m.from, QModelIndex(), m.to);
+ cur.eraseInPlace(m.from);
+ cur.insertInPlace(m.to, std::move(item));
+ Q_EMIT endMoveRows();
+ },
+ [&cur, this](ListAdd<IdType> a) {
+ Q_EMIT beginInsertRows(QModelIndex(), a.index, a.index);
+ cur.insertInPlace(a.index, a.id);
+ Q_EMIT endInsertRows();
+ },
+ [&cur, this](ListRemove r) {
+ Q_EMIT beginRemoveRows(QModelIndex(), r.index, r.index);
+ cur.eraseInPlace(r.index);
+ Q_EMIT endRemoveRows();
+ }
+ );
+ }
+
+ if (nextCast.size() != last.size()) {
+ Q_EMIT countChanged();
+ }
+}
diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt
--- a/src/tests/CMakeLists.txt
+++ b/src/tests/CMakeLists.txt
@@ -38,6 +38,7 @@
db-store-test.cpp
sso-login-process-test.cpp
kazv-config-test.cpp
+ list-diff-test.cpp
LINK_LIBRARIES Qt${QT_MAJOR_VERSION}::Test kazvtestlib
)
diff --git a/src/tests/appium-tests/TestRoomTimelineView.qml b/src/tests/appium-tests/TestRoomTimelineView.qml
--- a/src/tests/appium-tests/TestRoomTimelineView.qml
+++ b/src/tests/appium-tests/TestRoomTimelineView.qml
@@ -11,42 +11,12 @@
import '../../contents/ui' as Kazv
import 'qml-helpers' as AppiumQmlHelpers
+import '../quick-tests/test-helpers' as QmlHelpers
AppiumQmlHelpers.TestItem {
id: upper
signal loadEventRequested(string eventId)
- property var makeLocalEcho: (i) => ({
- eventId: '',
- sender: '',
- type: 'm.room.message',
- stateKey: '',
- content: {
- msgtype: 'm.text',
- body: 'some body' + i,
- },
- encrypted: false,
- isState: false,
- unsignedData: {},
- isLocalEcho: true,
- isSending: true,
- isFailed: false,
- isAvailable: true,
- txnId: 'some-txn-id' + i,
- })
-
- property var makeTextEvent: (i) => ({
- eventId: '$' + i,
- sender: '@foo:tusooa.xyz',
- type: 'm.room.message',
- stateKey: '',
- content: {
- msgtype: 'm.text',
- body: 'some body',
- },
- isAvailable: true,
- formattedTime: '4:06 P.M.',
- })
property var room: ({
pinEvents: mockHelper.promise(),
@@ -67,17 +37,11 @@
}
})
- property var timeline: ListModel {
- ListElement {}
- ListElement {}
-
- function at(index) {
- if (index < 1) {
- return makeLocalEcho(index);
- } else {
- return makeTextEvent(index);
- }
- }
+ property var timeline: QmlHelpers.MatrixRoomTimelineMock {
+ data: [
+ { eventOrTxnId: 'some-txn-id0', isLocalEcho: true },
+ { eventOrTxnId: '$1', isLocalEcho: false },
+ ]
}
ColumnLayout {
diff --git a/src/tests/list-diff-test.cpp b/src/tests/list-diff-test.cpp
new file mode 100644
--- /dev/null
+++ b/src/tests/list-diff-test.cpp
@@ -0,0 +1,46 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2026 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <kazv-defs.hpp>
+#include "list-diff.hpp"
+#include <immer/flex_vector.hpp>
+#include <zug/meta/util.hpp>
+#include <lager/util.hpp>
+#include <QtTest>
+
+class ListDiffTest : public QObject
+{
+ Q_OBJECT
+
+private Q_SLOTS:
+ void testDiff();
+};
+
+void ListDiffTest::testDiff()
+{
+ immer::flex_vector<std::string> last{"a", "b", "c", "d", "e", "f"};
+ immer::flex_vector<std::string> next{"x", "a", "c", "f", "y", "d", "z", "e"};
+ auto diff = listDiff(last, next, [](std::string s) -> std::string { return s; });
+ auto work = last;
+ for (auto act : diff) {
+ lager::match(act)(
+ [&](ListMove m) {
+ auto item = work.at(m.from);
+ work = std::move(work).erase(m.from).insert(m.to, std::move(item));
+ },
+ [&](ListAdd<std::string> a) {
+ work = std::move(work).insert(a.index, a.id);
+ },
+ [&](ListRemove r) {
+ work = std::move(work).erase(r.index);
+ }
+ );
+ }
+ QCOMPARE(work, next);
+}
+
+QTEST_MAIN(ListDiffTest)
+#include "list-diff-test.moc"
diff --git a/src/tests/matrix-room-pinned-events-timeline-test.cpp b/src/tests/matrix-room-pinned-events-timeline-test.cpp
--- a/src/tests/matrix-room-pinned-events-timeline-test.cpp
+++ b/src/tests/matrix-room-pinned-events-timeline-test.cpp
@@ -44,7 +44,7 @@
auto roomList = toUniquePtr(s.session.roomList());
auto room = toUniquePtr(roomList->room(QString::fromStdString(r.roomId)));
auto timeline = toUniquePtr(room->pinnedEventsTimeline());
- QCOMPARE(timeline->count(), 2);
+ QTRY_COMPARE(timeline->count(), 2);
auto e1 = toUniquePtr(timeline->at(1));
QCOMPARE(e1->eventId(), u"$1"_s);
diff --git a/src/tests/matrix-room-timeline-test.cpp b/src/tests/matrix-room-timeline-test.cpp
--- a/src/tests/matrix-room-timeline-test.cpp
+++ b/src/tests/matrix-room-timeline-test.cpp
@@ -12,7 +12,7 @@
#include <factory.hpp>
-#include <matrix-room-timeline.hpp>
+#include <matrix-room-timeline-p.hpp>
#include <matrix-sdk.hpp>
#include <matrix-room-list.hpp>
#include <matrix-room.hpp>
@@ -36,6 +36,8 @@
void testReadReceipts();
void testEdits();
void testIndexOfEvent();
+ void testTotalItems();
+ void testModification();
};
void MatrixRoomTimelineTest::testLocalEcho()
@@ -46,7 +48,7 @@
auto room = toUniquePtr(roomList->room(u"!foo:tusooa.xyz"_s));
auto timeline = toUniquePtr(room->timeline());
- QCOMPARE(timeline->count(), 2);
+ QTRY_COMPARE(timeline->count(), 2);
auto first = toUniquePtr(timeline->at(1)); // reverse order
QCOMPARE(first->isSending(), true);
QCOMPARE(first->isLocalEcho(), true);
@@ -151,12 +153,67 @@
auto room = toUniquePtr(roomList->room(QString::fromStdString(r.roomId)));
auto timeline = toUniquePtr(room->timeline());
- QCOMPARE(timeline->indexOfEvent(u"$1"_s), 4);
+ QTRY_COMPARE(timeline->indexOfEvent(u"$1"_s), 4);
QCOMPARE(timeline->indexOfEvent(u"$2"_s), 3);
QCOMPARE(timeline->indexOfEvent(u"$3"_s), 2);
QCOMPARE(timeline->indexOfEvent(u"$4"_s), -1);
}
+void MatrixRoomTimelineTest::testModification()
+{
+ auto r = makeRoom(withRoomTimeline({
+ makeEvent(withEventId("$1") | withEventKV("/origin_server_ts"_json_pointer, 1000)),
+ makeEvent(withEventId("$2") | withEventKV("/origin_server_ts"_json_pointer, 2000)),
+ makeEvent(withEventId("$3") | withEventKV("/origin_server_ts"_json_pointer, 3000)),
+ }));
+ r.localEchoes = {
+ LocalEchoDesc{"txn1", makeEvent(withEventId("")), LocalEchoDesc::Sending},
+ LocalEchoDesc{"txn2", makeEvent(withEventId("")), LocalEchoDesc::Sending},
+ };
+
+ auto model = SdkModel{makeClient(withRoom(r))};
+ SessionSetup s(model);
+ auto roomList = toUniquePtr(s.session.roomList());
+ auto room = toUniquePtr(roomList->room(QString::fromStdString(r.roomId)));
+ auto timeline = toUniquePtr(room->timeline());
+ QSignalSpy spy(timeline.get(), &MatrixRoomTimeline::countChanged);
+ s.sdk.context().dispatch(UpdateRoomAction{r.roomId, AddToTimelineAction{
+ {
+ makeEvent(withEventId("$xxx") | withEventKV("/origin_server_ts"_json_pointer, 500)),
+ makeEvent(withEventId("$yyy") | withEventKV("/origin_server_ts"_json_pointer, 1500)),
+ makeEvent(withEventId("$zzz") | withEventKV("/origin_server_ts"_json_pointer, 5000)),
+ },
+ "prev-batch-1",
+ true,
+ std::nullopt,
+ }});
+ spy.wait();
+ QCOMPARE(timeline->count(), 8);
+ QCOMPARE(timeline->data(timeline->index(0), Qt::UserRole), u"txn2"_s);
+ QCOMPARE(timeline->data(timeline->index(1), Qt::UserRole), u"txn1"_s);
+ QCOMPARE(timeline->data(timeline->index(2), Qt::UserRole), u"$zzz"_s);
+ QCOMPARE(timeline->data(timeline->index(2), Qt::UserRole + 1), false);
+ QCOMPARE(timeline->data(timeline->index(3), Qt::UserRole), u"$3"_s);
+ QCOMPARE(timeline->data(timeline->index(4), Qt::UserRole), u"$2"_s);
+ QCOMPARE(timeline->data(timeline->index(5), Qt::UserRole), u"$yyy"_s);
+ QCOMPARE(timeline->data(timeline->index(6), Qt::UserRole), u"$1"_s);
+ QCOMPARE(timeline->data(timeline->index(7), Qt::UserRole), u"$xxx"_s);
+}
+
+void MatrixRoomTimelineTest::testTotalItems()
+{
+ EnhancedTotalItems t = static_cast<EnhancedTotalItems>(TotalItems{{"$1", "$2", "$3"}, {"txn1", "txn2"}});
+ QCOMPARE(t.at(0), (TimelineItemDesc{"txn2", true}));
+ QCOMPARE(t.at(1), (TimelineItemDesc{"txn1", true}));
+ QCOMPARE(t.at(2), (TimelineItemDesc{"$3", false}));
+ t.insertInPlace(2, TimelineItemDesc{"$4", false});
+ QCOMPARE(t.at(2), (TimelineItemDesc{"$4", false}));
+ t.insertInPlace(2, TimelineItemDesc{"txn3", true});
+ QCOMPARE(t.at(2), (TimelineItemDesc{"txn3", true}));
+ t.insertInPlace(7, TimelineItemDesc{"$x", false});
+ QCOMPARE(t.at(7), (TimelineItemDesc{"$x", false}));
+}
+
QTEST_MAIN(MatrixRoomTimelineTest)
#include "matrix-room-timeline-test.moc"
diff --git a/src/tests/quick-tests/test-helpers/MatrixRoomTimelineMock.qml b/src/tests/quick-tests/test-helpers/MatrixRoomTimelineMock.qml
new file mode 100644
--- /dev/null
+++ b/src/tests/quick-tests/test-helpers/MatrixRoomTimelineMock.qml
@@ -0,0 +1,64 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2026 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import QtQuick
+
+ListModel {
+ property var data
+ onDataChanged: {
+ clear();
+ for (const i of data) {
+ append(i);
+ }
+ }
+
+ function txnIdFor(i) { return 'some-txn-id' + i; }
+ function eventIdFor(i) { return '$' + i; }
+
+ function makeLocalEcho(i) {
+ return {
+ eventId: '',
+ sender: '',
+ type: 'm.room.message',
+ stateKey: '',
+ content: {
+ msgtype: 'm.text',
+ body: 'some body' + i,
+ },
+ encrypted: false,
+ isState: false,
+ unsignedData: {},
+ isLocalEcho: true,
+ isSending: true,
+ isFailed: false,
+ isAvailable: true,
+ txnId: i,
+ };
+ }
+
+ function makeTextEvent(i) {
+ return {
+ eventId: i,
+ sender: '@foo:tusooa.xyz',
+ type: 'm.room.message',
+ stateKey: '',
+ content: {
+ msgtype: 'm.text',
+ body: 'some body',
+ },
+ isAvailable: true,
+ formattedTime: '4:06 P.M.',
+ };
+ }
+
+ function at(index) {
+ return get(index).isLocalEcho ? makeLocalEcho(get(index).eventOrTxnId) : makeTextEvent(get(index).eventOrTxnId);
+ }
+
+ function byId(id, isLocalEcho) {
+ return isLocalEcho ? makeLocalEcho(id) : makeTextEvent(id);
+ }
+}
diff --git a/src/tests/quick-tests/tst_RoomPage.qml b/src/tests/quick-tests/tst_RoomPage.qml
--- a/src/tests/quick-tests/tst_RoomPage.qml
+++ b/src/tests/quick-tests/tst_RoomPage.qml
@@ -17,26 +17,28 @@
QmlHelpers.TestItem {
id: item
- property var roomTimeline: QtObject {
- property var count: 5
- function at(i) {
- if (i < 2) {
- return {
- isLocalEcho: true,
- };
- }
- if (i < 3) {
+ property var roomTimeline: QmlHelpers.MatrixRoomTimelineMock {
+ data: [
+ { eventOrTxnId: 'txn0', isLocalEcho: true },
+ { eventOrTxnId: 'txn1', isLocalEcho: true },
+ { eventOrTxnId: '$event2', isLocalEcho: false },
+ { eventOrTxnId: '$event3', isLocalEcho: false },
+ { eventOrTxnId: '$event4', isLocalEcho: false },
+ ]
+
+ function makeTextEvent(i) {
+ if (i === '$event2') {
return {
isLocalEcho: false,
sender: '@foo:example.org',
- eventId: `$event${i}`,
+ eventId: i,
};
}
return {
isLocalEcho: false,
sender: '@bar:example.org',
- eventId: `$event${i}`,
+ eventId: i,
};
}
}
@@ -66,7 +68,7 @@
})
property var roomScrollToLatest: JsHelpers.factory.room({
- timeline: () => Qt.createComponent("QtQml.Models", "ListModel").createObject()
+ timeline: () => item.roomTimeline
})
Component {
diff --git a/src/tests/quick-tests/tst_RoomPinnedEventsPage.qml b/src/tests/quick-tests/tst_RoomPinnedEventsPage.qml
--- a/src/tests/quick-tests/tst_RoomPinnedEventsPage.qml
+++ b/src/tests/quick-tests/tst_RoomPinnedEventsPage.qml
@@ -19,25 +19,11 @@
return item.timeline;
},
})
- 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 timeline: ListModel {
- ListElement {}
- ListElement {}
-
- function at(index) {
- return makeTextEvent(index);
- }
+ property var timeline: QmlHelpers.MatrixRoomTimelineMock {
+ data: [
+ { eventOrTxnId: '$0', isLocalEcho: false },
+ { eventOrTxnId: '$1', isLocalEcho: false },
+ ]
}
Kazv.RoomPinnedEventsPage {
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
@@ -20,37 +20,6 @@
id: item
signal loadEventRequested(string eventId)
- property var makeLocalEcho: (i) => ({
- eventId: '',
- sender: '',
- type: 'm.room.message',
- stateKey: '',
- content: {
- msgtype: 'm.text',
- body: 'some body' + i,
- },
- encrypted: false,
- isState: false,
- unsignedData: {},
- isLocalEcho: true,
- isSending: true,
- isFailed: false,
- isAvailable: true,
- txnId: 'some-txn-id' + i,
- })
-
- property var makeTextEvent: (i) => ({
- eventId: '$' + i,
- sender: '@foo:tusooa.xyz',
- type: 'm.room.message',
- stateKey: '',
- content: {
- msgtype: 'm.text',
- body: 'some body',
- },
- isAvailable: true,
- formattedTime: '4:06 P.M.',
- })
property var room: ({
pinEvents: mockHelper.promise(),
@@ -74,17 +43,11 @@
property var backfillRequested: mockHelper.noop()
- property var timeline: ListModel {
- ListElement {}
- ListElement {}
-
- function at(index) {
- if (index < 1) {
- return makeLocalEcho(index);
- } else {
- return makeTextEvent(index);
- }
- }
+ property var timeline: QmlHelpers.MatrixRoomTimelineMock {
+ data: [
+ { eventOrTxnId: 'some-txn-id0', isLocalEcho: true },
+ { eventOrTxnId: '$1', isLocalEcho: true },
+ ]
}
TestCase {

File Metadata

Mime Type
text/plain
Expires
Wed, Oct 7, 4:28 PM (22 h, 45 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1784112
Default Alt Text
D366.1791415718.diff (29 KB)

Event Timeline